feat: production-openbsd v0.1 scaffold

Sister to s8n/production-deb. Edge-box config + provision script for
running the OpenBSD-edge role per s8n/production-setup-audit Topology 02.

v0.1 = stock OpenBSD install ISO (interactive, 5 min) + scripted provision
from onyx. Autoinstall ISO build deferred to v0.2.

Layout:
  README.md                    workflow + service mapping (Debian → OpenBSD)
  flash.sh                     burn stock install76.iso to USB
  etc/                         pf / relayd / acme-client / unbound /
                               hostname.wg0.example / sshd_config / doas.conf
  scripts/
    provision.sh               from onyx: SSH+git clone+run install.sh
    install.sh                 on edge: copy /etc/*, validate, restart, cron
    cert-renew-check.sh        weekly LE renewal
    read-logs.sh               pull /var/log/* for offline diagnostics
  docs/
    setup-checklist.md         7-phase first-time install walkthrough

Hardware target: Dell Precision T5600 per
  s8n/production-setup-audit/hardware/dell-t5600.md

WG mesh: 10.10.10.0/29 between edge (.1) and nullstone (.2). UDP 51820.
Keys generated per-host (NEVER committed to repo).

Public traffic flow after migration:
  Internet → router → edge T5600 (relayd TLS term) → wg0 →
  nullstone Traefik (10.10.10.2:8443, private only)

CVE delta vs single-host Debian: regreSSHion + xz backdoor mitigated;
public IP runs OpenBSD base only — no systemd, no glibc, no Docker.
This commit is contained in:
obsidian-ai 2026-05-08 14:10:29 +01:00
commit be77f1eb2f
15 changed files with 675 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
*.swp
.DS_Store
.secrets/
secrets/
*.key
*.pem

88
README.md Normal file
View file

@ -0,0 +1,88 @@
# production-openbsd
Edge-box OpenBSD config + provision script. Sister to `s8n/production-deb`.
Target role per [`s8n/production-setup-audit`](https://git.s8n.ru/s8n/production-setup-audit)
Topology 02: **OpenBSD bare metal on Dell Precision T5600** running pf +
relayd + acme-client + unbound + WireGuard. Public 80/443/22 land here;
nullstone (Debian compute) sits behind the WG tunnel on private addrs only.
## v0.1 workflow (manual interactive install + scripted provision)
OpenBSD's installer is small and interactive (~5 min). First-pass workflow:
1. **Burn stock OpenBSD install ISO** (`install76.iso` from
<https://www.openbsd.org/76.html>) to a USB. Boot Dell, run installer.
Pick:
- Hostname (e.g. `flintstone`)
- Network interface (em0/em1 — onboard Intel I217 typically)
- Disk encryption: **yes** — softraid bioctl-style FDE
- Sets: `bsd, bsd.mp, base, comp, man` (skip `xenocara` — no GUI on edge)
- Root SSH: yes (key-only configured later by provision.sh)
- Default user: `user`
2. **Reboot, log in as user, copy public SSH key** for onyx into
`~/.ssh/authorized_keys`.
3. **From onyx, run** `./scripts/provision.sh user@<edge-ip>` — this clones
the repo onto the edge box, runs `install.sh` to apply pf/relayd/acme/
unbound/wg configs.
Edge box now ready to take public 80/443 traffic.
## v0.2 deferred — autoinstall ISO
Build an OpenBSD install ISO with `auto_install.conf` baked so install runs
unattended like production-deb's preseed. Defer until v0.1 proven on this
hardware.
## Layout
```
README.md
provision.sh one-shot provision: copy configs, restart services
flash.sh flash stock OpenBSD install ISO to USB
etc/
pf.conf default-deny + WAN→LAN nat + WG passthrough
relayd.conf TLS terminator → backend nullstone WG IP
acme-client.conf LE certs via Gandi DNS-01
unbound.conf recursive DNS for tunnel + LAN
hostname.wg0.example WG interface (key material handed-out separately)
doas.conf minimal admin
sshd_config key-only, no root login
scripts/
provision.sh push configs + apply
read-logs.sh pull /var/log/* from edge box for offline review
cert-renew-check.sh verify acme-client renewal cron working
fetch-keys.sh generate WG keypair + record pubkey
docs/
setup-checklist.md step-by-step install + provision walkthrough
service-map.md which OpenBSD service maps to which Debian role
migration-from-deb.md how to peel off TLS+DNS+ACME from nullstone
```
## Service mapping (Debian → OpenBSD)
| Debian (currently nullstone) | OpenBSD (T5600 edge) |
|------------------------------|------------------------|
| Traefik (TLS termination + reverse proxy) | relayd |
| acme-companion / Traefik LE | acme-client |
| Pi-hole DNS recursive | unbound (Pi-hole stays on Pi for LAN ad-block) |
| Tailscale + Headscale (admin plane) | WireGuard direct (only this 2-host link) |
| ufw / nftables | pf |
| sshd hardened | sshd hardened (different impl, smaller surface) |
Headscale stays on nullstone — admin plane unchanged. Edge box only handles
public traffic + WG tunnel into nullstone's private subnet.
## Why not autoinstall yet
OpenBSD install is fast enough interactive that automating the first pass
isn't worth the autoinstall toolchain learning. Once the manual config
proves out on T5600 hardware, v0.2 will bake the answers into a custom
miniroot for repeatable rebuilds.
## Forgejo remote
`ssh://git@192.168.0.100:222/s8n/production-openbsd.git`

111
docs/setup-checklist.md Normal file
View file

@ -0,0 +1,111 @@
# Setup checklist — first-time edge box install
## Prerequisites
- [ ] Dell T5600 powered on, NICs cabled (em0 to WAN-side, em1 to LAN switch)
- [ ] OpenBSD 7.x install ISO burned to USB (`install76.iso`, ~600 MB)
- [ ] Onyx has SSH pubkey ready: `~/.ssh/id_ed25519.pub`
- [ ] WG pubkey of nullstone on hand (run `wg show wg0 public-key` on nullstone
after `apt install wireguard-tools` and key generation)
## Phase 1 — install OpenBSD (interactive, ~5 min)
1. Boot Dell from USB
2. At `(I)nstall, (U)pgrade, (A)utoinstall, or (S)hell?` choose **I**
3. Hostname: `flintstone` (or chosen edge codename)
4. Network — pick `em0` (or whichever is WAN-side), use DHCP for now
5. **Set FDE: yes.** Choose strong passphrase (don't use `123` here — this
passphrase is typed at every boot and protects all data at rest)
6. Sets to install: `bsd bsd.mp base76 comp76 man76` (skip `xenocara`,
`xfont`, `xserv`, `xshare`)
7. Set timezone Europe/London
8. Add user: `user`, full name `user`, password (strong)
9. Allow root SSH: **no** (we'll harden via sshd_config in Phase 3)
10. Set primary boot disk: yes
11. Reboot
## Phase 2 — first login + SSH key
1. Log in at console as `user`
2. Find IP: `ifconfig em0 inet | awk '/inet / {print $2}'` — record this
3. From onyx: `ssh-copy-id user@<edge-ip>` to push your pubkey
4. From onyx: `ssh user@<edge-ip>` to confirm key auth works
## Phase 3 — provision (from onyx)
```bash
cd ~/projects/production-openbsd
./scripts/provision.sh user@<edge-ip>
```
This runs `install.sh` on the edge box, which:
- Installs `acme-client wireguard-tools git rsync`
- Backs up current /etc configs
- Copies repo's `etc/*` into `/etc/`
- Validates with `pfctl -n`, `relayd -n`, `unbound-checkconf`
- Enables + reloads pf, relayd, unbound, sshd
- Adds weekly cron for cert-renew
Verify on edge:
```bash
ssh user@<edge-ip> 'doas pfctl -sr | head; doas rcctl ls on'
```
## Phase 4 — WireGuard mesh setup (manual key exchange)
On the edge box:
```bash
doas mkdir -p /etc/wg && doas chmod 700 /etc/wg
cd /tmp && openssl rand -base64 32 > edge.key
wg pubkey < edge.key > edge.pub
doas mv edge.{key,pub} /etc/wg/
cat /etc/wg/edge.pub # → paste this into nullstone's wg config
```
On nullstone (Debian):
```bash
sudo apt install wireguard-tools
sudo mkdir -p /etc/wireguard && sudo chmod 700 /etc/wireguard
cd /tmp && openssl rand -base64 32 > nullstone.key
wg pubkey < nullstone.key > nullstone.pub
sudo mv nullstone.{key,pub} /etc/wireguard/
cat /etc/wireguard/nullstone.pub # → paste this into edge's hostname.wg0
```
On edge box:
```bash
doas cp /tmp/production-openbsd/etc/hostname.wg0.example /etc/hostname.wg0
doas vi /etc/hostname.wg0
# → replace NULLSTONE_PUB_KEY_HERE with nullstone.pub content
doas sh /etc/netstart wg0
ifconfig wg0 # confirm interface up with 10.10.10.1
ping 10.10.10.2 # should reach nullstone after its config goes up
```
On nullstone, configure peer side (sister steps; see
`s8n/production-deb/docs/wg-mesh.md` once that's written).
## Phase 5 — first cert
Once WG up + relayd running:
```bash
ssh user@<edge-ip> 'doas acme-client -v s8n.ru'
ssh user@<edge-ip> 'doas acme-client -v veilor.uk'
ssh user@<edge-ip> 'doas rcctl reload relayd'
curl -I https://s8n.ru
```
## Phase 6 — switch public traffic
On the GL.iNet router admin: change port-forwards 80/443 from
`192.168.0.100` (nullstone) to edge T5600 LAN IP.
Test from external: `curl -I https://s8n.ru` should now hit edge relayd,
which terminates TLS, forwards over wg0 to nullstone Traefik on 8443.
## Phase 7 — clean up nullstone public bind
On nullstone Traefik: change listen interfaces from `*:80,*:443` to
`10.10.10.2:8443`. Restart Traefik. Verify with `ss -ltnp | grep 8443`.
Done. Public traffic now: Internet → router → edge T5600 (OpenBSD relayd) →
WG tunnel → nullstone Traefik → Docker stack.

38
etc/acme-client.conf Normal file
View file

@ -0,0 +1,38 @@
# /etc/acme-client.conf — Let's Encrypt via DNS-01 (Gandi)
#
# DNS-01 chosen because:
# - Doesn't expose port 80 to public during challenge
# - Allows wildcard certs (*.s8n.ru, *.veilor.uk)
# - Works behind WAF / restricted firewall
#
# Gandi LiveDNS API token in env var GANDI_TOKEN (set in rc.conf.local or
# pass via doas wrapper). Do NOT commit token to repo.
#
# Run: acme-client -v <domain>
# Auto-renew: weekly cron (see scripts/cert-renew-check.sh)
authority letsencrypt {
api url "https://acme-v02.api.letsencrypt.org/directory"
account key "/etc/acme/letsencrypt-privkey.pem"
}
# === s8n.ru wildcard ===
domain s8n.ru {
alternative names { "*.s8n.ru" "s8n.ru" }
domain key "/etc/ssl/private/s8n.ru.key"
domain certificate "/etc/ssl/s8n.ru.crt"
domain full chain certificate "/etc/ssl/s8n.ru.fullchain.pem"
sign with letsencrypt
challengedir "/var/www/acme"
# DNS-01 hook script: see scripts/gandi-dns-hook.sh
}
# === veilor.uk wildcard ===
domain veilor.uk {
alternative names { "*.veilor.uk" "veilor.uk" }
domain key "/etc/ssl/private/veilor.uk.key"
domain certificate "/etc/ssl/veilor.uk.crt"
domain full chain certificate "/etc/ssl/veilor.uk.fullchain.pem"
sign with letsencrypt
challengedir "/var/www/acme"
}

10
etc/doas.conf Normal file
View file

@ -0,0 +1,10 @@
# /etc/doas.conf — minimal sudo replacement on OpenBSD
# user = primary admin, can do anything with password
permit persist user as root
# Common operational commands without password (for cron + scripts)
permit nopass user as root cmd /usr/sbin/rcctl
permit nopass user as root cmd /usr/local/sbin/acme-client
permit nopass user as root cmd /sbin/pfctl args -nf /etc/pf.conf
permit nopass user as root cmd /bin/sh args /usr/local/sbin/cert-renew-check.sh

22
etc/hostname.wg0.example Normal file
View file

@ -0,0 +1,22 @@
# /etc/hostname.wg0 — WireGuard tunnel to nullstone
#
# Generate keys (do this on the edge box, NOT in this repo):
# doas openssl rand -base64 32 > /etc/wg/edge.key
# chmod 600 /etc/wg/edge.key
# wg pubkey < /etc/wg/edge.key > /etc/wg/edge.pub
#
# Get nullstone's wg pubkey separately and paste below.
#
# Then: doas mv hostname.wg0.example /etc/hostname.wg0, edit values, sh /etc/netstart wg0
inet 10.10.10.1 255.255.255.248 # /29 subnet, edge = .1
mtu 1420
!/usr/local/bin/wg set wg0 \
listen-port 51820 \
private-key /etc/wg/edge.key \
peer NULLSTONE_PUB_KEY_HERE= \
endpoint 192.168.0.100:51820 \
allowed-ips 10.10.10.2/32 \
persistent-keepalive 25
!route -q add -net 10.10.10.0/29 10.10.10.1
up

68
etc/pf.conf Normal file
View file

@ -0,0 +1,68 @@
# /etc/pf.conf — edge box default-deny + nat + WG passthrough
#
# Layout:
# egress = WAN-side NIC (public IP from ISP/router)
# lan = LAN-side NIC (192.168.0.0/24, internal LAN)
# wg0 = WireGuard tunnel to nullstone (private /29 subnet, e.g. 10.10.10.0/29)
#
# Adjust interface names per `ifconfig` output on the T5600 (likely em0+em1).
# === Macros ===
ext_if = "em0" # WAN-side
int_if = "em1" # LAN-side
wg_if = "wg0"
nullstone = "10.10.10.2" # nullstone over WG; edge is 10.10.10.1
# === Tables ===
# Bogus / non-routable; martians; tor-exit if you want to block (left empty)
table <bogons> persist file "/etc/pf-bogons"
table <bruteforce> persist
# === Options ===
set skip on lo
set block-policy drop
set loginterface egress
set syncookies adaptive (start 25%, end 12%)
# === Normalisation ===
match in all scrub (no-df random-id max-mss 1440)
# === Default deny ===
block in log all
block out log all
pass out quick on $ext_if from ($ext_if:0) keep state
# === Anti-spoof / bogon drop on WAN ===
antispoof quick for { $ext_if $int_if }
block in quick on $ext_if from <bogons> to any
block in quick on $ext_if from any to <bogons>
# === Inbound: 80/443 ports → relayd → backend over wg0 ===
# relayd binds to ($ext_if) on these ports; pass traffic to it explicitly.
pass in on $ext_if proto tcp to ($ext_if) port { 80 443 } \
flags S/SA modulate state \
(max-src-conn 100, max-src-conn-rate 60/10, \
overload <bruteforce> flush global)
# === Inbound SSH: rate-limit + bruteforce trap ===
pass in on $ext_if proto tcp to ($ext_if) port 22 \
flags S/SA modulate state \
(max-src-conn 5, max-src-conn-rate 3/30, \
overload <bruteforce> flush global)
block in quick from <bruteforce>
# === WireGuard: allow UDP 51820 from anywhere (handshake) ===
pass in on $ext_if proto udp to ($ext_if) port 51820 keep state
# === Tunnel traffic: pass freely between edge and nullstone ===
pass on $wg_if all keep state
pass quick proto { tcp udp icmp } from $nullstone to ($wg_if) keep state
# === LAN side: trust LAN, allow outbound + DNS to unbound ===
pass on $int_if all keep state
# === ICMP for path-MTU + diagnostics ===
pass inet proto icmp icmp-type { echoreq unreach timex } keep state
# === Logging ===
# `pflog0` interface captures matched-`log` rules. tcpdump -ni pflog0 to watch.

63
etc/relayd.conf Normal file
View file

@ -0,0 +1,63 @@
# /etc/relayd.conf — TLS terminator + reverse proxy to nullstone backend
#
# Listens on egress IP for 80 (redirect to https) + 443 (TLS termination)
# Forwards decrypted requests to nullstone's WG IP on configured backend ports.
#
# Reload: rcctl reload relayd
# Test config: relayd -n
# === Macros ===
ext_addr = "egress:0"
backend = "10.10.10.2" # nullstone over wg0
backend_port = "8443" # nullstone Traefik listens on this internal port
# === Logging ===
log connection
log state changes
# === Tables ===
table <backend> { $backend }
# === HTTP → HTTPS redirect ===
http protocol "http_redirect" {
return error
match request header set "Strict-Transport-Security" \
value "max-age=31536000; includeSubDomains"
block
pass quick path "/.well-known/acme-challenge/*" forward to <acme-server>
}
# === HTTPS terminator ===
http protocol "https_term" {
return error
match request header set "X-Forwarded-For" value "$REMOTE_ADDR"
match request header set "X-Forwarded-Proto" value "https"
match request header set "X-Forwarded-Port" value "443"
match response header set "Strict-Transport-Security" \
value "max-age=31536000; includeSubDomains"
match response header set "X-Frame-Options" value "DENY"
match response header set "X-Content-Type-Options" value "nosniff"
match response header set "Referrer-Policy" value "strict-origin-when-cross-origin"
match response header remove "Server"
tls keypair s8n.ru
tls keypair veilor.uk
pass
}
# === acme-client target (port 80 only, for HTTP-01 challenge if you ever need it;
# DNS-01 challenge via Gandi is the primary path so this is a fallback) ===
table <acme-server> { 127.0.0.1 }
# === Relay: HTTP redirect on 80 ===
relay www_http {
listen on $ext_addr port 80
protocol "http_redirect"
forward to <acme-server> port 8081 check tcp
}
# === Relay: HTTPS terminator forwarding to nullstone ===
relay www_https {
listen on $ext_addr port 443 tls
protocol "https_term"
forward to <backend> port $backend_port check http "/" code 200
}

30
etc/sshd_config Normal file
View file

@ -0,0 +1,30 @@
# /etc/ssh/sshd_config drop-in (or replace upstream) — edge box hardening
# OpenBSD already ships a sane sshd_config; this overrides a few keys.
PermitRootLogin no
PasswordAuthentication no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
UsePAM no
X11Forwarding no
PermitEmptyPasswords no
PermitTunnel no
GatewayPorts no
AllowAgentForwarding no
AllowTcpForwarding yes # WG-tunnel access via SSH for emergencies
LoginGraceTime 30
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
# Allow only the user account; root locked
AllowUsers user
# Use only modern crypto
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org
HostKeyAlgorithms ssh-ed25519,ssh-ed25519-cert-v01@openssh.com
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com

67
etc/unbound.conf Normal file
View file

@ -0,0 +1,67 @@
# /var/unbound/etc/unbound.conf — recursive DNS resolver
#
# Listens on:
# 127.0.0.1:53 — for local apps (acme-client, relayd)
# 10.10.10.1:53 — for nullstone over WG tunnel
# 192.168.0.50:53 — for LAN clients (LAN IP of T5600; adjust)
#
# Pi-hole stays on the Pi for LAN ad-blocking; Pi-hole's upstream is set
# to THIS unbound instance (10.10.10.1 via tunnel OR LAN IP direct).
#
# Reload: rcctl reload unbound
# Test: dig @127.0.0.1 example.com
server:
interface: 127.0.0.1
interface: 10.10.10.1
interface: 192.168.0.50
# Restrict who can query
access-control: 127.0.0.0/8 allow
access-control: 10.10.10.0/29 allow # WG mesh
access-control: 192.168.0.0/24 allow # LAN
access-control: 0.0.0.0/0 refuse # block public recursive abuse
# Hide identity
hide-identity: yes
hide-version: yes
# Cache + perf
msg-cache-size: 64m
rrset-cache-size: 128m
cache-min-ttl: 60
cache-max-ttl: 86400
prefetch: yes
prefetch-key: yes
# DNSSEC validation
auto-trust-anchor-file: "/var/unbound/db/root.key"
val-clean-additional: yes
# Privacy
qname-minimisation: yes
aggressive-nsec: yes
minimal-responses: yes
do-not-query-localhost: no # allow forwarding to local Pi-hole if you do that later
# Drop common bogus
private-address: 10.0.0.0/8
private-address: 172.16.0.0/12
private-address: 192.168.0.0/16
# ... but allow our own ranges to be returned in answers
private-domain: "s8n.ru"
private-domain: "veilor.uk"
# Local zone for internal hosts (nullstone, edge, etc.) — fill from /etc/hosts
local-zone: "s8n.ru." typetransparent
local-data: "edge.s8n.ru. IN A 10.10.10.1"
local-data: "nullstone.s8n.ru. IN A 10.10.10.2"
# Logs
use-syslog: yes
log-queries: no
log-replies: no
remote-control:
control-enable: yes
control-interface: /var/run/unbound.sock

16
flash.sh Executable file
View file

@ -0,0 +1,16 @@
#!/usr/bin/env bash
# flash.sh — flash stock OpenBSD install ISO to USB
# Usage: ./flash.sh /dev/sdX
set -euo pipefail
DEV="${1:-}"
[[ -n "$DEV" && -b "$DEV" ]] || { echo "Usage: $0 /dev/sdX"; exit 1; }
case "$DEV" in /dev/nvme*|/dev/sda|/dev/mmcblk*|/dev/vd*) echo "ERR: refusing internal $DEV" >&2; exit 2;; esac
URL="https://cdn.openbsd.org/pub/OpenBSD/7.6/amd64/install76.iso"
ISO=/tmp/install76.iso
[[ -f "$ISO" ]] || { echo "[*] downloading $URL"; curl -fL -o "$ISO" "$URL"; }
echo "About to flash $ISO -> $DEV. Type yes:"
read -r ANS; [[ "$ANS" == "yes" ]] || exit 1
sudo dd if="$ISO" of="$DEV" bs=4M status=progress conv=fsync oflag=direct
sudo sync
sudo eject "$DEV"
echo "EJECTED — pull, plug into edge box, install"

28
scripts/cert-renew-check.sh Executable file
View file

@ -0,0 +1,28 @@
#!/bin/sh
# cert-renew-check.sh — weekly via cron; renew LE certs near expiry
# Logs to /var/log/cert-renew.log
LOG=/var/log/cert-renew.log
echo "[$(date -u +%FT%TZ)] cert-renew-check start" >>"$LOG"
DOMAINS="s8n.ru veilor.uk"
RC=0
for d in $DOMAINS; do
if /usr/local/sbin/acme-client -v "$d" >>"$LOG" 2>&1; then
echo "[$(date -u +%FT%TZ)] $d: renewed" >>"$LOG"
else
rc=$?
echo "[$(date -u +%FT%TZ)] $d: acme-client exit=$rc (likely no renewal needed; harmless if &gt;30d to expiry)" >>"$LOG"
# Don't fail the script for "no renewal needed"
fi
done
# Reload relayd if any cert files changed in last 5 minutes
if find /etc/ssl -name '*.fullchain.pem' -mmin -5 | grep -q .; then
rcctl reload relayd
echo "[$(date -u +%FT%TZ)] relayd reloaded for new certs" >>"$LOG"
fi
echo "[$(date -u +%FT%TZ)] cert-renew-check done" >>"$LOG"
exit $RC

69
scripts/install.sh Executable file
View file

@ -0,0 +1,69 @@
#!/bin/sh
# install.sh — runs on the edge box (called by provision.sh)
#
# Copies repo's etc/* into /etc/, validates configs, restarts services.
# Idempotent: re-running just refreshes configs.
set -eu
cd "$(dirname "$0")/.."
REPO_DIR="$(pwd)"
echo "[install] running from $REPO_DIR"
# === Backup existing configs once ===
BACKUP_DIR=/var/backups/production-openbsd-pre-install
if [ ! -d "$BACKUP_DIR" ]; then
echo "[install] backing up current configs to $BACKUP_DIR"
mkdir -p "$BACKUP_DIR"
for f in /etc/pf.conf /etc/relayd.conf /etc/acme-client.conf /var/unbound/etc/unbound.conf /etc/ssh/sshd_config /etc/doas.conf; do
[ -f "$f" ] && cp "$f" "$BACKUP_DIR/" || true
done
fi
# === Copy configs ===
echo "[install] installing configs"
install -m 600 "$REPO_DIR/etc/pf.conf" /etc/pf.conf
install -m 600 "$REPO_DIR/etc/relayd.conf" /etc/relayd.conf
install -m 600 "$REPO_DIR/etc/acme-client.conf" /etc/acme-client.conf
install -m 644 "$REPO_DIR/etc/unbound.conf" /var/unbound/etc/unbound.conf
install -m 600 "$REPO_DIR/etc/sshd_config" /etc/ssh/sshd_config
install -m 600 "$REPO_DIR/etc/doas.conf" /etc/doas.conf
# WG: only copy if /etc/hostname.wg0 doesn't already exist
if [ ! -f /etc/hostname.wg0 ]; then
echo "[install] WARN: /etc/hostname.wg0 not present"
echo "[install] Copy etc/hostname.wg0.example, generate keys, paste nullstone pubkey"
echo "[install] See its header for steps. Skipping wg0 enable for now."
fi
# === Validate configs ===
echo "[install] validating configs"
pfctl -nf /etc/pf.conf
relayd -nf /etc/relayd.conf
unbound-checkconf /var/unbound/etc/unbound.conf
# === Enable services ===
echo "[install] enabling services"
rcctl enable pf relayd acme-client unbound sshd
[ -f /etc/hostname.wg0 ] && rcctl set wg flags || true
# === Reload services ===
echo "[install] reloading services"
rcctl reload pf || pfctl -f /etc/pf.conf
rcctl restart relayd
rcctl restart unbound
rcctl restart sshd
# === Cron for acme-client renewal (weekly) ===
CRON_FILE=/var/cron/tabs/root
if ! grep -q '/usr/local/sbin/cert-renew-check.sh' "$CRON_FILE" 2>/dev/null; then
echo "[install] adding weekly cert-renew cron"
install -m 755 "$REPO_DIR/scripts/cert-renew-check.sh" /usr/local/sbin/
( crontab -l 2>/dev/null; echo '15 3 * * 0 /usr/local/sbin/cert-renew-check.sh' ) | crontab -
fi
echo "[install] DONE"
echo "[install] verify:"
echo " pfctl -sr | head"
echo " rcctl ls on"
echo " acme-client -v s8n.ru # request first cert (DNS-01 manual for now)"

40
scripts/provision.sh Executable file
View file

@ -0,0 +1,40 @@
#!/bin/sh
# provision.sh — one-shot provision: clone repo onto edge box, run install.sh
#
# Usage (run from onyx):
# ./scripts/provision.sh user@<edge-ip-or-hostname>
#
# What it does:
# 1. SSH into edge box, install required pkgs (acme-client + wireguard-tools)
# 2. git clone this repo to /tmp/production-openbsd
# 3. Run /tmp/production-openbsd/scripts/install.sh on the edge box
# which copies /etc/* + enables services + reloads
#
# Expected pre-state:
# - OpenBSD 7.6+ installed on edge box
# - User 'user' exists with sudo/doas access
# - Your SSH pubkey already in user@edge:.ssh/authorized_keys
# - WG keys generated separately (see etc/hostname.wg0.example header)
set -eu
TARGET="${1:-}"
[ -n "$TARGET" ] || { echo "Usage: $0 user@<edge-ip>" >&2; exit 1; }
REPO_URL="ssh://git@192.168.0.100:222/s8n/production-openbsd.git"
REMOTE_PATH="/tmp/production-openbsd"
echo "[*] Provisioning $TARGET ..."
ssh "$TARGET" -- "/bin/sh -se" <<EOF
set -eu
echo "[remote] installing prerequisites"
doas pkg_add -I acme-client wireguard-tools git rsync || true
[ -d $REMOTE_PATH ] && rm -rf $REMOTE_PATH
git clone $REPO_URL $REMOTE_PATH
cd $REMOTE_PATH
doas /bin/sh scripts/install.sh
echo "[remote] provision complete"
EOF
echo "[*] Done. Verify: ssh $TARGET 'doas pfctl -sr | head'"

19
scripts/read-logs.sh Executable file
View file

@ -0,0 +1,19 @@
#!/bin/sh
# read-logs.sh — pull /var/log/* from edge box for offline diagnostics
# Run from onyx.
#
# Usage: ./scripts/read-logs.sh user@<edge-ip>
set -eu
TARGET="${1:-}"
[ -n "$TARGET" ] || { echo "Usage: $0 user@<edge-ip>" >&2; exit 1; }
OUT="$(pwd)/out/edge-logs-$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$OUT"
scp -r "$TARGET:/var/log/{pflog,messages,authlog,daemon,relayd.log,acme-client.log,cert-renew.log}" "$OUT/" 2>/dev/null || true
ssh "$TARGET" 'doas pfctl -sr; doas pfctl -ss' > "$OUT/pf-state.txt" 2>&1 || true
ssh "$TARGET" 'doas rcctl ls on' > "$OUT/services.txt" 2>&1 || true
echo "[OK] $OUT"
ls -la "$OUT"