Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## 3.3.0 — 2026-10-19

- **heinzel knows NetBSD.** `uname -s` = `NetBSD` used
to match no branch of the OS detection.
`rules/netbsd.md` covers pkgsrc and `pkgin`, npf,
`rc.conf` without `sysrc`, and the traps found on a
NetBSD 11.0 VM: commands over SSH lack `/sbin` in
`PATH` and report installed tools as missing,
`npfctl start` cuts the running session, `sockstat`
needs `-n` for port numbers, and ntpd never syncs
where an IPv6 route leads nowhere. Port, activity
and version checks gained a NetBSD case.

## 3.2.2 — 2026-10-18

- **Aliased `cp`, `mv` and `rm` no longer trip
Expand Down
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ when working with code in this repository.
## Project

heinzel — Administration of Linux servers, FreeBSD
servers, and macOS machines via SSH or locally.
Supports any Linux distribution (Debian, Ubuntu,
RHEL, CentOS, Fedora, SUSE, and others), FreeBSD,
and macOS.
and NetBSD servers, and macOS machines via SSH or
locally. Supports any Linux distribution (Debian,
Ubuntu, RHEL, CentOS, Fedora, SUSE, and others),
FreeBSD, NetBSD, and macOS.

## Ethics (above every other rule)

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ your OS, explains each one, and waits for your approval. It
backs up configs, tests before applying, and remembers every
server it has worked on.

