deploy: harden qube fips ingress + diagnostics + persistence

This commit is contained in:
Laan Tungir
2026-05-03 12:35:37 -04:00
parent 436216e706
commit 7c14cb4e80
2 changed files with 245 additions and 1 deletions
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env bash
set -euo pipefail
# Simple inter-qube FIPS connectivity test + optional direct connect bootstrap.
# Usage:
# ./test_qube_pair.sh <remote_npub> [remote_underlay_addr:port]
# Example:
# ./test_qube_pair.sh npub1... 10.137.0.22:2121
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <remote_npub> [remote_underlay_addr:port]" >&2
exit 1
fi
REMOTE_NPUB="$1"
REMOTE_ADDR="${2:-}"
REMOTE_FQDN="${REMOTE_NPUB}.fips"
CURL_TIMEOUT="${CURL_TIMEOUT:-10}"
MIN_BYTES="${MIN_BYTES:-200}"
need_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "Error: required command not found: $1" >&2
exit 1
fi
}
need_cmd curl
need_cmd python3
need_cmd getent
need_cmd fipsctl
echo "==> Local node"
STATUS_JSON="$(sudo fipsctl show status)"
python3 - "${STATUS_JSON}" <<'PY'
import json
import sys
s = json.loads(sys.argv[1])
print(f"npub={s.get('npub')}")
print(f"ipv6={s.get('ipv6_addr')}")
print(f"state={s.get('state')}")
print(f"peer_count={s.get('peer_count')}")
print(f"session_count={s.get('session_count')}")
PY
echo ""
echo "==> Attempting to resolve ${REMOTE_FQDN}"
REMOTE_IPV6=""
if RESOLVED="$(getent ahosts "${REMOTE_FQDN}" | awk '{print $1}' | head -n1)" && [[ -n "${RESOLVED}" ]]; then
REMOTE_IPV6="${RESOLVED}"
echo "resolved_ipv6=${REMOTE_IPV6}"
else
echo "not_resolved"
fi
if [[ -n "${REMOTE_ADDR}" ]]; then
echo ""
echo "==> Bootstrapping direct connect to ${REMOTE_NPUB} at ${REMOTE_ADDR}"
sudo fipsctl connect "${REMOTE_NPUB}" "${REMOTE_ADDR}" udp || true
sleep 2
fi
echo ""
echo "==> Peer/session check (looking for remote npub)"
PEERS_JSON="$(sudo fipsctl show peers)"
python3 - "${REMOTE_NPUB}" "${PEERS_JSON}" <<'PY'
import json
import sys
needle = sys.argv[1]
obj = json.loads(sys.argv[2])
for p in obj.get("peers", []):
if p.get("npub") == needle:
print("peer_present=yes")
print(f"connectivity={p.get('connectivity')}")
print(f"transport_addr={p.get('transport_addr')}")
print(f"ipv6_addr={p.get('ipv6_addr')}")
break
else:
print("peer_present=no")
PY
echo ""
echo "==> HTTP check over .fips"
TMP_BODY="$(mktemp)"
trap 'rm -f "${TMP_BODY}"' EXIT
HTTP_CODE="$(curl -sS --max-time "${CURL_TIMEOUT}" -o "${TMP_BODY}" -w '%{http_code}' "http://${REMOTE_FQDN}/" || true)"
BYTES="$(wc -c < "${TMP_BODY}" | tr -d '[:space:]')"
echo "http_code=${HTTP_CODE:-000} bytes=${BYTES}"
if [[ "${HTTP_CODE:-000}" == "200" ]] && (( BYTES >= MIN_BYTES )); then
echo ""
echo "RESULT=PASS (inter-qube communication confirmed via .fips HTTP)"
exit 0
fi
echo ""
echo "RESULT=FAIL (communication not yet proven)"
echo ""
echo "Next actions:"
echo "1) In EACH qube, get underlay IP: ip -4 -o addr show dev eth0"
echo "2) From this qube, connect to remote: sudo fipsctl connect ${REMOTE_NPUB} <REMOTE_ETH0_IP>:2121 udp"
echo "3) From remote qube, connect back to this qube: sudo fipsctl connect <THIS_QUBE_NPUB> <THIS_QUBE_ETH0_IP>:2121 udp"
echo "4) Re-run this script in both qubes."
exit 1
+139 -1
View File
@@ -192,6 +192,23 @@ ask_yes_no() {
done
}
print_connectivity_diagnostics() {
echo "--- ip -6 route ---" >&2
ip -6 route show >&2 || true
if command -v nft >/dev/null 2>&1; then
echo "--- nft list chain ip6 qubes custom-input ---" >&2
sudo nft list chain ip6 qubes custom-input >&2 || true
echo "--- nft list chain ip qubes custom-input ---" >&2
sudo nft list chain ip qubes custom-input >&2 || true
fi
if command -v ip6tables >/dev/null 2>&1; then
echo "--- ip6tables -S ---" >&2
sudo ip6tables -S >&2 || true
fi
}
restart_unit_with_diagnostics() {
local unit="$1"
local required="${2:-yes}"
@@ -211,6 +228,7 @@ restart_unit_with_diagnostics() {
sudo systemctl status "${unit}" --no-pager >&2 || true
echo "--- journalctl -u ${unit} (last ${journal_lines} lines) ---" >&2
sudo journalctl -u "${unit}" -n "${journal_lines}" --no-pager >&2 || true
print_connectivity_diagnostics
[[ "${required}" == "yes" ]] && return 1
return 0
}
@@ -223,6 +241,7 @@ restart_unit_with_diagnostics() {
sudo systemctl status "${unit}" --no-pager >&2 || true
echo "--- journalctl -u ${unit} (last ${journal_lines} lines) ---" >&2
sudo journalctl -u "${unit}" -n "${journal_lines}" --no-pager >&2 || true
print_connectivity_diagnostics
[[ "${required}" == "yes" ]] && return 1
return 0
}
@@ -234,6 +253,7 @@ restart_unit_with_diagnostics() {
sudo systemctl status "${unit}" --no-pager >&2 || true
echo "--- journalctl -u ${unit} (last ${journal_lines} lines) ---" >&2
sudo journalctl -u "${unit}" -n "${journal_lines}" --no-pager >&2 || true
print_connectivity_diagnostics
[[ "${required}" == "yes" ]] && return 1
return 0
fi
@@ -345,6 +365,95 @@ EOF
fi
}
persist_qubes_rc_local_network_overrides() {
local persist_dns="${1:-no}"
local rc_file="/rw/config/rc.local"
local tmp_file
[[ -d /rw/config ]] || return 0
require_sudo
sudo mkdir -p /rw/config
tmp_file="$(mktemp)"
if [[ -f "${rc_file}" ]]; then
sudo cat "${rc_file}" > "${tmp_file}"
else
printf '%s\n' '#!/bin/sh' > "${tmp_file}"
fi
awk '
BEGIN { skip = 0 }
/^# BEGIN FIPS_DEPLOY_NETWORK_OVERRIDES$/ { skip = 1; next }
/^# END FIPS_DEPLOY_NETWORK_OVERRIDES$/ { skip = 0; next }
skip == 0 { print }
' "${tmp_file}" > "${tmp_file}.clean"
mv "${tmp_file}.clean" "${tmp_file}"
{
echo ""
echo "# BEGIN FIPS_DEPLOY_NETWORK_OVERRIDES"
echo "if command -v nft >/dev/null 2>&1; then"
echo " nft add rule ip6 qubes custom-input iifname \"fips0\" accept 2>/dev/null || true"
echo " nft add rule ip qubes custom-input iifname \"fips0\" accept 2>/dev/null || true"
echo "fi"
if [[ "${persist_dns}" == "yes" ]]; then
cat <<'SH'
if ! grep -Eq '^[[:space:]]*nameserver[[:space:]]+127\.0\.0\.1([[:space:]]|$)' /etc/resolv.conf 2>/dev/null; then
tmp_resolv="$(mktemp)"
{
echo "nameserver 127.0.0.1"
cat /etc/resolv.conf 2>/dev/null || true
} > "${tmp_resolv}"
install -m 0644 "${tmp_resolv}" /etc/resolv.conf || true
rm -f "${tmp_resolv}"
fi
SH
fi
echo "# END FIPS_DEPLOY_NETWORK_OVERRIDES"
} >> "${tmp_file}"
sudo install -m 0755 "${tmp_file}" "${rc_file}"
rm -f "${tmp_file}"
echo "==> Persisted FIPS network overrides in ${rc_file}"
}
ensure_fips_ingress_allowed() {
local persist_dns="${1:-no}"
require_sudo
echo "==> Ensuring inbound traffic on fips0 is allowed"
if command -v nft >/dev/null 2>&1; then
if sudo nft list chain ip6 qubes custom-input >/dev/null 2>&1; then
if ! sudo nft list chain ip6 qubes custom-input 2>/dev/null | grep -q 'iifname "fips0" accept'; then
sudo nft add rule ip6 qubes custom-input iifname "fips0" accept
fi
if sudo nft list chain ip qubes custom-input >/dev/null 2>&1; then
if ! sudo nft list chain ip qubes custom-input 2>/dev/null | grep -q 'iifname "fips0" accept'; then
sudo nft add rule ip qubes custom-input iifname "fips0" accept
fi
fi
persist_qubes_rc_local_network_overrides "${persist_dns}"
return 0
fi
fi
if command -v ip6tables >/dev/null 2>&1; then
if ! sudo ip6tables -C INPUT -i fips0 -j ACCEPT >/dev/null 2>&1; then
sudo ip6tables -I INPUT 1 -i fips0 -j ACCEPT
fi
fi
if command -v iptables >/dev/null 2>&1; then
if ! sudo iptables -C INPUT -i fips0 -j ACCEPT >/dev/null 2>&1; then
sudo iptables -I INPUT 1 -i fips0 -j ACCEPT
fi
fi
}
ensure_build_toolchain
ensure_tun_ready
@@ -420,6 +529,7 @@ ADD_DEFAULT_PEERS="$(ask_yes_no "Add default static peers (laantungir/fips-test-
ADD_CURRENT_USER_TO_FIPS_GROUP="$(ask_yes_no "Add current user to fips group?" "yes")"
START_SERVICE_NOW="$(ask_yes_no "Start/restart fips service now?" "yes")"
CONFIGURE_DNSMASQ_FOR_FIPS="$(ask_yes_no "Configure dnsmasq for .fips DNS forwarding in this qube?" "yes")"
ALLOW_FIPS_INGRESS="$(ask_yes_no "Allow inbound traffic on fips0 (needed for inter-qube TCP over FIPS on Qubes)?" "yes")"
echo "==> Preparing ${INSTALL_MODE} payload"
@@ -561,6 +671,7 @@ echo "==> Cleaning potentially stale fd00::/8 route state"
require_sudo
sudo ip -6 route del fd00::/8 >/dev/null 2>&1 || true
DNS_FORWARDING_ACTIVE="no"
if [[ "${START_SERVICE_NOW}" == "yes" ]]; then
echo "==> Starting/restarting FIPS services"
sudo systemctl daemon-reload
@@ -568,7 +679,15 @@ if [[ "${START_SERVICE_NOW}" == "yes" ]]; then
restart_unit_with_diagnostics "fips-dns.service" "no"
if [[ "${CONFIGURE_DNSMASQ_FOR_FIPS}" == "yes" ]]; then
configure_dnsmasq_fips_dns || echo "Warning: dnsmasq configuration failed; continuing." >&2
if configure_dnsmasq_fips_dns; then
DNS_FORWARDING_ACTIVE="yes"
else
echo "Warning: dnsmasq configuration failed; continuing." >&2
fi
fi
if [[ "${ALLOW_FIPS_INGRESS}" == "yes" ]]; then
ensure_fips_ingress_allowed "${DNS_FORWARDING_ACTIVE}"
fi
echo "==> Running post-deploy health check"
@@ -582,6 +701,25 @@ else
fi
echo
if command -v fipsctl >/dev/null 2>&1; then
STATUS_JSON="$(sudo fipsctl show status 2>/dev/null || true)"
if [[ -n "${STATUS_JSON}" ]]; then
echo "==> Local node summary"
python3 - "${STATUS_JSON}" <<'PY'
import json
import sys
s = json.loads(sys.argv[1])
print(f"npub: {s.get('npub', 'n/a')}")
print(f"ipv6: {s.get('ipv6_addr', 'n/a')}")
PY
ETH0_IPV4="$(ip -4 -o addr show dev eth0 2>/dev/null | awk '{print $4}' | head -n1 | cut -d/ -f1)"
if [[ -n "${ETH0_IPV4}" ]]; then
echo "connect hint: sudo fipsctl connect <THIS_NODE_NPUB> ${ETH0_IPV4}:2121 udp"
fi
fi
fi
echo "Done. fips has been built and ${INSTALL_MODE} has completed."
echo "Check service status with: sudo systemctl status fips --no-pager"
echo "Follow logs with: sudo journalctl -u fips -f"