From 82fdb161cced3209e8bc769818b60678d6c53866 Mon Sep 17 00:00:00 2001 From: reachableceo Date: Thu, 3 Sep 2026 07:52:07 -0500 Subject: [PATCH] =?UTF-8?q?feat(ca):=20fleet=20CA=20tooling=20=E2=80=94=20?= =?UTF-8?q?intermediate=20live=20on=20tsys-ca,=20first=20cert=20issued=20[?= =?UTF-8?q?#697]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- AGENTS.md | 1 + ca/README.md | 36 ++++++++++++++++++++++++++++++++++ ca/ca-init.sh | 50 ++++++++++++++++++++++++++++++++++++++++++++++++ ca/issue-cert.sh | 36 ++++++++++++++++++++++++++++++++++ ca/selftest.sh | 17 ++++++++++++++++ 5 files changed, 140 insertions(+) create mode 100644 ca/README.md create mode 100644 ca/ca-init.sh create mode 100644 ca/issue-cert.sh create mode 100644 ca/selftest.sh diff --git a/AGENTS.md b/AGENTS.md index 185e80e..d71a5c6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -364,6 +364,7 @@ oam/ OAM platform tooling (oxidized, unpoller, smokeping, netdisc proxmox/ Proxmox fleet docs (hardware audit, capacity, storage) + perf tuning (perf/) awx/ Ansible AWX deployment (k3s + AWX Operator) siem/ Wazuh/SIEM deploy tooling (agents on VMs, rsyslog forwarding on PVE) [#335] +ca/ Fleet CA tooling (init/issue/selftest; intermediate on tsys-ca; design: Discourse t/320) [#697] cmdb/ GLPI/CMDB seed tooling (inventory→CSV converter; design: Discourse t/319, #705) tests/ Test suite + VM validation harness + remote.sh SSH chokepoint scripts/ Framework: git hooks, rule engine (check-rules.sh), shared lib diff --git a/ca/README.md b/ca/README.md new file mode 100644 index 0000000..abcd6ee --- /dev/null +++ b/ca/README.md @@ -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 "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. diff --git a/ca/ca-init.sh b/ca/ca-init.sh new file mode 100644 index 0000000..515b53a --- /dev/null +++ b/ca/ca-init.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# +# ca-init.sh — initialize the fleet CA on the CA host [#697] +# Usage: ca-init.sh +# 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 }" +INT_DIR="${2:?usage: ca-init.sh }" +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" diff --git a/ca/issue-cert.sh b/ca/issue-cert.sh new file mode 100644 index 0000000..32941a8 --- /dev/null +++ b/ca/issue-cert.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# +# issue-cert.sh — issue a leaf certificate from the fleet intermediate [#697] +# Usage: issue-cert.sh "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: /.crt (leaf+chain) + /.key (0600) on this host. +# +set -euo pipefail + +CN="${1:?usage: issue-cert.sh \"SAN list\"}" +SAN="${2:?usage: issue-cert.sh \"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)" diff --git a/ca/selftest.sh b/ca/selftest.sh new file mode 100644 index 0000000..5ff9ccc --- /dev/null +++ b/ca/selftest.sh @@ -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"