It manages Linux, FreeBSD and macOS, remote over SSH or on
It manages Linux, FreeBSD, NetBSD and macOS, remote over SSH or on
the local machine, and runs in
[Claude Code](https://docs.anthropic.com/en/docs/claude-code),
[OpenCode](https://opencode.ai) or any terminal AI tool that
Expand Down Expand Up @@ -375,6 +375,7 @@ to a 1.x tag, run `bin/heinzel-update --unpin` first.
| SUSE | openSUSE, SLES | `rules/suse.md` |
| macOS | Apple Silicon and Intel | `rules/macos.md` |
| FreeBSD | all versions | `rules/freebsd.md` |
| NetBSD | 10.x and 11.x | `rules/netbsd.md` |
| Unraid | observed on 7.3 | `rules/unraid.md` |

Other distributions work with general best practices.
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.2.2
3.3.0
12 changes: 12 additions & 0 deletions rules/activity-check.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ Note: this shows the last 20 matches, not a strict
(`messages.0`). Older rotated logs are usually
compressed; mention the limitation if relevant.

**NetBSD:** as FreeBSD, but with `grep -a`. After an
unclean shutdown `/var/log/messages` can hold NUL
bytes, and plain `grep` then prints only `Binary
file matches`:

```
grep -ah heinzel /var/log/messages 2>/dev/null \
| tail -20
```

Rotated copies are gzipped (`messages.0.gz`).

If the command returns nothing — and it actually ran,
and journal visibility is not limited (see above) —
skip silently: no activity to report.
Expand Down
184 changes: 184 additions & 0 deletions rules/netbsd.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# NetBSD

Rules for NetBSD (10.x and 11.x, the supported
series).

## PATH Over SSH

A command sent as `ssh root@host '<cmd>'` runs with
`PATH=/usr/bin:/bin:/usr/pkg/bin:/usr/local/bin`.
`/sbin` and `/usr/sbin` are missing: `npfctl`,
`ifconfig`, `route`, `ping`, `ntpq`, `pkg_add` and
`service` answer `not found` although they are
installed; only `/root/.profile` adds them, and a
command over SSH does not read it. Start every
remote command with
`export PATH=/sbin:/usr/sbin:/usr/pkg/sbin:$PATH;`
and never read `not found` as "not installed"
without it.

## Package Manager

- Packages come from pkgsrc and live under
`/usr/pkg/` (config in `/usr/pkg/etc/`). A fresh
install has none: no `pkgin`, `sudo`, `bash` or
`lsof`.
- Base tools: `pkg_info` (installed),
`pkg_add <pkg>`, `pkg_delete <pkg>`.
- `pkgin` is the apt-like front end. Installing it
asks first, like any package:
```
rel=$(uname -r | cut -f '1 2' -d. | cut -f 1 -d_)
cdn=https://cdn.NetBSD.org/pub/pkgsrc/packages/NetBSD
PKG_PATH="$cdn/$(uname -p)/$rel/All" pkg_add pkgin
```
- With `pkgin`: `pkgin update`, `pkgin -n upgrade`
(dry run), `pkgin -y install <pkg>`,
`pkgin search <name>`.
- Vulnerabilities: `pkg_admin fetch-pkg-vulnerabilities`,
then `pkg_admin audit`.

## Version Detection

- `uname -r` — release (`11.0`); `uname -m` and
`uname -p` — port and CPU (`evbarm`, `aarch64`).
- `head -1 /etc/release` — `NetBSD 11.0/evbarm`.
- There is no `/etc/os-release`.
- Lifecycle: netbsd.org/releases lists the
supported series; older ones get no security
fixes.

## Firewall

- **Expected:** `npf`. Config `/etc/npf.conf`,
enabled by `npf=YES` in `/etc/rc.conf`. No
default config exists; the base example is
`/usr/share/examples/npf/host-npf.conf`.
- Status: `npfctl show` (`filtering: active`).
- **Mandatory before any load:**
`npfctl validate /etc/npf.conf`.
- Load: `npfctl reload`; on/off: `npfctl start`,
`npfctl stop`.
- **`npfctl start` cuts the running SSH session.**
npf has no state for a connection opened before
it was active, and `pass stateful … port ssh`
matches only the opening SYN. Arm the rollback
timer (`rules/rollback-timer.md`, "anything
else" variant) and start npf in the same detached
command, then confirm with a fresh login.
- Minimal inbound-SSH-only config (list every port
sshd listens on):
```
$ext_if = "vioif0" # see ifconfig -l
alg "icmp"
group "external" on $ext_if {
pass stateful in proto tcp to ifaddrs($ext_if) port ssh
}
group default {
block all
pass on lo0 all
pass in family inet4 proto udp from any port bootps to any port bootpc
pass family inet6 proto ipv6-icmp all
pass stateful out all
}
```
Without the DHCP line the host loses its lease;
without ICMPv6 it loses IPv6 neighbours.
- `npfctl start` may warn that it cannot load the
`bpfjit` module. The filter still works.
- NetBSD 11 renamed blacklistd to **blocklistd**
(`/sbin/blocklistd`).

## Automatic Security Updates

- Nothing applies updates on its own, base or
packages. Flag this to the user.
- `/etc/security` runs daily and audits installed
packages (`check_pkg_vulnerabilities=YES` by
default) and mails root: the notify-only
baseline. Check `/etc/security.conf` keeps it on.
- **Base system:** binary updates to a new point
release (11.0 → 11.1) go through `sysupgrade`
(pkgsrc `sysutils/sysupgrade`). It replaces the
kernel and needs a reboot: a decision for the
user, never a default.
- **Packages:** `pkgin -n upgrade` to see what is
pending, `pkgin upgrade` only on request.

## Service Manager

- `rc.d`, like FreeBSD, but **no `sysrc`**.
- Control: `service <name> start|stop|restart|status`
or `/etc/rc.d/<name> <cmd>`. `onestart` runs a
service that is not enabled.
- Enable: `<name>=YES` in `/etc/rc.conf`
(`sshd=YES`, not `sshd_enable`). Back up, then
append; the last assignment wins, and the
installer often wrote the variable already.
- Flags: `<name>_flags="…"`; defaults in
`/etc/defaults/rc.conf`.
- Enabled services: `service -e`.
- Own scripts in `/etc/rc.d/` need the
`PROVIDE` / `REQUIRE` / `BEFORE` header that
`rcorder` sorts by.

## Networking

- `ifconfig`, `route -n show`, no `ip`.
- DHCP is `dhcpcd` (`dhcpcd=YES`). Hooks go in
`/etc/dhcpcd.exit-hook`.
- Static address: `/etc/ifconfig.<if>` (one line:
`inet 192.168.1.10/24`) plus `defaultroute=` in
`/etc/rc.conf`; the rollback timer applies.
- Hostname: `hostname=` in `/etc/rc.conf`. Without
one the host is `Amnesiac` and **postfix fails at
boot** (`unable to use my own hostname`).
- `sockstat -l` prints service names and a dot
before the port (`*.ssh`). For a port check use
`sockstat -n -4 -6 -l | grep -E '\.<port> '`.

## Logs

- Syslog only, no `journalctl`: `/var/log/messages`,
`/var/log/authlog`; rotated copies are gzipped
(`messages.0.gz`).
- After an unclean shutdown `messages` can hold NUL
bytes, and `grep` then prints only `Binary file
matches`. Use `grep -a`, also for the heinzel
journal (`grep -a heinzel /var/log/messages`).

## Common Pitfalls

- **No `sudo`, no `doas`** in base. The sudoers line
is Heinzel Pro's work.
- **No `daemon(8)`**: the rollback timer uses the
`nohup` variant. Disarm with `pkill -F`, never
`pkill -f '<pattern>'`: the pattern also matches
the shell running the command and kills heinzel's
own session.
- **ntpd and IPv6**: where the host has an IPv6
route that does not reach the internet (UTM's
shared network, many home routers), ntpd solicits
only IPv6 pool servers and never syncs. Check with
`ntpdate -q -4` against `-6`; the fix is
`ntpd_flags="-4"` and `ntpdate_flags="-4 -b -s"`.
`ntpq` then needs `ntpq -4 -pn 127.0.0.1`.
- **The console is the serial port** on evbarm
(`constty` in `/etc/ttys`). getty prints
`/etc/issue` only with `if=/etc/issue` in the
`default` entry of `/etc/gettytab`.

## Boot

`/boot` is the EFI system partition (msdos), with
`/boot/EFI/BOOT/bootaa64.efi` and an optional
`/boot/boot.cfg`. Boot loader, `boot.cfg` and EFI
entries are Heinzel Pro's (`CLAUDE.md` → What
Heinzel does not do). Read them, report them.

One known case to report: NetBSD 11.0 under Apple's
Hypervisor.framework (UTM, QEMU with HVF) panics at
boot in `vmt_probe` (`Illegal Instruction`), and the
display only shows "Display output is not active".
The fix is `userconf=disable vmt` in
`/boot/boot.cfg`; `vmt*` does not match.
14 changes: 12 additions & 2 deletions rules/os-detection.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ its OS.
(see `rules/dns-aliases.md`). If the hostname is
an alias for a known server, skip OS detection.

1. Determine Linux, macOS, or FreeBSD: `uname -s`
1. Determine Linux, macOS, FreeBSD or NetBSD:
`uname -s`

2. **If Linux** — detect distro and version:
```
Expand Down Expand Up @@ -48,7 +49,16 @@ its OS.
(`sysctl` for CPU/RAM, `df -h`,
`zpool status` if ZFS).

5. Create a server memory file.
5. **If NetBSD** — detect version and arch:
```
uname -r && uname -p
```
Read `rules/netbsd.md` first: commands over SSH
lack `/sbin` and `/usr/sbin` in `PATH`. Gather
hardware info (`sysctl hw.model hw.ncpu
hw.physmem64`, `df -h`).

6. Create a server memory file.

## On subsequent connections

Expand Down
10 changes: 10 additions & 0 deletions rules/port-check.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ sockstat -4 -6 -l | grep -E ':<port>\b'
No root needed. Shows user, command, PID, protocol,
and local address.

### NetBSD

```bash
sockstat -n -4 -6 -l | grep -E '\.<port> '
```

`-n` is required: without it NetBSD prints service
names (`*.ssh`). The port follows a dot, not a
colon.

### macOS

```bash
Expand Down
2 changes: 1 addition & 1 deletion rules/rollback-timer.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ guard hook denies a redirect onto a computed target.

Not `at`: `atrun` runs from cron every five
minutes, so the window becomes unpredictable.
- Anything else (macOS, OpenRC):
- Anything else (macOS, NetBSD, OpenRC):

nohup sh -c 'echo $$ > /var/run/heinzel-rollback-<ts>.pid; \
sleep 120; sh /var/backups/heinzel/rollback-<ts>.sh' \
Expand Down
2 changes: 2 additions & 0 deletions rules/version-check.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ end-of-life date. Use official sources:
- **Ubuntu:** ubuntu.com/about/release-cycle
- **RHEL/CentOS:** Red Hat lifecycle policy
- **FreeBSD:** freebsd.org/security
- **NetBSD:** netbsd.org/releases (supported
series)
- **SUSE:** suse.com/lifecycle
- **macOS:** Apple typically supports current and
two prior major versions
Expand Down
Loading