feat(ca): fleet CA tooling — intermediate live on tsys-ca, first cert issued [#697]
ca-init/issue-cert/selftest (TDD loop, shellcheck clean); design doc on Discourse t/320. Offline RSA-4096 root (Nitrokey ceremony later), 5y intermediate, 825d SAN leaves. First cert: tsys-wazuh.knel.net (fingerprint on the ticket note). Meat: https://projects.knownelement.com/issues/697#note-4033
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# ca/ — PFV fleet Certificate Authority tooling [#697]
|
||||
|
||||
> Design + runbook (canonical): https://community.turnsys.com/t/320
|
||||
> Root of trust for: LDAPS, RADIUS (#476), OPNsense, k8s, iDRAC/OME,
|
||||
> Wazuh/syslog TLS. HSM-backed root ceremony comes later (#697 Nitrokey).
|
||||
|
||||
## Architecture (v1, software)
|
||||
|
||||
- **Root CA**: RSA-4096, 10y, OFFLINE — lives only in `/root/ca-root/` on
|
||||
tsys-ca (moved to Nitrokey at the #697 ceremony). Never leaves the box;
|
||||
signs only the intermediate.
|
||||
- **Intermediate CA**: RSA-4096, 5y, on tsys-ca at `/etc/ssl/tsys-ca/` —
|
||||
signs all leaf certs. pathlen:0.
|
||||
- **Leaves**: RSA-2048, ≤825 days, SAN-based (serverAuth + clientAuth).
|
||||
- **No CRL/OCSP in v1** (fleet-internal); revocation = re-issue + intermediate
|
||||
pinning. CRL endpoint deferred to the GLPI/ITSM pass.
|
||||
- **Compat stance**: RSA+SHA-256 only — old Dell/iDRAC-era clients.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `ca-init.sh` | Initialize root + intermediate dirs ON THE CA HOST (keys never leave tsys-ca) |
|
||||
| `issue-cert.sh` | Issue a leaf cert: `issue-cert.sh <common-name> "DNS:a,DNS:b,IP:x"` |
|
||||
| `selftest.sh` | Full throwaway loop in /tmp — init, issue, chain-verify (TDD gate) |
|
||||
|
||||
## Usage (on tsys-ca, as root)
|
||||
|
||||
```bash
|
||||
bash ca-init.sh /root/ca-root /etc/ssl/tsys-ca # once
|
||||
bash issue-cert.sh tsys-wazuh.knel.net "DNS:tsys-wazuh.knel.net,DNS:tsys-siem.knel.net"
|
||||
```
|
||||
|
||||
Certs/CSRs land in the intermediate dir's `certs/` + `csr/`. Private keys
|
||||
stay on tsys-ca (0600); nothing in this directory is ever committed with
|
||||
key material.
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# ca-init.sh — initialize the fleet CA on the CA host [#697]
|
||||
# Usage: ca-init.sh <root-dir> <intermediate-dir>
|
||||
# Creates: root CA (RSA-4096, 10y, offline dir) + intermediate (RSA-4096, 5y)
|
||||
# signing with the root. Keys are generated locally; nothing leaves the host.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="${1:?usage: ca-init.sh <root-dir> <intermediate-dir>}"
|
||||
INT_DIR="${2:?usage: ca-init.sh <root-dir> <intermediate-dir>}"
|
||||
ROOT_KEY="$ROOT_DIR/root.key"
|
||||
ROOT_CRT="$ROOT_DIR/root.crt"
|
||||
INT_KEY="$INT_DIR/intermediate.key"
|
||||
INT_CSR="$INT_DIR/intermediate.csr"
|
||||
INT_CRT="$INT_DIR/intermediate.crt"
|
||||
|
||||
if [ -s "$ROOT_CRT" ]; then
|
||||
echo "root already exists at $ROOT_CRT — refusing to overwrite" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$ROOT_DIR" "$INT_DIR/certs" "$INT_DIR/csr"
|
||||
chmod 700 "$ROOT_DIR"
|
||||
|
||||
# --- Root CA (offline; signs only the intermediate) ---
|
||||
openssl genrsa -out "$ROOT_KEY" 4096 2>/dev/null
|
||||
chmod 400 "$ROOT_KEY"
|
||||
openssl req -x509 -new -key "$ROOT_KEY" -sha256 -days 3650 \
|
||||
-out "$ROOT_CRT" \
|
||||
-subj "/C=US/ST=Texas/O=Known Element Enterprises/OU=TechOps/CN=PFV Fleet Root CA" \
|
||||
-addext "basicConstraints=critical,CA:TRUE,pathlen:1" \
|
||||
-addext "keyUsage=critical,keyCertSign,cRLSign" \
|
||||
-addext "subjectKeyIdentifier=hash"
|
||||
echo "root CA: $ROOT_CRT ($(openssl x509 -in "$ROOT_CRT" -noout -subject))"
|
||||
|
||||
# --- Intermediate CA (signs leaves) ---
|
||||
openssl genrsa -out "$INT_KEY" 4096 2>/dev/null
|
||||
chmod 400 "$INT_KEY"
|
||||
openssl req -new -key "$INT_KEY" -out "$INT_CSR" -sha256 \
|
||||
-subj "/C=US/ST=Texas/O=Known Element Enterprises/OU=TechOps/CN=PFV Fleet Intermediate CA"
|
||||
openssl x509 -req -in "$INT_CSR" -CA "$ROOT_CRT" -CAkey "$ROOT_KEY" \
|
||||
-CAcreateserial -days 1825 -sha256 -out "$INT_CRT" \
|
||||
-extfile <(printf 'basicConstraints=critical,CA:TRUE,pathlen:0\nkeyUsage=critical,keyCertSign,cRLSign\nsubjectKeyIdentifier=hash\nauthorityKeyIdentifier=keyid:always')
|
||||
echo "intermediate: $INT_CRT"
|
||||
|
||||
# --- Chain file for distribution to TLS servers ---
|
||||
cat "$INT_CRT" "$ROOT_CRT" > "$INT_DIR/ca-chain.crt"
|
||||
openssl verify -CAfile "$ROOT_CRT" "$INT_CRT"
|
||||
echo "init complete: intermediate ready at $INT_DIR"
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# issue-cert.sh — issue a leaf certificate from the fleet intermediate [#697]
|
||||
# Usage: issue-cert.sh <common-name> "SAN list" e.g.
|
||||
# issue-cert.sh tsys-wazuh.knel.net "DNS:tsys-wazuh.knel.net,DNS:tsys-siem.knel.net"
|
||||
# Requires: intermediate initialized at /etc/ssl/tsys-ca (ca-init.sh).
|
||||
# Output: <certs>/<cn>.crt (leaf+chain) + <certs>/<cn>.key (0600) on this host.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
CN="${1:?usage: issue-cert.sh <common-name> \"SAN list\"}"
|
||||
SAN="${2:?usage: issue-cert.sh <common-name> \"SAN list\"}"
|
||||
INT_DIR="${INT_DIR:-/etc/ssl/tsys-ca}"
|
||||
INT_KEY="$INT_DIR/intermediate.key"
|
||||
INT_CRT="$INT_DIR/intermediate.crt"
|
||||
CHAIN="$INT_DIR/ca-chain.crt"
|
||||
|
||||
[ -s "$INT_CRT" ] || { echo "intermediate missing at $INT_CRT — run ca-init.sh first" >&2; exit 1; }
|
||||
[ -s "$INT_KEY" ] || { echo "intermediate key missing" >&2; exit 1; }
|
||||
|
||||
KEY="$INT_DIR/certs/$CN.key"
|
||||
CSR="$INT_DIR/csr/$CN.csr"
|
||||
CRT="$INT_DIR/certs/$CN.crt"
|
||||
[ -e "$CRT" ] && { echo "cert already exists: $CRT (revoke/rename first)" >&2; exit 1; }
|
||||
|
||||
openssl genrsa -out "$KEY" 2048 2>/dev/null
|
||||
chmod 600 "$KEY"
|
||||
openssl req -new -key "$KEY" -out "$CSR" -sha256 \
|
||||
-subj "/C=US/ST=Texas/O=Known Element Enterprises/OU=TechOps/CN=$CN"
|
||||
openssl x509 -req -in "$CSR" -CA "$INT_CRT" -CAkey "$INT_KEY" \
|
||||
-CAcreateserial -days 825 -sha256 -out "$CRT" \
|
||||
-extfile <(printf 'basicConstraints=CA:FALSE\nkeyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth,clientAuth\nsubjectAltName=%s\nauthorityKeyIdentifier=keyid,issuer\n' "$SAN")
|
||||
|
||||
cat "$CRT" "$CHAIN" > "$INT_DIR/certs/$CN.fullchain"
|
||||
openssl verify -CAfile "$CHAIN" "$CRT"
|
||||
echo "issued: $CRT (+ .key, .csr, .fullchain)"
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# selftest.sh — throwaway end-to-end CA loop in /tmp [#697]
|
||||
# TDD gate for the CA tooling: init -> issue -> verify chain -> negative check.
|
||||
#
|
||||
set -euo pipefail
|
||||
T="$(mktemp -d)"
|
||||
trap 'rm -rf "$T"' EXIT
|
||||
|
||||
bash "$(dirname "$0")/ca-init.sh" "$T/root" "$T/int" > /dev/null
|
||||
[ -s "$T/root/root.key" ] && [ -s "$T/int/intermediate.crt" ] || { echo "FAIL: init" ; exit 1; }
|
||||
|
||||
INT_DIR="$T/int" bash "$(dirname "$0")/issue-cert.sh" test.knel.net "DNS:test.knel.net,IP:10.0.0.1" > /dev/null
|
||||
openssl verify -CAfile "$T/root/root.crt" "$T/int/intermediate.crt" > /dev/null
|
||||
openssl x509 -in "$T/int/certs/test.knel.net.crt" -noout -ext subjectAltName | grep -q "test.knel.net"
|
||||
openssl x509 -in "$T/int/certs/test.knel.net.fullchain" -noout | head -1 > /dev/null
|
||||
echo "SELFTEST PASS: root->intermediate->leaf chain verified with SANs"
|
||||
Reference in New Issue
Block a user