# Phase 3 CA quickstart — the "few lines of code" made literal **What this is:** `PHASED_ROLLOUT.md` Phase 3 says "a small utility does not need an enterprise PKI; the PoC's CA is a few lines." This is those lines, as real `openssl` commands instead of Python — every command below was actually run, in order, on 2026-08-03 (OpenSSL 3.5.5), not copied from a tutorial. Same architecture Test 3 builds programmatically: a self-signed root CA, per-device certs with identity in the SAN, not the deprecated CommonName. *Security review 2026-08-03 (Unity-Harmonia, binding axis): the commands below were hardened after first drafting — passphrase-protected CA key, RSA-3072 root, and explicit certificate extensions instead of copying them from the request. Every command in its current form was re-run and its output checked, including a deliberate attempt to have a request escalate itself to a CA.* **Windows / Git Bash note:** if you're running these in Git Bash, prefix with `MSYS_NO_PATHCONV=1` — otherwise Git Bash mangles the `/CN=...` subject string into a path. Not an issue in a real Linux shell or PowerShell. ## 1. Create the root CA (once) ```bash openssl req -x509 -newkey rsa:3072 -keyout ca.key -out ca.pem -days 3650 \ -subj "/CN=YourUtility Root CA" ``` You will be prompted for a passphrase, twice. **Use one.** There is no `-noenc` here on purpose: `-noenc` writes the CA private key to disk in plaintext, and this key is the single highest-value target on your network — whoever holds it can mint a valid certificate for *any* identity in the fleet. Store the passphrase somewhere other than the machine holding the key. `ca.key` is that private key — the one file that must never leave a controlled location. `ca.pem` is the public CA certificate every device and workstation will trust. **On key size and lifetime, honestly:** RSA-3072 is used for the *root* because this certificate lives 10 years (`-days 3650`) and RSA-2048 is only credited with security guidance through roughly 2030 — a 2048-bit root minted today would outlive its own algorithm. ECDSA P-384 is an equally good or better choice **if your devices accept it**; legacy Logix hardware often does not, which is why RSA is shown here. If your equipment can only handle RSA-2048, that is a workable answer — but then shorten the root's lifetime (e.g. `-days 1825`) so it does not outlast the crypto. Device certificates below stay at RSA-2048/825 days, which is fine at that lifetime and maximises compatibility. ## 2. Issue a per-device certificate (once per device — this is what "per-device identity" means) ```bash # Generate the device's own key + certificate request: openssl req -newkey rsa:2048 -keyout device-a.key -out device-a.csr -noenc \ -subj "/CN=device-a" # State the extensions YOURSELF -- never copy them from the request (see the warning below): cat > device-a.ext <<'EOF' subjectAltName = DNS:device-a basicConstraints = CA:FALSE keyUsage = critical, digitalSignature, keyEncipherment extendedKeyUsage = serverAuth, clientAuth EOF # Sign it with the CA (you will be prompted for the CA passphrase): openssl x509 -req -in device-a.csr -CA ca.pem -CAkey ca.key -CAcreateserial \ -out device-a.pem -days 825 -extfile device-a.ext ``` > **Why an `-extfile` and not `-copy_extensions copyall`.** `copyall` copies *every* extension out > of the certificate request into the signed certificate — including `basicConstraints = CA:TRUE` > if the request asks for it. OpenSSL's own manual warns about this. In this quickstart you write > the request yourself, so nothing is wrong today; but step 2 says *repeat for every device and > every engineer credential*, and in real operation requests arrive **from** the requester. At that > point a device that asks to be a CA becomes one — and a device certificate that is a CA can mint > a valid identity for **anything on your network**, which is the exact fleet-wide-compromise shape > this whole repo exists to close. > > Verified on 2026-08-03 (OpenSSL 3.5.5), by signing a deliberately malicious request that asked > for `CA:TRUE`: with `-copy_extensions copyall` the issued certificate came back **`CA:TRUE`**; > with the `-extfile` above it came back **`CA:FALSE`**. Stating the extensions yourself is what > closes it. Verify the extensions actually landed — the SAN is the field Test 3 checks (not CommonName), and `CA:FALSE` is what keeps a device credential from becoming an issuer: ```bash openssl x509 -in device-a.pem -noout -text | grep -A1 -E "Alternative Name|Basic Constraints|Key Usage" # X509v3 Subject Alternative Name: DNS:device-a # X509v3 Basic Constraints: CA:FALSE # X509v3 Key Usage: critical Digital Signature, Key Encipherment # X509v3 Extended Key Usage: TLS Web Server Authentication, TLS Web Client Authentication ``` Verify the chain: ```bash openssl verify -CAfile ca.pem device-a.pem # device-a.pem: OK ``` Repeat step 2 for every device and every engineer/workstation credential, changing `device-a` to the real identity each time. ## Where this quickstart stops, honestly **Revocation and rotation** (Tests 4/5 — CRL-based) are **not** a "few lines" via raw `openssl` CLI the way issuance is. Real CRL management needs the fuller `openssl ca` config-based workflow (an `index.txt`, a serial file, a CA config file) — genuinely more setup than steps 1–2 above, not something this quickstart should pretend is equally simple. For a small utility, a purpose-built lightweight CA tool (e.g. `step-ca` / Smallstep, designed specifically to make issuance *and* revocation manageable without an enterprise PKI team) is a more honest recommendation than hand-rolling the `openssl ca` workflow. If already in the Rockwell ecosystem, FactoryTalk Policy Manager is the vendor-native path for all of this, issuance through revocation. --- *l0gic — Patrick Crosby, 2026-08-03. Every command above was run and its output checked, not assumed. DRAFT — not committed, not pushed; holding for review.*