certauth/setup-certauth.sh
Jarian Cottingham 4bd538b217 fix: YubiKey CA signing, remove inline API copies, env-driven config
Critical: build_leaf_cert self-signed leaves with the leaf key instead of
the YubiKey-held Intermediate CA key. Now extracts TBS, signs via
pkcs11-tool (ECDSA-SHA384), reassembles, and verifies against the
intermediate CA public key before returning.

- setup-certauth.sh: ~1100 lines of stale inline api/ copies replaced with
  copy-from-repo (single source of truth); writes private
  /etc/certauth/certauth.env (0600); DB init loads env, no more
  swallowed errors; systemd unit gets EnvironmentFile=
- config.py: YubiKey serials no longer hard-coded (env, fail closed);
  aarch64-only PKCS#11 path replaced with arch-neutral default; all
  paths env-overridable (CERTAUTH_*)
- main.py: removed dead fastapi.security.CSRFProtection import (crashed
  startup); module-relative static/templates dirs; created_by resolved
  from the authenticated user instead of hard-coded 1; unclosed file
  handles fixed; domain_id 0 stored as NULL (FK bug)
- models.py: certificates.domain_id FK pointed at users(id), now domains(id)
- login: CSRF token now actually sent and validated
- tests: 23 tests (auth, API flows, DER helpers, signing pipeline)
- README, LICENSE, requirements.txt, pyproject.toml
2026-08-20 22:03:01 +00:00

651 lines
22 KiB
Bash

