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:
commit
be77f1eb2f
15 changed files with 675 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
*.swp
|
||||
.DS_Store
|
||||
.secrets/
|
||||
secrets/
|
||||
*.key
|
||||
*.pem
|
||||
88
README.md
Normal file
88
README.md
Normal 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
111
docs/setup-checklist.md
Normal 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
38
etc/acme-client.conf
Normal 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
10
etc/doas.conf
Normal 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
22
etc/hostname.wg0.example
Normal 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
68
etc/pf.conf
Normal 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
63
etc/relayd.conf
Normal 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
30
etc/sshd_config
Normal 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
67
etc/unbound.conf
Normal 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
16
flash.sh
Executable 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
28
scripts/cert-renew-check.sh
Executable 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 >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
69
scripts/install.sh
Executable 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
40
scripts/provision.sh
Executable 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
19
scripts/read-logs.sh
Executable 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"
|
||||
Loading…
Reference in a new issue