A Go CLI tool to manage a homelab/small-office Certificate Authority with centrally managed, age encrypted private keys.
Typical usage scenario: Run it on your desktop once a year or once a month to issue and deploy TLS certificates for your LAN/VPN devices, enabling them to provide HTTPS access without warnings. For easy management, you can keep your (encrypted) CA store and configuration within a Git repository.
- Features
- Motivation and Design Targets
- Cryptographic Implementation
- Installation
- Quick Start
- CLI Reference
- Common Workflows
- Emergency Access
- Configuration
- X.509 Certificate Extensions
- Store Structure
- Cryptographic Options
- Key Protection and Authentication
- Intermediate CAs
- agenix integration
- Development
- Browser Compatibility Matrix
- Limitations
- Alternative Solutions
- Further Reading: Introduction to PKI and X.509
- Create and manage a self-signed Certificate Authority
- Generate and renew certificates for hosts/services and other entities
- Strong key encryption with multiple providers:
- Password-based encryption using age with scrypt key derivation
- SSH key-based encryption using existing SSH identities (age-ssh)
- Hardware token encryption using age plugins (Secure Enclave, YubiKey, etc.)
- Certificate inventory and expiration tracking
- Simple deployment to target locations via shell scripts, for example directly to your FritzBox, Proxmox PVE instance or NixOS configuration
- Single statically-linked binary with no runtime dependencies
For a quick overview, maybe you want to have a look at the example configs.
Running your own CA works well to provide X.509 certificates to internal hosts and services, for them to offer TLS encryption. But certificate lifetimes are nowadays, 2025, limited to one year (by Apple at least), and the Industry [is] to Shift to 47-Day SSL/TLS Certificate Validity by 2029.
The certificate lifespan reductions will be implemented in phases:
- ~6 months (starting March 2026),
- ~3 months (starting March 2027), and
- 1.5 months (starting March 2029)
Therefore a one-button reissue & deploy solution is required, easily manageable as part of an infrastructure Git repo.
- “Inversion of control” of traditional CA flow: CSRs are rare, all keys are managed centrally
- Easily rekey, reissue and deploy to many hosts with a single command, so certificates can be kept fresh with minimal infrastructure and configuration
- Encryption of private keys, so config and store can be shared via Git
- Modern CLI
- Sane and secure defaults
- Easy to deploy and package
- Proper documentation including basic X.509/CA knowledge
ReactorCA is built on proven cryptographic foundations:
- Go Standard Crypto: Uses crypto/x509 for certificate operations, crypto/rsa and crypto/ecdsa for key generation, and crypto/rand for secure randomness
- age Encryption: Modern file encryption using Filippo Valsorda's age library for private key protection
Private keys are stored as .key.age files, protected by the age encryption format. All methods use ChaCha20-Poly1305 for the actual encryption, with different approaches for securing the file key:
Password protection:
- scrypt derives a key from your password
- This key encrypts the file key in the age header
- Password strength directly impacts security
SSH key protection:
- Your SSH private key decrypts the file key
- Optional additional password protection
- Works with Ed25519, RSA, and ECDSA keys
Hardware security:
- Hardware device required to decrypt the file key
- YubiKey PIV slots via age-plugin-yubikey
- Apple Secure Enclave (Touch ID) via age-plugin-se
- TPM support via age-plugin-tpm
- Etc.
Download the latest release for your platform from the releases page.
First, create the default config files:
ReactorCA automatically detects your SSH keys and configures encryption accordingly:
- SSH keys found: Uses SSH-based encryption (prefers Ed25519 over RSA)
- No SSH keys: Falls back to password-based encryption
This creates configuration files in the config/ directory. Edit them according to your needs.
After editing the configuration, create the CA:
This creates a self-signed CA certificate and private key (encrypted with the password you provide).
To issue a certificate for a host defined in your hosts.yaml:
To list all certificates with their expiration dates:
ReactorCA supports flexible certificate export and deployment:
Export only (automatic during certificate issuance):
Deploy only (run deployment commands independently):
Issue, export and deploy together:
Deploy will create temp files if the required files are not exported, so export and deploy options can be used independently from each other.
Deploy scripts run in Bash, except for Windows where they run in PowerShell.
- --root <path> - Root directory for config and store (env: REACTOR_CA_ROOT)
| ca ca create | Create a new CA key and self-signed certificate |
| ca ca renew | Renew the CA certificate using the existing key |
| ca ca rekey | Create a new key and certificate, replacing the old ones |
| ca ca info | Display detailed information about the CA certificate |
| ca ca info --openssl | Invoke openssl to display full text dump |
| ca ca import --cert <path> --key <path> | Import an existing CA certificate and private key |
| ca ca export-key | Export unencrypted CA private key to stdout |
| ca ca export-key -o file.key | Export CA private key to file |
| ca ca reencrypt | Change the master password/update recipients for all encrypted keys |
| ca host issue <host-id> | Issue/renew a certificate for a host |
| ca host issue --all | Issue/renew certificates for all hosts |
| ca host issue <host-id> --rekey | Force generation of a new private key |
| ca host issue <host-id> --deploy | Issue and deploy certificate in one step |
| ca host list | List all host certificates with their status |
| ca host list --expired | Show only expired certificates |
| ca host list --expiring-in 30 | Show certificates expiring in next 30 days |
| ca host list --json | Output in JSON format |
| ca host info <host-id> | Display detailed certificate information |
| ca host info <host-id> --openssl | Invoke openssl to display full text dump |
| ca host deploy <host-id> | Run deployment command for a host |
| ca host deploy --all | Deploy all host certificates |
| ca host export-key <host-id> | Export unencrypted private key to stdout |
| ca host export-key <host-id> -o file.key | Export private key to file |
| ca host import-key <host-id> --key <path> | Import existing private key |
| ca host sign-csr --csr <path> --out <path> | Sign external CSR |
| ca host clean | Remove certificates for hosts no longer in config |
| ca config validate | Validate configuration files |
All modifications to the store are recorded in store/ca.log.
If ReactorCA cannot be run, you can manually decrypt private keys using the age command:
The store structure is simple: certificates are in PEM format (.crt files) and private keys are age-encrypted (.key.age files). Your encryption method determines which identity file to use with age -d -i.
Host certificates inherit CA subject fields (organization, country, etc.) when not specified, except common_name which must be explicitly set for each host, if desired.
In that case, common_name must also be listed in alternative_names.dns. RFC 2818 (2000) deprecates the use of Common Name for host identities.
For detailed information about certificate extensions (Key Usage, Extended Key Usage, Name Constraints, custom OIDs, etc.), see the X.509 Certificate Extensions section.
ReactorCA supports fine-grained configuration of X.509 certificate extensions for both CA and host certificates. Extensions provide additional constraints, identifiers, and capabilities beyond the basic certificate fields.
ReactorCA provides sensible defaults for basic TLS use cases:
-
CA certificates (ca ca create, ca ca renew, ca ca rekey):
- Basic Constraints (critical, CA=true)
- Key Usage (critical, cert_sign + crl_sign)
-
Host certificates (ca host issue):
- Key Usage (digital_signature + key_encipherment)
- Extended Key Usage (server_auth + client_auth)
- Subject Alternative Names are populated from alternative_names configuration
When you specify extensions, ReactorCA applies defaults first, then merges your configuration at the field level.
Homelab CA with Name Constraints (recommended for security):
Web Server Certificate:
Code Signing Certificate (disable TLS capabilities):
Email Certificate (S/MIME only, no TLS):
See RFC 5280 for details.
Controls whether a certificate can act as a CA and limits certification path length.
Specifies the cryptographic operations for which the public key may be used.
Defines specific purposes for which the public key may be used, beyond basic cryptographic operations.
Provides a means of identifying certificates that contain a particular public key.
Identifies the public key corresponding to the private key used to sign a certificate.
Powerful security feature: Restricts the namespace within which all subject names in subsequent certificates must be located. Ideal for homelab CAs to prevent certificate misuse.
Specifies where Certificate Revocation Lists (CRLs) can be retrieved for checking certificate revocation status. Supports multiple distribution points with optional reason codes.
Supported CRL Reasons:
- unspecified - General revocation
- key_compromise - Private key compromised
- ca_compromise - CA key compromised
- affiliation_changed - Subject's affiliation changed
- superseded - Certificate replaced
- cessation_of_operation - Entity ceased operation
- certificate_hold - Temporary revocation
- privilege_withdrawn - Privileges revoked
- aa_compromise - Attribute authority compromised
Define custom extensions using Object Identifiers (OIDs) with flexible encoding options. Start the extension name with custom_.
ReactorCA supports multiple encoding formats for custom extension values:
Simple Encodings:
- base64: "data" - Base64-encoded binary data
- hex: "data" - Hexadecimal-encoded binary data
ASN.1 Types:
- asn1: {string: "text"} - UTF-8 string
- asn1: {int: 123} - Integer value
- asn1: {bool: true} - Boolean value
- asn1: {oid: "1.2.3.4"} - Object identifier
- asn1: {sequence: [{string: "foo"}, {int: 64}]} - SEQUENCE of values
- asn1: {octetstring: {string: "wrapped data"}} - OCTET STRING wrapper
- asn1: {bitstring: "10110000"} - BIT STRING from binary string
- asn1: {bitstring: [0, 2, 3]} - BIT STRING from bit positions
See browser support matrix for real-life test results.
Rule of thumb: use ECP; for cheap Chinese plastic appliances it might be necessary to fall back to RSA2048-SHA256.
| RSA2048 | 2048-bit | Medium | Good | Excellent |
| RSA3072 | 3072-bit | Slow | Strong | Excellent |
| RSA4096 | 4096-bit | Slow | Very Strong | Excellent |
| ECP256 | P-256 | Fast | Strong | Good |
| ECP384 | P-384 | Medium | Very Strong | Good |
| ECP521 | P-521 | Medium | Very Strong | Good |
| ED25519 | 256-bit | Very Fast | Strong | Limited |
- SHA256: Good default, excellent compatibility
- SHA384: Stronger, recommended for ECP384 and higher
- SHA512: Strongest, for high-security environments
ReactorCA supports multiple SAN types:
Note that alternative_name is special, for convenience it is an entity-level parameter for an extension, which usually live under extensions.
ReactorCA supports multiple encryption providers for private key protection:
Password sources are checked in order:
- Password File: Specified in ca.yaml under password.file
- Environment Variable: Set via REACTOR_CA_PASSWORD (or custom env var)
- Interactive Prompt: Secure terminal input (fallback)
Uses existing SSH infrastructure for key protection:
- Identity File: Your SSH private key (e.g., ~/.ssh/id_ed25519)
- Recipients: SSH public keys that can decrypt the private keys
- Supports: Ed25519, RSA, and ECDSA SSH keys
- No passwords required: Leverages SSH agent or unlocked SSH keys
Uses age plugins for hardware-backed key protection:
- Identity File: Plugin identity file (e.g., ~/.age/plugin-identity.txt)
- Recipients: Hardware token public keys (e.g., Secure Enclave, YubiKey)
- Supports: Any age-plugin-* binary (secure-enclave, yubikey, tpm, etc.)
- Hardware security: Private keys never leave the secure hardware
Currently, ReactorCA is primarily designed for a very basic setup: A single root CA directly signs all certificates without intermediaries. But you should be fine creating an intermediate CA manually and importing it into ReactorCA, then using it for your everyday operation.
If you are using agenix (or a similar system) for secret distribution, you can share secrets between ReactorCA and agenix, usually by employing the additional_recipients option. Note that password encryption does NOT mix with age-ssh or plugin modes for security reasons.
This project uses devenv.nix for reproducible development and Just as build helper:
PRs to the develop branch welcome!
| RSA2048-SHA256 | 🟢 PASS | 🟢 PASS | 🟢 PASS | 🟢 PASS | 🟢 PASS |
| RSA2048-SHA512 | 🟢 PASS | 🔴 FAIL | 🟢 PASS | 🔴 FAIL | 🟢 PASS |
| RSA3072-SHA256 | 🟢 PASS | 🔴 FAIL | 🔴 FAIL | 🔴 FAIL | 🟢 PASS |
| RSA3072-SHA512 | 🟢 PASS | 🔴 FAIL | 🔴 FAIL | 🔴 FAIL | 🟢 PASS |
| RSA4096-SHA256 | 🟢 PASS | 🔴 FAIL | 🔴 FAIL | 🔴 FAIL | 🟢 PASS |
| RSA4096-SHA512 | 🟢 PASS | 🔴 FAIL | 🔴 FAIL | 🔴 FAIL | 🟢 PASS |
| ECP256-SHA256 | 🟢 PASS | 🔴 FAIL | 🔴 FAIL | 🔴 FAIL | 🟢 PASS |
| ECP256-SHA512 | 🟢 PASS | 🔴 FAIL | 🔴 FAIL | 🔴 FAIL | 🟢 PASS |
| ECP384-SHA256 | 🟢 PASS | 🔴 FAIL | 🔴 FAIL | 🔴 FAIL | 🟢 PASS |
| ECP384-SHA512 | 🟢 PASS | 🔴 FAIL | 🔴 FAIL | 🔴 FAIL | 🟢 PASS |
| ECP521-SHA256 | 🟢 PASS | 🔴 FAIL | 🔴 FAIL | 🔴 FAIL | 🟢 PASS |
| ECP521-SHA512 | 🟢 PASS | 🔴 FAIL | 🔴 FAIL | 🔴 FAIL | 🟢 PASS |
| ED25519-SHA256 | 🔴 FAIL | 🔴 FAIL | 🔴 FAIL | 🔴 FAIL | 🟢 PASS |
| ED25519-SHA512 | 🔴 FAIL | 🔴 FAIL | 🔴 FAIL | 🔴 FAIL | 🟢 PASS |
-
ED25519 not recommended for general public in 2025.
-
Apparently, the browsers used by Microsoft's Playwright container in the Github Action have very limited crypto support.
-
Local tests won't run with Chrome, so far, and won't succeed with Safari on my machine. To run:
just test browser just update-browser-matrix
- No intermediate CA support
- No certificate revocation (CRL/OCSP) support
- No PKCS#12 bundle creation
- No automated renewal daemon (use cron/systemd timers)
- XCA: great, simple GUI-based CA
- certstrap: didn't know about it before starting ;)
- EasyRSA: a classic
- CFSSL: powerful client-server solution (there'a also a wrapper)
- step-ca: fully fledged server with ACME support
- Zytrax Survival Guide: comprehensive guide and explanation
- Tutorial by Jamie Nguyen: demonstrates how to act as your own certificate authority (CA) using the OpenSSL command-line tools
.png)