#!/bin/bash
###############################################################################
# CertAuth Setup Script
# Provisions a fresh Ubuntu machine as a Certificate Authority
#
# Prerequisites:
# - Fresh Ubuntu 24.04+ Server (aarch64/x86_64)
# - Two YubiKey 5 Nano devices plugged in
# - Root or sudo access
# - Network access to Ubuntu repositories
#
# Usage:
# sudo bash setup-certauth.sh
#
# Configuration: Edit the variables below or pass as environment variables
###############################################################################
set -euo pipefail
# ===================== CONFIGURATION =====================
CA_ORG="${CA_ORG:-Home}"
CA_COUNTRY="${CA_COUNTRY:-US}"
ROOT_CA_CN="${ROOT_CA_CN:-certauth Root CA}"
INT_CA_CN="${INT_CA_CN:-certauth Intermediate CA}"
ROOT_VALID_DAYS="${ROOT_VALID_DAYS:-9125}" # 25 years
INT_VALID_DAYS="${INT_VALID_DAYS:-5475}" # 15 years
LEAF_VALID_DAYS="${LEAF_VALID_DAYS:-365}" # 1 year per cert
NETWORK_CIDR="${NETWORK_CIDR:-192.168.8.0/24}"
ADMIN_USER="${ADMIN_USER:-certauth}"
ADMIN_PASS="${ADMIN_PASS:-CHANGE_ME_ADMIN_PASS}"
# YubiKey assignment (will be detected automatically if not set)
YK1_SERIAL="${YK1_SERIAL:-}" # Root CA YubiKey serial
YK2_SERIAL="${YK2_SERIAL:-}" # Intermediate CA YubiKey serial
# PINs (will be generated if not set)
YK1_PIN="${YK1_PIN:-}"
YK1_PUK="${YK1_PUK:-}"
YK2_PIN="${YK2_PIN:-}"
YK2_PUK="${YK2_PUK:-}"
# ===================== COLORS =====================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
success() { echo -e "${GREEN}[OK]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }
# ===================== PRE-FLIGHT CHECKS =====================
check_prerequisites() {
info "Running pre-flight checks..."
# Must be root
if [[ $EUID -ne 0 ]]; then
error "This script must be run as root (use sudo)"
fi
# Check Ubuntu version
if ! grep -q "Ubuntu" /etc/os-release; then
error "This script requires Ubuntu"
fi
UBUNTU_VERSION=$(grep VERSION_ID /etc/os-release | cut -d'"' -f2)
info "Ubuntu version: $UBUNTU_VERSION"
# Check architecture
ARCH=$(uname -m)
info "Architecture: $ARCH"
# Check YubiKeys
YK_COUNT=$(lsusb 2>/dev/null | grep -c "Yubico" || true)
if [[ $YK_COUNT -lt 2 ]]; then
error "Need at least 2 YubiKeys connected (found: $YK_COUNT)"
fi
success "Found $YK_COUNT YubiKeys"
# Check disk space
AVAIL=$(df -BG / | awk 'NR==2 {print $4}' | tr -d 'G')
if [[ $AVAIL -lt 5 ]]; then
error "Need at least 5GB free disk space (have: ${AVAIL}GB)"
fi
success "Disk space: ${AVAIL}GB available"
}
# ===================== STEP 1: SYSTEM SETUP =====================
setup_system() {
info "=== Step 1: System Setup ==="
# Update system
info "Updating package lists..."
apt-get update -qq
# Install base packages
info "Installing packages..."
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
ufw fail2ban opensc pcscd yubikey-manager ykcs11 \
python3-pip python3-venv caddy sqlite3 libsqlite3-dev \
haveged apparmor-utils curl wget \
2>/dev/null || true
# Install Python packages
info "Installing Python packages..."
pip3 install --break-system-packages --ignore-installed \
typing_extensions 2>/dev/null || true
pip3 install --break-system-packages \
fastapi "uvicorn[standard]" jinja2 python-multipart \
bcrypt "python-jose[cryptography]" ecdsa cryptography \
2>/dev/null || true
success "Packages installed"
}
# ===================== STEP 2: CREATE USER =====================
setup_user() {
info "=== Step 2: Create $ADMIN_USER user ==="
if ! id "$ADMIN_USER" &>/dev/null; then
useradd -m -s /bin/bash "$ADMIN_USER"
usermod -aG sudo,adm,plugdev,pcscd "$ADMIN_USER"
echo "$ADMIN_USER:$ADMIN_PASS" | chpasswd
echo "$ADMIN_USER ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/certauth
chmod 0440 /etc/sudoers.d/certauth
success "User $ADMIN_USER created"
else
warn "User $ADMIN_USER already exists"
fi
}
# ===================== STEP 3: FIREWALL =====================
setup_firewall() {
info "=== Step 3: Configure Firewall ==="
ufw --force reset 2>/dev/null || true
ufw default deny incoming
ufw default deny outgoing
ufw allow in on lo
ufw allow out on lo
ufw allow out 53
ufw allow out 123
ufw allow out 80/tcp
ufw allow out 443/tcp
ufw allow from "$NETWORK_CIDR" port 22 proto tcp
ufw allow from "$NETWORK_CIDR" port 80 proto tcp
ufw allow from "$NETWORK_CIDR" port 443 proto tcp
echo "y" | ufw enable
# Create swap
if [[ ! -f /swapfile ]]; then
fallocate -l 4G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo "/swapfile none swap sw 0 0" >> /etc/fstab
success "Swap created (4GB)"
fi
success "Firewall configured"
}
# ===================== STEP 4: DETECT YUBIKEYS =====================
detect_yubikeys() {
info "=== Step 4: Detect YubiKeys ==="
# Start pcscd
systemctl enable pcscd
systemctl start pcscd
sleep 2
# Fix polkit for pcscd access
mkdir -p /etc/polkit-1/rules.d
cat > /etc/polkit-1/rules.d/45-access-pcsc.rules << 'POLKIT'
polkit.addRule(function(action, subject) {
if (action.id == "org.debian.pcsc-lite.access_pcsc") {
return polkit.Result.YES;
}
});
POLKIT
systemctl restart polkit
systemctl restart pcscd
sleep 2
# List YubiKeys
info "Connected YubiKeys:"
sudo -u "$ADMIN_USER" ykman list 2>/dev/null || ykman list
# Auto-detect serials if not set
SERIALS=$(ykman list 2>/dev/null | grep -oP 'Serial: \K\d+' || true)
if [[ -z "$YK1_SERIAL" ]]; then
YK1_SERIAL=$(echo "$SERIALS" | head -1)
fi
if [[ -z "$YK2_SERIAL" ]]; then
YK2_SERIAL=$(echo "$SERIALS" | tail -1)
fi
info "YK1 (Root CA): $YK1_SERIAL"
info "YK2 (Intermediate): $YK2_SERIAL"
# Generate PINs if not set
if [[ -z "$YK1_PIN" ]]; then
YK1_PIN=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 8)
YK1_PUK=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 8)
fi
if [[ -z "$YK2_PIN" ]]; then
YK2_PIN=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 8)
YK2_PUK=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 8)
fi
echo ""
echo "============================================================"
echo " YUBIKEY CREDENTIALS - RECORD THESE SECURELY"
echo "============================================================"
echo ""
echo "YK1 (Root CA) - Serial $YK1_SERIAL:"
echo " PIN: $YK1_PIN"
echo " PUK: $YK1_PUK"
echo ""
echo "YK2 (Intermediate) - Serial $YK2_SERIAL:"
echo " PIN: $YK2_PIN"
echo " PUK: $YK2_PUK"
echo ""
echo "============================================================"
echo ""
}
# ===================== STEP 5: CONFIGURE YUBIKEYS =====================
configure_yubikeys() {
info "=== Step 5: Configure YubiKeys ==="
DEFAULT_MGMT="010203040506070801020304050607080102030405060708"
# --- YK1: Root CA ---
info "Configuring YK1 (Root CA)..."
echo "y" | ykman -d "$YK1_SERIAL" piv reset
ykman -d "$YK1_SERIAL" piv keys generate --algorithm ECCP384 \
-m "$DEFAULT_MGMT" 9c /tmp/yk1-root-pub.pem
ykman -d "$YK1_SERIAL" piv access change-pin -P 123456 --new-pin "$YK1_PIN"
ykman -d "$YK1_SERIAL" piv access change-puk -p 12345678 --new-puk "$YK1_PUK"
echo "" | ykman -d "$YK1_SERIAL" piv certificates generate \
-P "$YK1_PIN" -m "$DEFAULT_MGMT" \
--subject "CN=$ROOT_CA_CN,O=$CA_ORG,C=$CA_COUNTRY" \
--valid-days "$ROOT_VALID_DAYS" --hash-algorithm SHA384 \
9c /tmp/yk1-root-pub.pem
ykman -d "$YK1_SERIAL" piv certificates export 9c /tmp/root-ca-temp.crt
success "YK1 configured"
# --- YK2: Intermediate CA ---
info "Configuring YK2 (Intermediate CA)..."
echo "y" | ykman -d "$YK2_SERIAL" piv reset
ykman -d "$YK2_SERIAL" piv keys generate --algorithm ECCP384 \
-m "$DEFAULT_MGMT" 9c /tmp/yk2-int-pub.pem
ykman -d "$YK2_SERIAL" piv access change-pin -P 123456 --new-pin "$YK2_PIN"
ykman -d "$YK2_SERIAL" piv access change-puk -p 12345678 --new-puk "$YK2_PUK"
echo "" | ykman -d "$YK2_SERIAL" piv certificates request \
-P "$YK2_PIN" \
--subject "CN=$INT_CA_CN,O=$CA_ORG,C=$CA_COUNTRY" \
9c /tmp/yk2-int-pub.pem /tmp/yk2-intermediate.csr
success "YK2 configured"
}
# ===================== STEP 6: GENERATE CA CERTS =====================
generate_ca_certs() {
info "=== Step 6: Generate CA Certificates ==="
# Create directories
mkdir -p /etc/ssl/ca/{root,intermediate,issued,crl}
mkdir -p /var/lib/certauth/tmp
mkdir -p /var/log/certauth
chown -R "$ADMIN_USER:$ADMIN_USER" /etc/ssl/ca /var/lib/certauth /var/log/certauth
chmod -R 700 /etc/ssl/ca /var/lib/certauth /var/log/certauth
# Generate Root CA cert with proper extensions
info "Generating Root CA certificate..."
python3 << ROOTPY
import datetime, subprocess, base64
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509.oid import NameOID
with open("/tmp/yk1-root-pub.pem", "rb") as f:
root_pub = serialization.load_pem_public_key(f.read())
subj = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "$CA_COUNTRY"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "$CA_ORG"),
x509.NameAttribute(NameOID.COMMON_NAME, "$ROOT_CA_CN"),
])
builder = (x509.CertificateBuilder()
.subject_name(subj).issuer_name(subj)
.public_key(root_pub).serial_number(x509.random_serial_number())
.not_valid_before(datetime.datetime.now(datetime.timezone.utc))
.not_valid_after(datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=$ROOT_VALID_DAYS))
.add_extension(x509.BasicConstraints(ca=True, path_length=1), critical=True)
.add_extension(x509.KeyUsage(digital_signature=True, key_cert_sign=True, crl_sign=True,
key_encipherment=False, content_commitment=False, data_encipherment=False,
key_agreement=False, encipher_only=False, decipher_only=False), critical=True)
.add_extension(x509.SubjectKeyIdentifier.from_public_key(root_pub), critical=False)
.add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(root_pub), critical=False))
tmp = ec.generate_private_key(ec.SECP384R1())
temp = builder.sign(tmp, hashes.SHA384())
td = temp.public_bytes(serialization.Encoding.DER)
# Parse TBS
o = 1
if td[o] & 0x80: n = td[o] & 0x7f; o += 1 + n
else: o += 1
tbs_start = o
o += 1
if td[o] & 0x80: n = td[o] & 0x7f; tl = int.from_bytes(td[o+1:o+1+n], "big"); o += 1 + n
else: tl = td[o]; o += 1
tbs_end = o + tl
tbs_full = td[tbs_start:tbs_end]
alg_start = tbs_end
o2 = alg_start + 1
if td[o2] & 0x80: n = td[o2] & 0x7f; al = int.from_bytes(td[o2+1:o2+1+n], "big"); o2 += 1 + n
else: al = td[o2]; o2 += 1
alg_full = td[alg_start:o2+al]
# Sign with YubiKey
with open("/tmp/tbs.der", "wb") as f: f.write(tbs_full)
r = subprocess.run(["pkcs11-tool", "--login", "--pin", "$YK1_PIN",
"--sign", "--mechanism", "ECDSA-SHA384", "--label", "SIGN key",
"--input-file", "/tmp/tbs.der", "--output-file", "/tmp/sig.bin"],
capture_output=True, text=True)
if r.returncode != 0:
print(f"Sign failed: {r.stderr}")
exit(1)
with open("/tmp/sig.bin", "rb") as f: raw = f.read()
rb = raw[:48].lstrip(b"\x00") or b"\x00"
sb = raw[48:].lstrip(b"\x00") or b"\x00"
if rb[0] & 0x80: rb = b"\x00" + rb
if sb[0] & 0x80: sb = b"\x00" + sb
r_der = b"\x02" + bytes([len(rb)]) + rb
s_der = b"\x02" + bytes([len(sb)]) + sb
seq = b"\x30" + bytes([len(r_der+s_der)]) + r_der + s_der
bs = b"\x00" + seq
new_sig = b"\x03" + bytes([len(bs)]) + bs
content = tbs_full + alg_full + new_sig
cl = len(content)
final = b"\x30\x82" + bytes([cl>>8, cl&0xff]) + content
with open("/tmp/root.der", "wb") as f: f.write(final)
r = subprocess.run(["openssl", "x509", "-inform", "DER", "-outform", "PEM",
"-in", "/tmp/root.der", "-out", "/tmp/root.pem"], capture_output=True, text=True)
if r.returncode != 0:
print(f"DER->PEM failed: {r.stderr}")
exit(1)
with open("/tmp/root.pem") as f: pem = f.read()
with open("/etc/ssl/ca/root/root-ca.crt", "w") as f: f.write(pem)
# Verify
r = subprocess.run(["openssl", "verify", "-CAfile", "/etc/ssl/ca/root/root-ca.crt",
"/etc/ssl/ca/root/root-ca.crt"], capture_output=True, text=True)
print(f"Root CA self-verify: {r.stdout.strip() or r.stderr.strip()}")
# Import to YK1
subprocess.run(["echo", "", "|", "ykman", "-d", "$YK1_SERIAL", "piv", "certificates",
"import", "9c", "/etc/ssl/ca/root/root-ca.crt"], shell=True)
print("Root CA imported to YK1")
ROOTPY
# Generate Intermediate CA cert signed by Root
info "Generating Intermediate CA certificate..."
python3 << INTPY
import datetime, subprocess, base64
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509.oid import NameOID
with open("/tmp/yk1-root-pub.pem", "rb") as f:
root_pub = serialization.load_pem_public_key(f.read())
with open("/tmp/yk2-int-pub.pem", "rb") as f:
int_pub = serialization.load_pem_public_key(f.read())
root_subj = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "$CA_COUNTRY"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "$CA_ORG"),
x509.NameAttribute(NameOID.COMMON_NAME, "$ROOT_CA_CN"),
])
int_subj = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "$CA_COUNTRY"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "$CA_ORG"),
x509.NameAttribute(NameOID.COMMON_NAME, "$INT_CA_CN"),
])
builder = (x509.CertificateBuilder()
.subject_name(int_subj).issuer_name(root_subj)
.public_key(int_pub).serial_number(x509.random_serial_number())
.not_valid_before(datetime.datetime.now(datetime.timezone.utc))
.not_valid_after(datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=$INT_VALID_DAYS))
.add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True)
.add_extension(x509.KeyUsage(digital_signature=True, key_cert_sign=True, crl_sign=True,
key_encipherment=False, content_commitment=False, data_encipherment=False,
key_agreement=False, encipher_only=False, decipher_only=False), critical=True)
.add_extension(x509.SubjectKeyIdentifier.from_public_key(int_pub), critical=False)
.add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(root_pub), critical=False))
tmp = ec.generate_private_key(ec.SECP384R1())
temp = builder.sign(tmp, hashes.SHA384())
td = temp.public_bytes(serialization.Encoding.DER)
o = 1
if td[o] & 0x80: n = td[o] & 0x7f; o += 1 + n
else: o += 1
tbs_start = o
o += 1
if td[o] & 0x80: n = td[o] & 0x7f; tl = int.from_bytes(td[o+1:o+1+n], "big"); o += 1 + n
else: tl = td[o]; o += 1
tbs_end = o + tl
tbs_full = td[tbs_start:tbs_end]
alg_start = tbs_end
o2 = alg_start + 1
if td[o2] & 0x80: n = td[o2] & 0x7f; al = int.from_bytes(td[o2+1:o2+1+n], "big"); o2 += 1 + n
else: al = td[o2]; o2 += 1
alg_full = td[alg_start:o2+al]
with open("/tmp/tbs.der", "wb") as f: f.write(tbs_full)
r = subprocess.run(["pkcs11-tool", "--login", "--pin", "$YK1_PIN",
"--sign", "--mechanism", "ECDSA-SHA384", "--label", "SIGN key",
"--input-file", "/tmp/tbs.der", "--output-file", "/tmp/sig.bin"],
capture_output=True, text=True)
if r.returncode != 0:
print(f"Sign failed: {r.stderr}")
exit(1)
with open("/tmp/sig.bin", "rb") as f: raw = f.read()
rb = raw[:48].lstrip(b"\x00") or b"\x00"
sb = raw[48:].lstrip(b"\x00") or b"\x00"
if rb[0] & 0x80: rb = b"\x00" + rb
if sb[0] & 0x80: sb = b"\x00" + sb
r_der = b"\x02" + bytes([len(rb)]) + rb
s_der = b"\x02" + bytes([len(sb)]) + sb
seq = b"\x30" + bytes([len(r_der+s_der)]) + r_der + s_der
bs = b"\x00" + seq
new_sig = b"\x03" + bytes([len(bs)]) + bs
content = tbs_full + alg_full + new_sig
cl = len(content)
final = b"\x30\x82" + bytes([cl>>8, cl&0xff]) + content
with open("/tmp/int.der", "wb") as f: f.write(final)
r = subprocess.run(["openssl", "x509", "-inform", "DER", "-outform", "PEM",
"-in", "/tmp/int.der", "-out", "/tmp/int.pem"], capture_output=True, text=True)
if r.returncode != 0:
print(f"DER->PEM failed: {r.stderr}")
exit(1)
with open("/tmp/int.pem") as f: pem = f.read()
with open("/etc/ssl/ca/intermediate/intermediate-ca.crt", "w") as f: f.write(pem)
# Verify chain
r = subprocess.run(["openssl", "verify", "-CAfile", "/etc/ssl/ca/root/root-ca.crt",
"/etc/ssl/ca/intermediate/intermediate-ca.crt"], capture_output=True, text=True)
print(f"Chain verify: {r.stdout.strip() or r.stderr.strip()}")
# Import to YK2
subprocess.run(["echo", "", "|", "ykman", "-d", "$YK2_SERIAL", "piv", "certificates",
"import", "9c", "/etc/ssl/ca/intermediate/intermediate-ca.crt"], shell=True)
print("Intermediate CA imported to YK2")
INTPY
# Create CA chain file
cat /etc/ssl/ca/intermediate/intermediate-ca.crt /etc/ssl/ca/root/root-ca.crt \
> /etc/ssl/ca/ca-chain.crt
success "CA certificates generated and verified"
}
# ===================== STEP 7: INSTALL API =====================
install_api() {
info "=== Step 7: Install Certificate API ==="
# Create API directory
mkdir -p /opt/certauth/{api/templates,api/static,logs}
chown -R "$ADMIN_USER:$ADMIN_USER" /opt/certauth
# Deploy API from the repository (single source of truth: the api/
# directory next to this script). Never inline a second copy here.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
for f in config.py models.py auth.py signing.py main.py; do
if [ ! -f "$SCRIPT_DIR/api/$f" ]; then
error "api/$f not found next to setup-certauth.sh (run from the repository checkout)"
fi
done
cp "$SCRIPT_DIR/api/config.py" "$SCRIPT_DIR/api/models.py" "$SCRIPT_DIR/api/auth.py" \
"$SCRIPT_DIR/api/signing.py" "$SCRIPT_DIR/api/main.py" /opt/certauth/api/
cp -r "$SCRIPT_DIR/api/templates/." /opt/certauth/api/templates/
mkdir -p "$SCRIPT_DIR/api/static"
cp -r "$SCRIPT_DIR/api/static/." /opt/certauth/api/static/
# Runtime configuration. The API fails closed on startup if any variable
# is missing, so this file must be complete and private.
JWT_SECRET_GENERATED="$(python3 -c 'import secrets; print(secrets.token_hex(32))')"
install -d -m 750 -o "$ADMIN_USER" -g "$ADMIN_USER" /etc/certauth
cat > /etc/certauth/certauth.env << ENV_EOF
YK_ROOT_SERIAL=$YK1_SERIAL
YK_INT_SERIAL=$YK2_SERIAL
YK_ROOT_PIN=$YK1_PIN
YK_INT_PIN=$YK2_PIN
JWT_SECRET=$JWT_SECRET_GENERATED
ADMIN_USERNAME=$ADMIN_USER
ADMIN_PASSWORD=$ADMIN_PASS
ENV_EOF
chown "$ADMIN_USER:$ADMIN_USER" /etc/certauth/certauth.env
chmod 600 /etc/certauth/certauth.env
# Initialize database (loads the private env file as the API user)
sudo -u "$ADMIN_USER" bash -c 'set -a; . /etc/certauth/certauth.env; set +a; cd /opt/certauth/api; python3 -c "from models import init_db; init_db()"'
success "API installed"
}
# ===================== STEP 8: SERVICES =====================
setup_services() {
info "=== Step 8: Configure Services ==="
# Caddy
cat > /etc/caddy/Caddyfile << 'CADDYEOF'
:80 {
encode gzip
reverse_proxy 127.0.0.1:8000 {
header_up Host {host}
header_up X-Real-IP {remote}
}
}
CADDYEOF
# Systemd service for API
cat > /etc/systemd/system/certauth-api.service << SVCCEOF
[Unit]
Description=CertAuth Certificate Management API
After=network.target pcscd.service
[Service]
Type=simple
User=$ADMIN_USER
Group=$ADMIN_USER
WorkingDirectory=/opt/certauth/api
Environment=PYTHONUNBUFFERED=1
EnvironmentFile=/etc/certauth/certauth.env
ExecStart=/usr/bin/python3 -m uvicorn main:app --host 127.0.0.1 --port 8000
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
SVCCEOF
systemctl daemon-reload
systemctl enable caddy certauth-api pcscd
systemctl start caddy certauth-api
success "Services configured and started"
}
# ===================== STEP 9: VERIFY =====================
verify_setup() {
info "=== Step 9: Verification ==="
sleep 3
# Check services
for svc in certauth-api caddy pcscd; do
STATUS=$(systemctl is-active "$svc")
if [[ "$STATUS" == "active" ]]; then
success "$svc: active"
else
warn "$svc: $STATUS"
fi
done
# Test API
HEALTH=$(curl -s http://localhost:8000/api/health 2>/dev/null || echo "FAILED")
if echo "$HEALTH" | grep -q "ok"; then
success "API health check: OK"
else
warn "API health check: $HEALTH"
fi
# Verify CA chain
openssl verify -CAfile /etc/ssl/ca/root/root-ca.crt \
/etc/ssl/ca/intermediate/intermediate-ca.crt 2>&1 | while read line; do
if echo "$line" | grep -q "OK"; then
success "CA chain verification: OK"
else
warn "CA chain: $line"
fi
done
echo ""
echo "============================================================"
echo " SETUP COMPLETE"
echo "============================================================"
echo ""
echo " Access the Key Vault UI at:"
echo " http://$(hostname -I | awk '{print $1}')/"
echo ""
echo " Admin login:"
echo " Username: $ADMIN_USER"
echo " Password: $ADMIN_PASS"
echo ""
echo " API endpoints:"
echo " POST /api/token - Login (returns JWT)"
echo " GET /api/domains - List domains"
echo " POST /api/domains - Register domain"
echo " GET /api/certs - List certificates"
echo " POST /api/certs/request - Request certificate"
echo " POST /api/certs/{id}/sign - Sign certificate (requires YK2)"
echo " GET /api/certs/{id}/download - Download cert"
echo " GET /api/certs/{id}/key - Download private key"
echo " GET /api/ca-chain - Download CA chain"
echo ""
echo " IMPORTANT: Change the admin password after first login!"
echo "============================================================"
}
# ===================== MAIN =====================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo ""
echo "============================================================"
echo " CertAuth Setup"
echo "============================================================"
echo ""
check_prerequisites
setup_system
setup_user
setup_firewall
detect_yubikeys
configure_yubikeys
generate_ca_certs
install_api
setup_services
verify_setup
echo ""
success "All done!"