207 lines
6.5 KiB
Nix
207 lines
6.5 KiB
Nix
{ pkgs, ... }:
|
|
let
|
|
unsealScript = pkgs.writeShellApplication {
|
|
name = "openbao-unseal";
|
|
runtimeInputs = with pkgs; [
|
|
openbao
|
|
coreutils
|
|
gnugrep
|
|
getent
|
|
];
|
|
text = ''
|
|
# Set environment to use Unix socket
|
|
export BAO_ADDR='unix:///run/openbao/openbao.sock'
|
|
SOCKET_PATH="/run/openbao/openbao.sock"
|
|
CREDS_DIR="''${CREDENTIALS_DIRECTORY:-}"
|
|
|
|
# Wait for socket to exist
|
|
echo "Waiting for OpenBao socket..."
|
|
for _ in {1..30}; do
|
|
if [ -S "$SOCKET_PATH" ]; then
|
|
echo "Socket exists"
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
# Wait for OpenBao to accept connections
|
|
echo "Waiting for OpenBao to be ready..."
|
|
for _ in {1..30}; do
|
|
output=$(timeout 2 bao status 2>&1 || true)
|
|
|
|
if echo "$output" | grep -q "Sealed.*false"; then
|
|
# Already unsealed
|
|
echo "OpenBao is already unsealed"
|
|
exit 0
|
|
elif echo "$output" | grep -qE "(Sealed|Initialized)"; then
|
|
# Got a valid response, OpenBao is ready (sealed)
|
|
echo "OpenBao is ready"
|
|
break
|
|
fi
|
|
|
|
sleep 1
|
|
done
|
|
|
|
# Check if already unsealed
|
|
if output=$(timeout 2 bao status 2>&1 || true); then
|
|
if echo "$output" | grep -q "Sealed.*false"; then
|
|
echo "OpenBao is already unsealed"
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
# Unseal using the TPM-decrypted keys (one per line)
|
|
if [ -n "$CREDS_DIR" ] && [ -f "$CREDS_DIR/unseal-key" ]; then
|
|
echo "Unsealing OpenBao..."
|
|
while IFS= read -r key; do
|
|
# Skip empty lines
|
|
[ -z "$key" ] && continue
|
|
|
|
echo "Applying unseal key..."
|
|
bao operator unseal "$key"
|
|
|
|
# Check if unsealed after each key
|
|
if output=$(timeout 2 bao status 2>&1 || true); then
|
|
if echo "$output" | grep -q "Sealed.*false"; then
|
|
echo "OpenBao unsealed successfully"
|
|
exit 0
|
|
fi
|
|
fi
|
|
done < "$CREDS_DIR/unseal-key"
|
|
|
|
echo "WARNING: Applied all keys but OpenBao is still sealed"
|
|
exit 0
|
|
else
|
|
echo "WARNING: Unseal key credential not found, OpenBao remains sealed"
|
|
exit 0
|
|
fi
|
|
'';
|
|
};
|
|
|
|
bootstrapCertScript = pkgs.writeShellApplication {
|
|
name = "bootstrap-vault-cert";
|
|
runtimeInputs = with pkgs; [
|
|
openbao
|
|
jq
|
|
openssl
|
|
coreutils
|
|
];
|
|
text = ''
|
|
# Bootstrap vault01 with a proper certificate from its own PKI
|
|
# This solves the chicken-and-egg problem where ACME clients can't trust
|
|
# vault01's self-signed certificate.
|
|
|
|
echo "=== Bootstrapping vault01 certificate ==="
|
|
|
|
# Use Unix socket to avoid TLS issues
|
|
export BAO_ADDR='unix:///run/openbao/openbao.sock'
|
|
|
|
# ACME certificate directory
|
|
CERT_DIR="/var/lib/acme/vault01.home.2rjus.net"
|
|
|
|
# Issue certificate for vault01 with vault as SAN
|
|
echo "Issuing certificate for vault01.home.2rjus.net (with SAN: vault.home.2rjus.net)..."
|
|
OUTPUT=$(bao write -format=json pki_int/issue/homelab \
|
|
common_name="vault01.home.2rjus.net" \
|
|
alt_names="vault.home.2rjus.net" \
|
|
ttl="720h")
|
|
|
|
# Create ACME directory structure
|
|
echo "Creating ACME certificate directory..."
|
|
mkdir -p "$CERT_DIR"
|
|
|
|
# Extract certificate components to temp files
|
|
echo "$OUTPUT" | jq -r '.data.certificate' > /tmp/vault01-cert.pem
|
|
echo "$OUTPUT" | jq -r '.data.private_key' > /tmp/vault01-key.pem
|
|
echo "$OUTPUT" | jq -r '.data.issuing_ca' > /tmp/vault01-ca.pem
|
|
|
|
# Create fullchain (cert + CA)
|
|
cat /tmp/vault01-cert.pem /tmp/vault01-ca.pem > /tmp/vault01-fullchain.pem
|
|
|
|
# Backup old certificates if they exist
|
|
if [ -f "$CERT_DIR/fullchain.pem" ]; then
|
|
echo "Backing up old certificate..."
|
|
cp "$CERT_DIR/fullchain.pem" "$CERT_DIR/fullchain.pem.backup"
|
|
cp "$CERT_DIR/key.pem" "$CERT_DIR/key.pem.backup"
|
|
fi
|
|
|
|
# Install new certificates
|
|
echo "Installing new certificate..."
|
|
mv /tmp/vault01-fullchain.pem "$CERT_DIR/fullchain.pem"
|
|
mv /tmp/vault01-cert.pem "$CERT_DIR/cert.pem"
|
|
mv /tmp/vault01-ca.pem "$CERT_DIR/chain.pem"
|
|
mv /tmp/vault01-key.pem "$CERT_DIR/key.pem"
|
|
|
|
# Set proper ownership and permissions (ACME-style)
|
|
chown -R acme:acme "$CERT_DIR"
|
|
chmod 750 "$CERT_DIR"
|
|
chmod 640 "$CERT_DIR"/*.pem
|
|
|
|
echo "Certificate installed successfully!"
|
|
echo ""
|
|
echo "Certificate details:"
|
|
openssl x509 -in "$CERT_DIR/cert.pem" -noout -subject -issuer -dates
|
|
echo ""
|
|
echo "Subject Alternative Names:"
|
|
openssl x509 -in "$CERT_DIR/cert.pem" -noout -ext subjectAltName
|
|
|
|
echo ""
|
|
echo "Now restart openbao service:"
|
|
echo " systemctl restart openbao"
|
|
echo ""
|
|
echo "After restart, verify ACME endpoint is accessible:"
|
|
echo " curl https://vault01.home.2rjus.net:8200/v1/pki_int/acme/directory"
|
|
echo ""
|
|
echo "Once working, ACME will automatically manage certificate renewals."
|
|
'';
|
|
};
|
|
in
|
|
{
|
|
# Make bootstrap script available as a command
|
|
environment.systemPackages = [ bootstrapCertScript ];
|
|
|
|
services.openbao = {
|
|
enable = true;
|
|
|
|
settings = {
|
|
ui = true;
|
|
|
|
storage.file.path = "/var/lib/openbao";
|
|
listener.default = {
|
|
type = "tcp";
|
|
address = "0.0.0.0:8200";
|
|
tls_cert_file = "/run/credentials/openbao.service/cert.pem";
|
|
tls_key_file = "/run/credentials/openbao.service/key.pem";
|
|
};
|
|
listener.socket = {
|
|
type = "unix";
|
|
address = "/run/openbao/openbao.sock";
|
|
};
|
|
};
|
|
};
|
|
|
|
systemd.services.openbao.serviceConfig = {
|
|
LoadCredential = [
|
|
"key.pem:/var/lib/acme/vault01.home.2rjus.net/key.pem"
|
|
"cert.pem:/var/lib/acme/vault01.home.2rjus.net/fullchain.pem"
|
|
];
|
|
# TPM2-encrypted unseal key (created manually, see setup instructions)
|
|
LoadCredentialEncrypted = [
|
|
"unseal-key:/var/lib/openbao/unseal-key.cred"
|
|
];
|
|
# Auto-unseal on service start
|
|
ExecStartPost = "${unsealScript}/bin/openbao-unseal";
|
|
# Add openbao user to acme group to read certificates
|
|
SupplementaryGroups = [ "acme" ];
|
|
};
|
|
|
|
# ACME certificate management
|
|
# Bootstrapped with bootstrap-vault-cert, now managed by ACME
|
|
security.acme.certs."vault01.home.2rjus.net" = {
|
|
server = "https://vault01.home.2rjus.net:8200/v1/pki_int/acme/directory";
|
|
listenHTTP = ":80";
|
|
reloadServices = [ "openbao" ];
|
|
extraDomainNames = [ "vault.home.2rjus.net" ];
|
|
};
|
|
}
|