From be77f1eb2f23eaa7fa301cae53a0ecaacbf23edd Mon Sep 17 00:00:00 2001 From: obsidian-ai Date: Fri, 8 May 2026 14:10:29 +0100 Subject: [PATCH] feat: production-openbsd v0.1 scaffold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .gitignore | 6 ++ README.md | 88 ++++++++++++++++++++++++++++ docs/setup-checklist.md | 111 ++++++++++++++++++++++++++++++++++++ etc/acme-client.conf | 38 ++++++++++++ etc/doas.conf | 10 ++++ etc/hostname.wg0.example | 22 +++++++ etc/pf.conf | 68 ++++++++++++++++++++++ etc/relayd.conf | 63 ++++++++++++++++++++ etc/sshd_config | 30 ++++++++++ etc/unbound.conf | 67 ++++++++++++++++++++++ flash.sh | 16 ++++++ scripts/cert-renew-check.sh | 28 +++++++++ scripts/install.sh | 69 ++++++++++++++++++++++ scripts/provision.sh | 40 +++++++++++++ scripts/read-logs.sh | 19 ++++++ 15 files changed, 675 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 docs/setup-checklist.md create mode 100644 etc/acme-client.conf create mode 100644 etc/doas.conf create mode 100644 etc/hostname.wg0.example create mode 100644 etc/pf.conf create mode 100644 etc/relayd.conf create mode 100644 etc/sshd_config create mode 100644 etc/unbound.conf create mode 100755 flash.sh create mode 100755 scripts/cert-renew-check.sh create mode 100755 scripts/install.sh create mode 100755 scripts/provision.sh create mode 100755 scripts/read-logs.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ffa53e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.swp +.DS_Store +.secrets/ +secrets/ +*.key +*.pem diff --git a/README.md b/README.md new file mode 100644 index 0000000..457beb1 --- /dev/null +++ b/README.md @@ -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 + ) 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@` — 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` diff --git a/docs/setup-checklist.md b/docs/setup-checklist.md new file mode 100644 index 0000000..7790643 --- /dev/null +++ b/docs/setup-checklist.md @@ -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@` to push your pubkey +4. From onyx: `ssh user@` to confirm key auth works + +## Phase 3 — provision (from onyx) + +```bash +cd ~/projects/production-openbsd +./scripts/provision.sh user@ +``` + +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@ '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@ 'doas acme-client -v s8n.ru' +ssh user@ 'doas acme-client -v veilor.uk' +ssh user@ '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. diff --git a/etc/acme-client.conf b/etc/acme-client.conf new file mode 100644 index 0000000..affccac --- /dev/null +++ b/etc/acme-client.conf @@ -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 +# 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" +} diff --git a/etc/doas.conf b/etc/doas.conf new file mode 100644 index 0000000..913117e --- /dev/null +++ b/etc/doas.conf @@ -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 diff --git a/etc/hostname.wg0.example b/etc/hostname.wg0.example new file mode 100644 index 0000000..2a60fbb --- /dev/null +++ b/etc/hostname.wg0.example @@ -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 diff --git a/etc/pf.conf b/etc/pf.conf new file mode 100644 index 0000000..98a0287 --- /dev/null +++ b/etc/pf.conf @@ -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 persist file "/etc/pf-bogons" +table 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 to any +block in quick on $ext_if from any to + +# === 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 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 flush global) +block in quick from + +# === 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. diff --git a/etc/relayd.conf b/etc/relayd.conf new file mode 100644 index 0000000..d3be43b --- /dev/null +++ b/etc/relayd.conf @@ -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 } + +# === 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 +} + +# === 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 { 127.0.0.1 } + +# === Relay: HTTP redirect on 80 === +relay www_http { + listen on $ext_addr port 80 + protocol "http_redirect" + forward to 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 port $backend_port check http "/" code 200 +} diff --git a/etc/sshd_config b/etc/sshd_config new file mode 100644 index 0000000..6d6c10e --- /dev/null +++ b/etc/sshd_config @@ -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 diff --git a/etc/unbound.conf b/etc/unbound.conf new file mode 100644 index 0000000..4b16384 --- /dev/null +++ b/etc/unbound.conf @@ -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 diff --git a/flash.sh b/flash.sh new file mode 100755 index 0000000..49a4b3a --- /dev/null +++ b/flash.sh @@ -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" diff --git a/scripts/cert-renew-check.sh b/scripts/cert-renew-check.sh new file mode 100755 index 0000000..07f25e2 --- /dev/null +++ b/scripts/cert-renew-check.sh @@ -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 >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 diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..c990137 --- /dev/null +++ b/scripts/install.sh @@ -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)" diff --git a/scripts/provision.sh b/scripts/provision.sh new file mode 100755 index 0000000..7154ac4 --- /dev/null +++ b/scripts/provision.sh @@ -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@ +# +# 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@" >&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" < + +set -eu +TARGET="${1:-}" +[ -n "$TARGET" ] || { echo "Usage: $0 user@" >&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"