diff --git a/opentofu/templates/opnsense-config.xml.tmpl b/opentofu/templates/opnsense-config.xml.tmpl index e6e7f02..a21498c 100644 --- a/opentofu/templates/opnsense-config.xml.tmpl +++ b/opentofu/templates/opnsense-config.xml.tmpl @@ -23,6 +23,11 @@ admins {{ROOT_PASSWORD_HASH}} 0 + + {{ROOT_AUTHORIZED_KEYS_B64}} Etc/UTC {{NTP_UPSTREAM_SERVERS}} @@ -37,7 +42,16 @@ monthly 1 + + enabled + 1 admins diff --git a/scripts/opnsense-render-config.sh b/scripts/opnsense-render-config.sh index 18f2460..e62c6a2 100644 --- a/scripts/opnsense-render-config.sh +++ b/scripts/opnsense-render-config.sh @@ -28,7 +28,13 @@ OPNSENSE_HOSTNAME DOMAIN ROOT_PASSWORD_HASH WAN_IF WAN_IPADDR WAN_SUBNET_BITS WAN_GATEWAY LAN_IF LAN_IPADDR LAN_SUBNET_BITS + ROOT_AUTHORIZED_KEYS ) +# ROOT_AUTHORIZED_KEYS: the PUBLIC key text (e.g. the contents of an id_ed25519.pub). +# REQUIRED, no default -- D-112(c) makes SSH the management path for the edge, so an edge +# rendered without a key is a lockout waiting to happen. PUBLIC key material only; the +# private key is never read by this repo. +# # MIRROR_SYNC_PROTOCOL/MIRROR_UPSTREAM_NET/MIRROR_SYNC_PORT removed (DOCFIX-185): # the edge is a normal simulated-ISP router, not an egress-airgap firewall -- the # WAN mirror/NTP egress-control rules that used them are gone from the template. @@ -36,6 +42,11 @@ req "$v" done +# Base64 for , which OPNsense base64_decode()s into ~/.ssh/authorized_keys +# (verified upstream, src/etc/inc/auth.inc). Computed AFTER req() so a missing key fails with +# the friendly message rather than a `set -u` unbound-variable crash. +ROOT_AUTHORIZED_KEYS_B64="$(printf '%s\n' "$ROOT_AUTHORIZED_KEYS" | base64 -w0)" + [ -r "$TEMPLATE" ] || { echo "FAIL: template not found: $TEMPLATE"; exit 1; } route_block() { # route_block @@ -52,7 +63,7 @@ ROUTE2_BLOCK="$(route_block ROUTE2_NETWORK ROUTE2_GATEWAY ROUTE2_DESCR)" content="$(cat "$TEMPLATE")" -for v in "${REQUIRED_VARS[@]}" NTP_UPSTREAM_SERVERS NTP_PREFER_SERVER; do +for v in "${REQUIRED_VARS[@]}" NTP_UPSTREAM_SERVERS NTP_PREFER_SERVER ROOT_AUTHORIZED_KEYS_B64; do content="${content//"{{$v}}"/${!v}}" done content="${content//"{{ROUTE1_BLOCK}}"/$ROUTE1_BLOCK}" diff --git a/tests/opnsense-render-config/run-tests.sh b/tests/opnsense-render-config/run-tests.sh index ab045ab..c7e5b02 100644 --- a/tests/opnsense-render-config/run-tests.sh +++ b/tests/opnsense-render-config/run-tests.sh @@ -15,6 +15,8 @@ export ROOT_PASSWORD_HASH='$2y$10$fakehashfakehashfakehashfakehashfakehas' export WAN_IF="vtnet0" WAN_IPADDR="203.0.113.2" WAN_SUBNET_BITS="30" WAN_GATEWAY="203.0.113.1" export LAN_IF="vtnet1" LAN_IPADDR="10.12.8.1" LAN_SUBNET_BITS="22" + # PUBLIC key only (a throwaway fixture value; never a real key). + export ROOT_AUTHORIZED_KEYS="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKE harness@fixture" # MIRROR_* removed (DOCFIX-185): edge is a normal ISP router, no egress-airgap tokens. unset MIRROR_SYNC_PROTOCOL MIRROR_UPSTREAM_NET MIRROR_SYNC_PORT unset ROUTE1_NETWORK ROUTE1_GATEWAY ROUTE1_DESCR ROUTE2_NETWORK ROUTE2_GATEWAY ROUTE2_DESCR @@ -94,5 +96,35 @@ echo " FAIL T6 missing output arg (rc=$rc)"; FAIL=$((FAIL+1)) fi +# --- D-112(c): the edge's management path is SSH, so a rendered config MUST be reachable --- +# The pre-D-112 template had admins and NO key: applying it would +# have DISABLED sshd and installed no authorized key, locking management out to the console. +# These three cases make that unrepeatable. + +base_env +run_ok "T7 renders with an SSH key" "$TMP/t7.xml" + +if grep -q "enabled" "$TMP/t7.xml" && grep -q "" "$TMP/t7.xml"; then + echo " PASS T7b sshd is ENABLED + permitrootlogin set (not the lockout config)"; PASS=$((PASS+1)) +else + echo " FAIL T7b rendered config does not enable sshd -- edge would be unmanageable"; FAIL=$((FAIL+1)) +fi + +# The authorizedkeys value must be BASE64 of the public key -- OPNsense base64_decode()s it +# (upstream src/etc/inc/auth.inc). Assert the ROUND TRIP, not merely that a field is present: +# a wrongly-encoded key yields a well-formed config that silently grants no access. +akeys="$(sed -n 's:.*\(.*\).*:\1:p' "$TMP/t7.xml")" +if [ -n "$akeys" ] && printf '%s' "$akeys" | base64 -d 2>/dev/null | grep -qF "$ROOT_AUTHORIZED_KEYS"; then + echo " PASS T7c base64-DECODES back to the supplied public key"; PASS=$((PASS+1)) +else + echo " FAIL T7c missing or does not decode to the key"; FAIL=$((FAIL+1)) +fi + +# T8: no key supplied -> FAIL LOUD. An edge rendered without a key is a lockout waiting to +# happen, so this must not be silently allowed. +base_env +unset ROOT_AUTHORIZED_KEYS +run_fail 1 "T8 missing ROOT_AUTHORIZED_KEYS FAILS (rc 1) -- no silent lockout" "$TMP/t8.xml" + echo; echo "RESULT: PASS=$PASS FAIL=$FAIL" [[ "$FAIL" -eq 0 ]] && { echo "ALL PASS"; exit 0; } || exit 1