Files
nix-config/packages/system/nebula-sign-cert/default.nix
2026-03-25 16:42:34 -05:00

187 lines
7.3 KiB
Nix

{
writeShellApplication,
nebula,
sops,
coreutils,
jq,
...
}:
writeShellApplication {
name = "nebula-sign-cert";
runtimeInputs = [
nebula
sops
coreutils
jq
];
text = ''
# ---------------------------------------------------------------------------
# nebula-sign-cert
#
# Signs a new Nebula host certificate using the CA stored in a SOPS secrets
# file and writes the resulting cert+key back into a (possibly different)
# SOPS secrets file.
#
# The CA is read from:
# <ca-sops-file> at YAML path <ca-prefix>/ca-cert and <ca-prefix>/ca-key
#
# The new cert+key are written to:
# <host-sops-file> at YAML paths
# <host-prefix>/<host-secret-name>-cert
# <host-prefix>/<host-secret-name>-key
#
# Usage:
# nebula-sign-cert \
# --name <node-name> # e.g. "nas" used in the cert
# --ip <overlay-ip/mask> # e.g. "10.1.1.2/24"
# --ca-file <path/to/ca-secrets.yaml>
# --ca-prefix <sops-key-prefix> # e.g. "pi5/nebula"
# --host-file <path/to/host-secrets.yaml>
# --host-prefix <sops-key-prefix> # e.g. "jallen-nas/nebula"
# --host-secret-name <name> # e.g. "nas" (cert stored as nas-cert/nas-key)
# [--groups <group1,group2>] # optional Nebula groups
# [--duration <duration>] # e.g. "8760h0m0s" (1 year), default: CA lifetime
#
# All temp files are written to a private tmpdir and shredded on exit.
# ---------------------------------------------------------------------------
set -euo pipefail
# argument parsing
NAME=""
IP=""
CA_FILE=""
CA_PREFIX=""
HOST_FILE=""
HOST_PREFIX=""
HOST_SECRET_NAME=""
NEBULA_GROUPS=""
DURATION=""
usage() {
echo "Usage: nebula-sign-cert \\"
echo " --name <node-name> \\"
echo " --ip <overlay-ip/mask> \\"
echo " --ca-file <path/to/ca-sops-file.yaml> \\"
echo " --ca-prefix <sops-key-prefix> (e.g. pi5/nebula) \\"
echo " --host-file <path/to/host-sops-file.yaml> \\"
echo " --host-prefix <sops-key-prefix> (e.g. jallen-nas/nebula) \\"
echo " --host-secret-name <name> (e.g. nas) \\"
echo " [--groups <group1,group2>] \\"
echo " [--duration <8760h0m0s>]"
exit 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--name) NAME="$2"; shift 2 ;;
--ip) IP="$2"; shift 2 ;;
--ca-file) CA_FILE="$2"; shift 2 ;;
--ca-prefix) CA_PREFIX="$2"; shift 2 ;;
--host-file) HOST_FILE="$2"; shift 2 ;;
--host-prefix) HOST_PREFIX="$2"; shift 2 ;;
--host-secret-name) HOST_SECRET_NAME="$2"; shift 2 ;;
--groups) NEBULA_GROUPS="$2"; shift 2 ;;
--duration) DURATION="$2"; shift 2 ;;
-h|--help) usage ;;
*) echo "Unknown argument: $1"; usage ;;
esac
done
# validate required args
missing=()
[[ -z "$NAME" ]] && missing+=(--name)
[[ -z "$IP" ]] && missing+=(--ip)
[[ -z "$CA_FILE" ]] && missing+=(--ca-file)
[[ -z "$CA_PREFIX" ]] && missing+=(--ca-prefix)
[[ -z "$HOST_FILE" ]] && missing+=(--host-file)
[[ -z "$HOST_PREFIX" ]] && missing+=(--host-prefix)
[[ -z "$HOST_SECRET_NAME" ]] && missing+=(--host-secret-name)
if [[ ''${#missing[@]} -gt 0 ]]; then
echo "error: missing required arguments: ''${missing[*]}"
usage
fi
[[ -f "$CA_FILE" ]] || { echo "error: CA secrets file not found: $CA_FILE"; exit 1; }
[[ -f "$HOST_FILE" ]] || { echo "error: host secrets file not found: $HOST_FILE"; exit 1; }
# Convert "a/b/c" prefix sops extract path ["a"]["b"]["c"]
prefix_to_sops_path() {
local prefix="$1"
local IFS='/'
local result=""
for segment in $prefix; do
result+="[\"$segment\"]"
done
echo "$result"
}
CA_SOPS_PATH=$(prefix_to_sops_path "$CA_PREFIX")
HOST_SOPS_PATH=$(prefix_to_sops_path "$HOST_PREFIX")
# setup temp directory (cleaned up on exit)
TMPDIR=$(mktemp -d)
cleanup() {
# Shred all temp files before removing the directory
find "$TMPDIR" -type f -exec shred -u {} \;
rm -rf "$TMPDIR"
}
trap cleanup EXIT
CA_CRT="$TMPDIR/ca.crt"
CA_KEY="$TMPDIR/ca.key"
HOST_CRT="$TMPDIR/host.crt"
HOST_KEY="$TMPDIR/host.key"
# extract CA cert and key from SOPS
echo "» Extracting CA from $CA_FILE ($CA_PREFIX)..."
sops decrypt --extract "''${CA_SOPS_PATH}[\"ca-cert\"]" "$CA_FILE" > "$CA_CRT"
sops decrypt --extract "''${CA_SOPS_PATH}[\"ca-key\"]" "$CA_FILE" > "$CA_KEY"
# build nebula-cert sign args
SIGN_ARGS=(
sign
-name "$NAME"
-ip "$IP"
-ca-crt "$CA_CRT"
-ca-key "$CA_KEY"
-out-crt "$HOST_CRT"
-out-key "$HOST_KEY"
)
[[ -n "$NEBULA_GROUPS" ]] && SIGN_ARGS+=(-groups "$NEBULA_GROUPS")
[[ -n "$DURATION" ]] && SIGN_ARGS+=(-duration "$DURATION")
# sign the certificate
echo "» Signing certificate for $NAME ($IP)..."
nebula-cert "''${SIGN_ARGS[@]}"
echo "» Certificate details:"
nebula-cert print -path "$HOST_CRT"
# write cert and key back into the host SOPS file
# sops set requires a JSON-encoded string value; use jq -Rs . to encode the
# file contents and pipe via --value-stdin to avoid leaking secrets in ps.
echo "» Writing ''${HOST_SECRET_NAME}-cert into $HOST_FILE ($HOST_PREFIX)..."
jq -Rs . "$HOST_CRT" | sops set --value-stdin \
"$HOST_FILE" \
"''${HOST_SOPS_PATH}[\"''${HOST_SECRET_NAME}-cert\"]"
echo "» Writing ''${HOST_SECRET_NAME}-key into $HOST_FILE ($HOST_PREFIX)..."
jq -Rs . "$HOST_KEY" | sops set --value-stdin \
"$HOST_FILE" \
"''${HOST_SOPS_PATH}[\"''${HOST_SECRET_NAME}-key\"]"
echo ""
echo " Done. Certificate for '$NAME' written to $HOST_FILE"
echo " Rebuild the host to apply: sudo nixos-rebuild switch --flake .#<hostname>"
'';
meta = {
description = "Sign a Nebula host certificate using a CA stored in SOPS";
mainProgram = "nebula-sign-cert";
};
}