diff --git a/.env.example b/.env.example index 2c943f90..f232b8af 100644 --- a/.env.example +++ b/.env.example @@ -92,6 +92,10 @@ JWT_SECRET=change-me-in-production # BOR_CA_KEY_FILE=/etc/bor/pki/ca.key # Directory for the auto-generated CA (default: /var/lib/bor/pki/ca). # BOR_CA_AUTOGEN_DIR=/var/lib/bor/pki/ca +# Validity of the auto-generated CA certificate: years (10y), days (3650d) or +# a duration (87600h), between 2 and 30 years (default: 10y). Applied only +# when the CA is first generated, so set it before the first start. +# BOR_CA_LIFETIME=10y # ── PKCS#11 HSM for CA private key (optional; requires make server-pkcs11) ──── # Store the CA private key in a hardware security module rather than a file. diff --git a/doc/configuration/server.md b/doc/configuration/server.md index 40d39cef..4929518e 100644 --- a/doc/configuration/server.md +++ b/doc/configuration/server.md @@ -218,6 +218,7 @@ and the auto-generated server certificate. | `ca.cert_file` | `BOR_CA_CERT_FILE` | `""` | PEM CA certificate to use instead of the auto-generated CA. Must be set together with `key_file`. | | `ca.key_file` | `BOR_CA_KEY_FILE` | `""` | PEM private key of the CA (PKCS#8 or traditional ECDSA/RSA). | | `ca.autogen_dir` | `BOR_CA_AUTOGEN_DIR` | `/var/lib/bor/pki/ca` | Directory for the auto-generated CA (`ca.crt`, `ca.key`). With PKCS#11, the CA certificate is stored here as `ca.crt`. | +| `ca.lifetime` | `BOR_CA_LIFETIME` | `10y` | Validity of the auto-generated CA certificate: years (`10y`), days (`3650d`) or a duration (`87600h`), between 2 and 30 years. Applied only when the CA certificate is first generated, so set it before the first start; an existing CA is never re-issued. Ignored when `cert_file`/`key_file` point to your own CA. | | `ca.pkcs11.lib` | `BOR_CA_PKCS11_LIB` | `""` | Path to the PKCS#11 module (`.so`) of the HSM. | | `ca.pkcs11.token_label` | `BOR_CA_PKCS11_TOKEN_LABEL` | `""` | Label of the HSM token. | | `ca.pkcs11.key_label` | `BOR_CA_PKCS11_KEY_LABEL` | `""` | Label of the CA private key on the token. | diff --git a/doc/configuration/tls.md b/doc/configuration/tls.md index 3a33ee57..f998d083 100644 --- a/doc/configuration/tls.md +++ b/doc/configuration/tls.md @@ -33,7 +33,7 @@ On first start, if no CA is configured, the server creates one in | Files | `ca.crt` and `ca.key` (PKCS#8), both mode `0600` | | Key | ECDSA P-384 | | Subject | `CN=Bor Internal CA, O=Bor` | -| Validity | 10 years from creation | +| Validity | 10 years from creation by default; set `ca.lifetime` (`BOR_CA_LIFETIME`) before the first start to change it | | Constraints | CA, path length 0 (can sign only end-entity certificates) | The CA signs two kinds of certificates: @@ -418,7 +418,8 @@ BOR_CA_PKCS11_PIN=123456 Behavior at startup: - If no key with `key_label` exists on the token, the server generates an - ECDSA P-384 key on the token and a new 10-year CA certificate. + ECDSA P-384 key on the token and a new CA certificate valid for + `ca.lifetime` (10 years by default). - If the key exists but the certificate file is missing, a new CA certificate is created for that key. - Otherwise both are loaded. diff --git a/doc/operations/backup.md b/doc/operations/backup.md index c9b1773e..917a7807 100644 --- a/doc/operations/backup.md +++ b/doc/operations/backup.md @@ -46,7 +46,7 @@ Not needed in a backup: Unless you configured your own CA with `ca.cert_file` and `ca.key_file`, the server generates an internal CA on first start in -`/var/lib/bor/pki/ca` (ECDSA P-384, valid 10 years) and uses it to: +`/var/lib/bor/pki/ca` (ECDSA P-384, valid 10 years unless `ca.lifetime` was set before the first start) and uses it to: - sign every agent's client certificate during enrollment, and - sign the server certificate in `/var/lib/bor/pki/ui` that agents verify on diff --git a/doc/security/compliance.md b/doc/security/compliance.md index f707f706..648b0430 100644 --- a/doc/security/compliance.md +++ b/doc/security/compliance.md @@ -91,8 +91,8 @@ syslog) set a minimum of TLS 1.2 and otherwise use Go defaults. | Item | Algorithm / curve | Signature | Lifetime and rotation | Package | |---|---|---|---|---| -| Internal CA (auto-generated in `ca.autogen_dir`) | ECDSA P-384 | Self-signed, ECDSA with SHA-384 | 10 years. No automatic rotation; rotating the CA means re-enrolling every agent. | `pki` | -| Internal CA in an HSM (PKCS#11, optional build) | ECDSA P-384 generated on the token | ECDSA with SHA-384 | 10 years | `pki` (`pkcs11` build tag) | +| Internal CA (auto-generated in `ca.autogen_dir`) | ECDSA P-384 | Self-signed, ECDSA with SHA-384 | 10 years by default, configurable before the first start with `ca.lifetime`. No automatic rotation; rotating the CA means re-enrolling every agent. | `pki` | +| Internal CA in an HSM (PKCS#11, optional build) | ECDSA P-384 generated on the token | ECDSA with SHA-384 | 10 years by default (`ca.lifetime`) | `pki` (`pkcs11` build tag) | | Server certificate (`ui.crt` in `tls.autogen_dir`) | ECDSA P-256, `digitalSignature` key usage, `serverAuth` EKU | Signed by the CA (ECDSA with SHA-384) | 1 year. Regenerated at server start when it no longer verifies against the CA (including when expired) or when required host names are missing. | `pki` | | Agent client certificate | ECDSA P-256 key generated on the endpoint, `digitalSignature` key usage, `clientAuth` EKU | Signed by the CA (ECDSA with SHA-384) | 90 days. The agent requests renewal (new key pair, new CSR over mTLS) when it starts and the certificate expires within 30 days. | `pki`, `policyclient` | | Certificate serial numbers | 128-bit random from `crypto/rand` | - | - | `pki` | diff --git a/doc/security/hardening.md b/doc/security/hardening.md index 01022e3e..059567a9 100644 --- a/doc/security/hardening.md +++ b/doc/security/hardening.md @@ -47,7 +47,7 @@ Keys and certificates generated by Bor: | Item | Algorithm | Lifetime | |------|-----------|----------| -| Internal CA (`/var/lib/bor/pki/ca`) | ECDSA P-384 | 10 years | +| Internal CA (`/var/lib/bor/pki/ca`) | ECDSA P-384 | 10 years by default (`ca.lifetime`, fixed at creation) | | Server certificate (`/var/lib/bor/pki/ui`) | ECDSA P-256, signed by the CA | 1 year, regenerated at startup when expired or when host names change | | Agent key and certificate | ECDSA P-256 generated on the endpoint, signed by the CA | 90 days | diff --git a/podman-compose.yml.sample b/podman-compose.yml.sample index b6c905c6..93fde552 100644 --- a/podman-compose.yml.sample +++ b/podman-compose.yml.sample @@ -66,6 +66,7 @@ services: # - BOR_CA_CERT_FILE=/etc/bor/pki/ca/ca.crt # - BOR_CA_KEY_FILE=/etc/bor/pki/ca/ca.key # - BOR_CA_AUTOGEN_DIR=/var/lib/bor/pki/ca + # - BOR_CA_LIFETIME=10y # - BOR_TLS_CERT_FILE=/etc/bor/pki/ui/server.crt # - BOR_TLS_KEY_FILE=/etc/bor/pki/ui/server.key # - BOR_TLS_AUTOGEN_DIR=/var/lib/bor/pki/ui diff --git a/server/internal/config/config.go b/server/internal/config/config.go index 116bd128..ed2839ce 100644 --- a/server/internal/config/config.go +++ b/server/internal/config/config.go @@ -249,6 +249,10 @@ type CAConfig struct { KeyFile string // BOR_CA_KEY_FILE – path to CA private key (unused when PKCS11 is set) AutogenDir string // BOR_CA_AUTOGEN_DIR – dir for auto-generated CA PKCS11 PKCS11Config // optional: load CA key from PKCS#11 HSM instead of a file + // Lifetime is the validity of the auto-generated CA certificate + // (BOR_CA_LIFETIME, default 10y). It is applied only when the CA + // certificate is first generated; an existing CA is never re-issued. + Lifetime time.Duration } // LDAPConfig holds LDAP connection configuration. @@ -358,6 +362,7 @@ type fileConfig struct { CertFile string `yaml:"cert_file"` KeyFile string `yaml:"key_file"` AutogenDir string `yaml:"autogen_dir"` + Lifetime string `yaml:"lifetime"` PKCS11 struct { Lib string `yaml:"lib"` TokenLabel string `yaml:"token_label"` @@ -541,6 +546,11 @@ func Load() (*Config, error) { return nil, fmt.Errorf("both BOR_CA_CERT_FILE and BOR_CA_KEY_FILE must be set, or neither") } + caLifetime, err := parseCALifetime(getEnv("BOR_CA_LIFETIME", fc.CA.Lifetime)) + if err != nil { + return nil, fmt.Errorf("invalid BOR_CA_LIFETIME: %w", err) + } + // ─── CA PKCS#11 (optional HSM) ───────────────────────────────────────── pkcs11Lib := getEnv("BOR_CA_PKCS11_LIB", fc.CA.PKCS11.Lib) pkcs11TokenLabel := getEnv("BOR_CA_PKCS11_TOKEN_LABEL", fc.CA.PKCS11.TokenLabel) @@ -689,6 +699,7 @@ func Load() (*Config, error) { CertFile: caCertFile, KeyFile: caKeyFile, AutogenDir: getEnv("BOR_CA_AUTOGEN_DIR", fc.CA.AutogenDir), + Lifetime: caLifetime, PKCS11: PKCS11Config{ Lib: pkcs11Lib, TokenLabel: pkcs11TokenLabel, @@ -799,6 +810,7 @@ func defaultFileConfig() fileConfig { fc.Security.RefreshLifetime = "24h" fc.TLS.AutogenDir = "/var/lib/bor/pki/ui" fc.CA.AutogenDir = "/var/lib/bor/pki/ca" + fc.CA.Lifetime = "10y" fc.LDAP.Host = "localhost" fc.LDAP.Port = 389 fc.LDAP.UserFilter = "(uid=%s)" @@ -855,6 +867,47 @@ func getEnv(key, defaultValue string) string { return defaultValue } +// Bounds for the auto-generated CA lifetime. The lower bound keeps the CA +// valid for longer than the server certificate it signs (1 year); the upper +// bound rejects lifetimes that no key of this strength should be trusted for. +const ( + minCALifetime = 2 * 365 * 24 * time.Hour + maxCALifetime = 30 * 365 * 24 * time.Hour +) + +// parseCALifetime parses a CA lifetime. Besides Go durations ("87600h") it +// accepts a whole number of years ("10y", 365 days each) or days ("3650d"). +func parseCALifetime(s string) (time.Duration, error) { + s = strings.TrimSpace(s) + var d time.Duration + switch { + case strings.HasSuffix(s, "y"), strings.HasSuffix(s, "d"): + n, err := strconv.Atoi(s[:len(s)-1]) + if err != nil { + return 0, fmt.Errorf("%q is not a whole number of years or days", s) + } + // Guard the multiplication below against overflow. + if n < 0 || n > 100*365 { + return 0, fmt.Errorf("%q is out of range", s) + } + unit := 24 * time.Hour + if strings.HasSuffix(s, "y") { + unit *= 365 + } + d = time.Duration(n) * unit + default: + var err error + d, err = time.ParseDuration(s) + if err != nil { + return 0, fmt.Errorf("%q: use years (10y), days (3650d) or a duration (87600h)", s) + } + } + if d < minCALifetime || d > maxCALifetime { + return 0, fmt.Errorf("%q is outside the allowed range of 2 to 30 years", s) + } + return d, nil +} + func getEnvBool(key string, defaultValue bool) bool { val := os.Getenv(key) switch val { diff --git a/server/internal/config/config_test.go b/server/internal/config/config_test.go index f98dc13d..a8060204 100644 --- a/server/internal/config/config_test.go +++ b/server/internal/config/config_test.go @@ -7,6 +7,7 @@ package config import ( "os" "testing" + "time" ) func TestLoad_Defaults(t *testing.T) { @@ -39,6 +40,51 @@ func TestLoad_Defaults(t *testing.T) { if cfg.CA.AutogenDir != "/var/lib/bor/pki/ca" { t.Errorf("CA.AutogenDir = %q, want %q", cfg.CA.AutogenDir, "/var/lib/bor/pki/ca") } + if want := 10 * 365 * 24 * time.Hour; cfg.CA.Lifetime != want { + t.Errorf("CA.Lifetime = %v, want %v", cfg.CA.Lifetime, want) + } +} + +func TestLoad_CALifetime(t *testing.T) { + const day = 24 * time.Hour + tests := []struct { + name string + value string + want time.Duration + wantErr bool + }{ + {name: "years", value: "5y", want: 5 * 365 * day}, + {name: "days", value: "1000d", want: 1000 * day}, + {name: "go duration", value: "43800h", want: 43800 * time.Hour}, + {name: "surrounding space", value: " 3y ", want: 3 * 365 * day}, + {name: "lower bound", value: "2y", want: 2 * 365 * day}, + {name: "upper bound", value: "30y", want: 30 * 365 * day}, + {name: "too short", value: "1y", wantErr: true}, + {name: "too long", value: "31y", wantErr: true}, + {name: "no unit", value: "10", wantErr: true}, + {name: "fractional years", value: "2.5y", wantErr: true}, + {name: "negative", value: "-5y", wantErr: true}, + {name: "overflow", value: "999999999999y", wantErr: true}, + {name: "garbage", value: "forever", wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("BOR_CA_LIFETIME", tt.value) + cfg, err := Load() + if tt.wantErr { + if err == nil { + t.Fatalf("Load() with BOR_CA_LIFETIME=%q succeeded, want error", tt.value) + } + return + } + if err != nil { + t.Fatalf("Load() error = %v", err) + } + if cfg.CA.Lifetime != tt.want { + t.Errorf("CA.Lifetime = %v, want %v", cfg.CA.Lifetime, tt.want) + } + }) + } } func TestLoad_FailFast_TLSCertWithoutKey(t *testing.T) { diff --git a/server/internal/pki/pkcs11.go b/server/internal/pki/pkcs11.go index 49cb5fac..be5fe2ca 100644 --- a/server/internal/pki/pkcs11.go +++ b/server/internal/pki/pkcs11.go @@ -50,14 +50,18 @@ import ( // - If a key with keyLabel already exists on the token and certPath exists, // both are loaded and returned. // - If the key exists but certPath is missing, a new self-signed CA -// certificate is generated (ECDSA P-384, 10 years) and written to certPath. +// certificate is generated (ECDSA P-384, valid for lifetime) and written +// to certPath. A non-positive lifetime means DefaultCALifetime. // - If no key with keyLabel is found on the token, a new ECDSA P-384 key is // generated on the HSM, then a CA certificate is created and written to certPath. // // The returned crypto.Signer wraps the HSM key; signing operations happen // inside the HSM. The PKCS#11 context is intentionally kept open for the // process lifetime so the signer remains valid. -func EnsureCAWithHSM(certPath, lib, tokenLabel, keyLabel, pin string) (*x509.Certificate, crypto.Signer, error) { +func EnsureCAWithHSM(certPath, lib, tokenLabel, keyLabel, pin string, lifetime time.Duration) (*x509.Certificate, crypto.Signer, error) { + if lifetime <= 0 { + lifetime = DefaultCALifetime + } ctx, err := crypto11.Configure(&crypto11.Config{ Path: lib, TokenLabel: tokenLabel, @@ -88,7 +92,7 @@ func EnsureCAWithHSM(certPath, lib, tokenLabel, keyLabel, pin string) (*x509.Cer return nil, nil, fmt.Errorf("failed to create CA cert directory: %w", mkErr) } log.Printf("pki: generating CA certificate for HSM key %q -> %s", keyLabel, certPath) - if genErr := generateCACert(certPath, signer); genErr != nil { + if genErr := generateCACert(certPath, signer, lifetime); genErr != nil { return nil, nil, genErr } } @@ -101,9 +105,9 @@ func EnsureCAWithHSM(certPath, lib, tokenLabel, keyLabel, pin string) (*x509.Cer return caCert, signer, nil } -// generateCACert creates a self-signed CA certificate using the given signer -// and writes it to certPath (PEM, 0o644). -func generateCACert(certPath string, signer crypto.Signer) error { +// generateCACert creates a self-signed CA certificate valid for lifetime using +// the given signer and writes it to certPath (PEM, 0o644). +func generateCACert(certPath string, signer crypto.Signer, lifetime time.Duration) error { serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) if err != nil { return fmt.Errorf("failed to generate CA serial number: %w", err) @@ -116,7 +120,7 @@ func generateCACert(certPath string, signer crypto.Signer) error { CommonName: "Bor Internal CA", }, NotBefore: time.Now().Add(-1 * time.Minute), - NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour), + NotAfter: time.Now().Add(lifetime), KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, BasicConstraintsValid: true, IsCA: true, diff --git a/server/internal/pki/pkcs11_stub.go b/server/internal/pki/pkcs11_stub.go index 23343413..2224fae0 100644 --- a/server/internal/pki/pkcs11_stub.go +++ b/server/internal/pki/pkcs11_stub.go @@ -15,6 +15,7 @@ import ( "crypto" "crypto/x509" "errors" + "time" ) // ErrPKCS11NotCompiled is returned by EnsureCAWithHSM in builds without the @@ -26,6 +27,6 @@ var ErrPKCS11NotCompiled = errors.New( // EnsureCAWithHSM is not available in this build. // Rebuild with -tags pkcs11 to enable PKCS#11 HSM support. -func EnsureCAWithHSM(_, _, _, _, _ string) (*x509.Certificate, crypto.Signer, error) { +func EnsureCAWithHSM(_, _, _, _, _ string, _ time.Duration) (*x509.Certificate, crypto.Signer, error) { return nil, nil, ErrPKCS11NotCompiled } diff --git a/server/internal/pki/pki.go b/server/internal/pki/pki.go index 45b83fcf..5b344dae 100644 --- a/server/internal/pki/pki.go +++ b/server/internal/pki/pki.go @@ -136,15 +136,28 @@ func EnsureServerCert(dir string, caCert *x509.Certificate, caKey crypto.Signer, return certPath, keyPath, nil } -// EnsureCA checks for an existing CA cert/key at dir/ca.crt and dir/ca.key. -// If they do not exist it generates a new CA (ECDSA P-384, 10 years). +// DefaultCALifetime is the validity of a newly generated internal CA +// certificate when no lifetime is configured. +const DefaultCALifetime = 10 * 365 * 24 * time.Hour + +// EnsureCA is EnsureCAWithLifetime with DefaultCALifetime. +func EnsureCA(dir string) (certPath, keyPath string, err error) { + return EnsureCAWithLifetime(dir, DefaultCALifetime) +} + +// EnsureCAWithLifetime checks for an existing CA cert/key at dir/ca.crt and +// dir/ca.key. If they do not exist it generates a new CA (ECDSA P-384) valid +// for lifetime; a non-positive lifetime means DefaultCALifetime. The lifetime +// only applies at generation time: an existing CA is returned unchanged. // -// ECDSA P-384 provides 192-bit security — appropriate for a CA with a -// 10-year lifetime. Satisfies FIPS 140-3, BSI TR-02102-1, ANSSI RGS, -// and ETSI TS 119 312. +// ECDSA P-384 provides 192-bit security - appropriate for a long-lived CA. +// Satisfies FIPS 140-3, BSI TR-02102-1, ANSSI RGS, and ETSI TS 119 312. // // Returns the paths to the CA cert and key files. -func EnsureCA(dir string) (certPath, keyPath string, err error) { +func EnsureCAWithLifetime(dir string, lifetime time.Duration) (certPath, keyPath string, err error) { + if lifetime <= 0 { + lifetime = DefaultCALifetime + } certPath = filepath.Join(dir, "ca.crt") keyPath = filepath.Join(dir, "ca.key") @@ -174,7 +187,7 @@ func EnsureCA(dir string) (certPath, keyPath string, err error) { CommonName: "Bor Internal CA", }, NotBefore: time.Now().Add(-1 * time.Minute), - NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour), + NotAfter: time.Now().Add(lifetime), KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, BasicConstraintsValid: true, IsCA: true, diff --git a/server/internal/pki/pki_test.go b/server/internal/pki/pki_test.go index c8e52208..70dcac10 100644 --- a/server/internal/pki/pki_test.go +++ b/server/internal/pki/pki_test.go @@ -16,6 +16,7 @@ import ( "os" "path/filepath" "testing" + "time" ) func TestEnsureServerCert_SelfSigned(t *testing.T) { @@ -196,6 +197,47 @@ func TestEnsureServerCert_RegeneratesWhenCAChanges(t *testing.T) { } } +func TestEnsureCAWithLifetime(t *testing.T) { + const day = 24 * time.Hour + tests := []struct { + name string + lifetime time.Duration + want time.Duration + }{ + {name: "custom", lifetime: 3 * 365 * day, want: 3 * 365 * day}, + {name: "zero falls back to default", lifetime: 0, want: DefaultCALifetime}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + certPath, keyPath, err := EnsureCAWithLifetime(dir, tt.lifetime) + if err != nil { + t.Fatalf("EnsureCAWithLifetime() error = %v", err) + } + caCert, _, err := LoadCA(certPath, keyPath) + if err != nil { + t.Fatalf("LoadCA() error = %v", err) + } + got := time.Until(caCert.NotAfter) + if got > tt.want || got < tt.want-time.Minute { + t.Errorf("CA expires in %v, want about %v", got, tt.want) + } + + // An existing CA keeps its lifetime. + if _, _, err = EnsureCAWithLifetime(dir, 20*365*day); err != nil { + t.Fatalf("EnsureCAWithLifetime() second call error = %v", err) + } + again, _, err := LoadCA(certPath, keyPath) + if err != nil { + t.Fatalf("LoadCA() error = %v", err) + } + if !again.NotAfter.Equal(caCert.NotAfter) { + t.Errorf("existing CA was re-issued: NotAfter %v -> %v", caCert.NotAfter, again.NotAfter) + } + }) + } +} + func TestEnsureCA(t *testing.T) { dir := t.TempDir() diff --git a/server/pkg/app/app.go b/server/pkg/app/app.go index 7c743abc..cbef89e5 100644 --- a/server/pkg/app/app.go +++ b/server/pkg/app/app.go @@ -120,6 +120,7 @@ func Run(opts Options) { caCertFile, cfg.CA.PKCS11.Lib, cfg.CA.PKCS11.TokenLabel, cfg.CA.PKCS11.KeyLabel, cfg.CA.PKCS11.PIN, + cfg.CA.Lifetime, ) if err != nil { log.Fatalf("Failed to initialise CA with PKCS#11 HSM: %v", err) @@ -131,7 +132,7 @@ func Run(opts Options) { caKeyFile := cfg.CA.KeyFile if caCertFile == "" && caKeyFile == "" { log.Println("BOR_CA_CERT_FILE/KEY not set – generating internal CA") - caCertFile, caKeyFile, err = pki.EnsureCA(cfg.CA.AutogenDir) + caCertFile, caKeyFile, err = pki.EnsureCAWithLifetime(cfg.CA.AutogenDir, cfg.CA.Lifetime) if err != nil { log.Fatalf("Failed to generate internal CA: %v", err) } diff --git a/server/server.yaml.example b/server/server.yaml.example index 7baa0633..725e2e96 100644 --- a/server/server.yaml.example +++ b/server/server.yaml.example @@ -93,6 +93,12 @@ ca: key_file: "" autogen_dir: "/var/lib/bor/pki/ca" + # Validity of the auto-generated CA certificate: years ("10y"), days + # ("3650d") or a duration ("87600h"), between 2 and 30 years. + # Applied only when the CA is first generated - set it before the first + # start. Changing it later does not re-issue an existing CA. + lifetime: "10y" + # Optional: store the CA private key in a PKCS#11 HSM instead of a file. # Requires the server binary to be built with: make server-pkcs11 # Set the PIN via the BOR_CA_PKCS11_PIN environment variable — do not