Manifests and a runbook for the internal CA that issues 1-year S/MIME certificates. Per the agreed split: these are applied by hand, and the root-key ceremony in section 3 is deliberately NOT automated - the whole value of an offline root is that its private key never exists on a machine that runs services or tooling. Structural recommendation up front (section 0), because it decides whether promoting to vncmail later is a config change or a re-rooting: name the root for the ORGANISATION, not the environment. One root, generated once at prod grade, with per-environment intermediates under it. Promotion is then "issue a second intermediate from the same root" - a one-hour ceremony - and the trust anchor already distributed to laptops, phones and partners does not change. A throwaway "VNC Sandbox Root" instead means redistributing a new anchor to every device and every external party who ever verified a signature. That cost is invisible today and expensive later. Security shape of the deployment: - Own namespace (vnc-ca), NOT vncmail. The webmail pod is internet-facing; the CA signs certificates. A compromise of the former must not be a compromise of the latter. - Port 8080 (CRL + OCSP) is the ONLY thing the public ingress routes, and only two path prefixes. Not the admin web, not the REST API, not the public enrolment pages. - Port 8443 (admin + REST, client-cert authenticated) is never exposed through an ingress - cluster-internal or kubectl port-forward only, enforced by NetworkPolicy as defence in depth. - The RA credential the enrolment route uses gets its own EJBCA role limited to issue/revoke under one profile. It lives on an internet-facing pod, so its blast radius should be "mint an S/MIME cert" and not "reconfigure the CA". Two things the runbook makes you prove rather than assume: - The NetworkPolicy actually enforces. Applying one on a CNI that does not implement it succeeds silently and protects nothing, so section 6 has a probe that MUST time out - a 401 means the REST API is exposed cluster-wide. - The CA backup restores. ejbca-db-data holds the intermediate private key and, with key recovery on, escrowed user decryption keys; an untested CA backup is a belief. Section 7 surfaces a decision rather than making it silently. S/MIME is unlike TLS in that losing a private key makes every message ever encrypted to that user permanently unreadable - re-issuing does not help, the old mail was encrypted to the old key. So key escrow is on by default here, which is the defensible choice when mail is a business record, but it means the CA operator can decrypt user mail. That is worth deciding consciously and being able to explain, not discovering. MariaDB rather than the container's embedded H2 deliberately: H2 is not supported for data you intend to keep, and the database is the one component that must not need re-platforming on promotion. Image tag pinned. The env-var contract is the part most likely to have drifted between EJBCA releases, so the runbook says to verify it against the tag pulled rather than trusting these values, and gives the log grep that shows the failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
115 lines
3.9 KiB
YAML
115 lines
3.9 KiB
YAML
# EJBCA Community Edition.
|
|
#
|
|
# VERIFY THE ENV CONTRACT BEFORE YOU TRUST THIS FILE. EJBCA's container
|
|
# configuration has changed across releases, so pin a tag and check its
|
|
# documented variables rather than assuming these carry over:
|
|
# docker run --rm keyfactor/ejbca-ce:<tag> cat /opt/keyfactor/bin/start.sh | head -60
|
|
# The shape below (external MariaDB, two ports, healthcheck path) is stable; the
|
|
# individual variable names are the part most likely to drift.
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: ejbca
|
|
namespace: vnc-ca
|
|
spec:
|
|
type: ClusterIP
|
|
selector:
|
|
app: ejbca
|
|
ports:
|
|
# 8080 — plain HTTP, NO client-certificate authentication. Only the public
|
|
# web is served here: CRL distribution and the OCSP responder. This is the
|
|
# only port the public ingress touches.
|
|
- name: http
|
|
port: 8080
|
|
targetPort: 8080
|
|
# 8443 — HTTPS with mandatory client-certificate auth. Admin web AND the
|
|
# REST API. Never exposed through an ingress; reachable only from inside the
|
|
# cluster (the enrolment route) or via `kubectl port-forward` (you, doing
|
|
# administration). See networkpolicy.yaml.
|
|
- name: https
|
|
port: 8443
|
|
targetPort: 8443
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: ejbca
|
|
namespace: vnc-ca
|
|
spec:
|
|
replicas: 1
|
|
strategy:
|
|
type: Recreate
|
|
selector:
|
|
matchLabels:
|
|
app: ejbca
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: ejbca
|
|
spec:
|
|
# EJBCA needs the DB reachable before WildFly deploys its datasource.
|
|
initContainers:
|
|
- name: wait-for-db
|
|
image: mariadb:11.4
|
|
command:
|
|
- sh
|
|
- -c
|
|
- |
|
|
until mariadb-admin ping -h ejbca-db --silent; do
|
|
echo "waiting for ejbca-db..."; sleep 3
|
|
done
|
|
containers:
|
|
- name: ejbca
|
|
# Pin an explicit tag. `latest` on a CA is how you get an unplanned
|
|
# schema migration during an incident.
|
|
image: keyfactor/ejbca-ce:9.1.1
|
|
env:
|
|
- name: DATABASE_JDBC_URL
|
|
value: jdbc:mariadb://ejbca-db:3306/ejbca?characterEncoding=UTF-8
|
|
- name: DATABASE_USER
|
|
valueFrom:
|
|
secretKeyRef: { name: ejbca-db, key: MARIADB_USER }
|
|
- name: DATABASE_PASSWORD
|
|
valueFrom:
|
|
secretKeyRef: { name: ejbca-db, key: MARIADB_PASSWORD }
|
|
# Lets EJBCA generate its own server TLS keypair on first boot. The
|
|
# REST/admin listener is cluster-internal and authenticated by
|
|
# CLIENT certificate, so a self-signed server cert here is fine —
|
|
# our enrolment route pins the CA chain explicitly rather than
|
|
# trusting the public roots. Do not "fix" this with cert-manager
|
|
# without also updating that pin.
|
|
- name: TLS_SETUP_ENABLED
|
|
value: "simple"
|
|
- name: LOG_LEVEL_APP
|
|
value: INFO
|
|
ports:
|
|
- name: http
|
|
containerPort: 8080
|
|
- name: https
|
|
containerPort: 8443
|
|
# First boot builds the schema and can take minutes. A tight
|
|
# startupProbe budget here will CrashLoop a CA that is merely slow.
|
|
startupProbe:
|
|
httpGet:
|
|
path: /ejbca/publicweb/healthcheck/ejbcahealth
|
|
port: 8080
|
|
periodSeconds: 10
|
|
failureThreshold: 60
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /ejbca/publicweb/healthcheck/ejbcahealth
|
|
port: 8080
|
|
periodSeconds: 15
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /ejbca/publicweb/healthcheck/ejbcahealth
|
|
port: 8080
|
|
periodSeconds: 30
|
|
failureThreshold: 5
|
|
resources:
|
|
requests:
|
|
cpu: 500m
|
|
memory: 2Gi
|
|
limits:
|
|
memory: 4Gi
|