mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 16:31:33 +00:00
f1fa311191
Bundle 3 closure (2026-05-12 acquisition diligence audit). Closes the
"chart claims production-ready but lying-fields silently break it"
hazard cluster: README install command had wrong key, required secrets
weren't fail-fast, external Postgres rendered the bundled StatefulSet
hostname, container-only security hardening fields landed at pod scope
(silently dropped by K8s API), and three advertised template surfaces
(ServiceMonitor, PodDisruptionBudget, NetworkPolicy) didn't render at
all even when their values.yaml toggles were on.
Source findings closed:
C2 C3 D1 D2 D3 D5 D7 D11 D12 (repo audit)
OPS-L1 OPS-L2 (cowork audit)
Source findings explicitly deferred (tracked in WORKSPACE-ROADMAP.md):
D6 OPS-H1 (backup automation — operator must choose target storage)
D10 (digest pinning of latest `:latest` tags)
OPS-M1 (prometheus/client_golang migration)
OPS-M2 (distributed tracing instrumentation)
Chart truth table (rendered with helm 3.16.3):
-f values.yaml + tls.existingSecret + auth.apiKey + pg.auth.password
→ 12 resources (default mode, no monitoring/PDB/networkpolicy)
+ postgresql.enabled=false + externalDatabase.url=…
→ NO StatefulSet, NO postgres-secret, NO postgres-service (D2)
+ server.tls.certManager.enabled=true
→ +1 Certificate (cert-manager mode)
+ replicas=3 + monitoring.enabled=true + serviceMonitor.enabled=true
+ podDisruptionBudget.enabled=true + networkPolicy.enabled=true
→ +1 ServiceMonitor + 1 PodDisruptionBudget + 1 NetworkPolicy (D5+D11)
tls.existingSecret AND tls.certManager.enabled both set
→ REFUSED with "EXACTLY ONE TLS ownership path" error (D7)
Missing required secrets (apiKey / pg password / external URL)
→ REFUSED at template time with operator-actionable guidance (D1)
Closures by source ID:
C2 — README Helm install example fixed. Was `--set postgresql.password=…`
(does not exist); now `--set postgresql.auth.password=…` matching
the chart key. README install block also wires TLS, mentions
fail-fast at template time, and links the external-Postgres example.
C3 — Kubernetes Secrets connector annotated PREVIEW in values.yaml.
The chart still exposes `kubernetesSecrets.enabled` for the RBAC
preview wiring, but the values block now states clearly that the
production K8s client at internal/connector/target/k8ssecret/
k8ssecret.go::realK8sClient is a stub (verified — go.mod imports
zero k8s.io/client-go packages). Production landing tracked in
WORKSPACE-ROADMAP.md.
D1 — `certctl.requiredSecrets` template helper. Fail-fasts at render
time when (a) server.auth.type=api-key + apiKey empty, (b)
postgresql.enabled=true + pg.auth.password empty, (c)
postgresql.enabled=false + externalDatabase.url + legacy env
CERTCTL_DATABASE_URL all empty. Each branch emits an
operator-actionable diagnostic with the openssl rand command or
values override needed. postgres-secret template additionally
uses Helm's `required` builtin so it can't render with the empty
fallback that pre-Bundle-3 produced ("changeme" literal).
D2 — externalDatabase.url first-class. New top-level values block.
certctl.databaseURL helper now branches on postgresql.enabled:
bundled path uses the helper-emitted in-cluster URL; external
path uses externalDatabase.url verbatim. postgres-secret,
postgres-statefulset, and postgres-service ALL gate on
postgresql.enabled — external mode renders ZERO postgres-*
resources. POSTGRES_PASSWORD env in server-deployment also gates.
D3 — Container-vs-pod security context split. K8s API silently drops
readOnlyRootFilesystem / allowPrivilegeEscalation / capabilities /
privileged when they land at pod scope (`spec.securityContext`);
they only work at container scope (`spec.containers[].securityContext`).
Pre-Bundle-3 all fields sat at pod scope so the chart's documented
"read-only rootfs + drop-all caps" hardening was effectively
unenforced. New certctl.podSecurityContext + containerSecurityContext
helpers split the operator-facing securityContext map by field-name
whitelist so existing values keep working byte-for-byte while
fields render at the K8s-valid scope. Applied to both
server-deployment.yaml and agent-daemonset.yaml (DaemonSet + Deployment
branches).
D5 — Prometheus ServiceMonitor template. New
templates/servicemonitor.yaml. Renders when monitoring.enabled AND
monitoring.serviceMonitor.enabled. Scrapes /api/v1/metrics/prometheus
(rbac-gated on metrics.read — needs bearerTokenSecret with an API
key holding that perm). values.yaml block extended with bearerTokenSecret,
tlsConfig, and relabelings knobs and the operator-facing comment
documenting the auth requirement.
D7 — TLS both-set rejection. certctl.tls.required helper extended.
Pre-Bundle-3 only the NEITHER-set case was caught; setting BOTH
rendered a dangling cert-manager Certificate alongside an
existing-Secret mount, two conflicting TLS sources of truth.
Now refuses with "EXACTLY ONE TLS ownership path" + remediation
steps for both possible operator intents.
D11 — PodDisruptionBudget + NetworkPolicy templates. New
templates/pdb.yaml (renders when podDisruptionBudget.enabled +
server.replicas > 1) + templates/networkpolicy.yaml (renders when
networkPolicy.enabled). PDB uses minAvailable / maxUnavailable
exclusivity per K8s spec. NetworkPolicy default-allows in-namespace
agent → server traffic, kube-DNS egress, and bundled-postgres
egress (when postgresql.enabled), with operator-extensible
extraIngress / extraEgress for CA / OIDC / SMTP egress. Both
default off so existing deploys don't lose network reach
unannounced.
D12 — Database max-conn config wired. Pre-Bundle-3
internal/repository/postgres/db.go::NewDB hard-coded
SetMaxOpenConns(25). config.go loaded CERTCTL_DATABASE_MAX_CONNS,
Validate() enforced the >= 1 floor, values.yaml documented it,
and docs/reference/configuration.md surfaced it — but the pool
ignored every operator setting. New NewDBWithMaxConns threads
the operator value into the pool with maxIdle = maxOpen / 5
(≥ 1) so the historical ratio carries forward. cmd/server/main.go
calls the new constructor; NewDB stays for compat at the default 25.
OPS-L1 — Chart version 0.1.0 → 1.0.0. Chart has shipped through 8 audit
closures since 2026-02 (M-018, U-1, U-2, U-3, H-1, G-1, B1, B2);
pre-1.0 version was implying instability the chart no longer has.
OPS-L2 — External-Postgres path is now properly documented in values.yaml
(externalDatabase block with mode-2 example), README install command
links the existing examples/values-external-db.yaml, and the chart
truth table above proves the external mode renders cleanly.
Receipts:
helm lint deploy/helm/certctl/ # clean
helm template c deploy/helm/certctl/ \
--set server.tls.existingSecret=ci \
--set postgresql.auth.password=p \
--set server.auth.apiKey=k # 12 kinds, default
helm template c deploy/helm/certctl/ \
--set server.tls.existingSecret=ci \
--set postgresql.enabled=false \
--set externalDatabase.url='postgres://u:p@h:5432/db?sslmode=require' \
--set server.auth.apiKey=k # 9 kinds, no postgres-*
helm template c deploy/helm/certctl/ \
--set server.tls.certManager.enabled=true \
--set server.tls.certManager.issuerRef.name=letsencrypt \
--set postgresql.auth.password=p --set server.auth.apiKey=k
# +1 Certificate (cert-manager)
helm template c deploy/helm/certctl/ \
--set server.tls.existingSecret=ci \
--set postgresql.auth.password=p --set server.auth.apiKey=k \
--set server.replicas=3 \
--set monitoring.enabled=true \
--set monitoring.serviceMonitor.enabled=true \
--set podDisruptionBudget.enabled=true \
--set networkPolicy.enabled=true # +ServiceMonitor +PDB +NetworkPolicy
(TLS both-set + missing apiKey + missing pg password + missing extDb URL all REFUSED.)
gofmt -l # clean
go vet ./internal/repository/postgres ./cmd/server # clean
go build ./cmd/server # clean
bash scripts/ci-guards/B3-helm-chart-coherence.sh # clean
Remaining operator warnings (deferred, tracked in WORKSPACE-ROADMAP.md):
- Backup CronJob + restore script (D6 + OPS-H1): operator chooses
target (S3, GCS, Azure Blob, NFS). Sample CronJob yaml may ship
in deploy/helm/examples/ once an operator workstation has run
one full backup-restore cycle.
- Distributed tracing (OPS-M2): otel/* are go.mod indirect deps,
not actively instrumented. Adding spans is a v3 work item.
- Prometheus client_golang migration (OPS-M1): the hand-rolled
/metrics/prometheus exposition format works today; client_golang
migration unlocks histograms + exemplars + native label sets.
Audit-Closes: BUNDLE-3 C2 C3 D1 D2 D3 D5 D7 D11 D12 OPS-L1 OPS-L2
Audit-Defers: D6 D10 OPS-H1 OPS-M1 OPS-M2
666 lines
22 KiB
YAML
666 lines
22 KiB
YAML
# Default values for certctl Helm chart
|
|
# This is a YAML-formatted file.
|
|
# Declare variables to be passed into your templates.
|
|
|
|
# Namespace override (optional)
|
|
namespace: ""
|
|
|
|
# Global configuration
|
|
commonLabels: {}
|
|
imagePullSecrets: []
|
|
nameOverride: ""
|
|
fullnameOverride: ""
|
|
|
|
# ==============================================================================
|
|
# Certctl Server Configuration
|
|
# ==============================================================================
|
|
server:
|
|
# Number of replicas (for HA deployments)
|
|
replicas: 1
|
|
|
|
# Image configuration
|
|
image:
|
|
repository: ghcr.io/certctl-io/certctl
|
|
tag: "" # defaults to Chart.appVersion
|
|
pullPolicy: IfNotPresent
|
|
|
|
# Server port
|
|
port: 8443
|
|
|
|
# Resource requests and limits
|
|
resources:
|
|
requests:
|
|
cpu: 100m
|
|
memory: 128Mi
|
|
limits:
|
|
cpu: 500m
|
|
memory: 512Mi
|
|
|
|
# Pod security context
|
|
securityContext:
|
|
runAsNonRoot: true
|
|
runAsUser: 1000
|
|
runAsGroup: 1000
|
|
fsGroup: 1000
|
|
readOnlyRootFilesystem: true
|
|
allowPrivilegeEscalation: false
|
|
capabilities:
|
|
drop:
|
|
- ALL
|
|
|
|
# Liveness and readiness probes (HTTPS-only as of v2.2).
|
|
#
|
|
# The two paths exposed for probes are `/health` and `/ready` —
|
|
# registered in internal/api/router/router.go:76-85 and bypassing the
|
|
# auth middleware via the no-auth list at cmd/server/main.go:920.
|
|
# Both serve the same JSON shape today (`{"status":"healthy"}` /
|
|
# `{"status":"ready"}`) but exist as separate routes so liveness and
|
|
# readiness can diverge in the future without renaming.
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /health
|
|
port: https
|
|
scheme: HTTPS
|
|
initialDelaySeconds: 10
|
|
periodSeconds: 10
|
|
timeoutSeconds: 5
|
|
failureThreshold: 3
|
|
|
|
# U-2 (P1, cat-u-healthcheck_protocol_mismatch — adjacent fix): pre-U-2
|
|
# the readiness probe pointed at `/readyz`, the conventional kube-flavor
|
|
# name. The certctl server doesn't register `/readyz` (only `/health`
|
|
# and `/ready`) — see cmd/server/main.go:920 and
|
|
# internal/api/router/router.go:81. K8s readiness probes therefore
|
|
# received a 404 (or, with auth enabled, a 401 from the api-key middleware
|
|
# because `/readyz` was NOT in the no-auth bypass set), pods stayed
|
|
# `NotReady` indefinitely, and Helm rollouts stalled. Post-U-2 the path
|
|
# matches a registered route.
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /ready
|
|
port: https
|
|
scheme: HTTPS
|
|
initialDelaySeconds: 5
|
|
periodSeconds: 5
|
|
timeoutSeconds: 3
|
|
failureThreshold: 2
|
|
|
|
# TLS configuration — REQUIRED. HTTPS is the only supported mode (v2.2+).
|
|
# Operator must configure EXACTLY ONE of:
|
|
# (a) server.tls.existingSecret: <name> # pre-existing kubernetes.io/tls Secret
|
|
# (b) server.tls.certManager.enabled: true # provision a cert-manager Certificate CR
|
|
# Refusing to set either makes `helm template` fail with a diagnostic pointing at docs/tls.md.
|
|
tls:
|
|
# Name of a pre-existing Secret (type kubernetes.io/tls) holding tls.crt + tls.key (+ optional ca.crt).
|
|
# Leave empty to fall through to the cert-manager path.
|
|
existingSecret: ""
|
|
|
|
# Mount path for the TLS Secret inside the server + agent containers.
|
|
mountPath: /etc/certctl/tls
|
|
|
|
# cert-manager auto-provisioning. Opt-in (off by default per milestone §3.4).
|
|
certManager:
|
|
enabled: false
|
|
|
|
# Secret name the cert-manager Certificate CR writes into. Agents and the server
|
|
# both read from this Secret. If empty, defaults to "<fullname>-tls".
|
|
secretName: ""
|
|
|
|
# Cert-manager issuer reference.
|
|
issuerRef:
|
|
name: "" # e.g. "letsencrypt-prod" or "internal-ca"
|
|
kind: ClusterIssuer # ClusterIssuer or Issuer
|
|
group: cert-manager.io
|
|
|
|
# Subject fields on the issued cert.
|
|
commonName: "certctl-server"
|
|
dnsNames:
|
|
- certctl-server
|
|
- localhost
|
|
|
|
# Certificate lifetime + renewal window.
|
|
duration: 2160h # 90 days
|
|
renewBefore: 360h # 15 days
|
|
|
|
# Service type (ClusterIP, LoadBalancer, NodePort)
|
|
service:
|
|
type: ClusterIP
|
|
port: 8443
|
|
annotations: {}
|
|
|
|
# Authentication configuration.
|
|
# Valid types: "api-key" (production) or "none" (demo only — disables
|
|
# authentication on the API and logs a loud Warn at server startup).
|
|
# For JWT/OIDC, run an authenticating gateway in front of certctl
|
|
# (oauth2-proxy / Envoy ext_authz / Traefik ForwardAuth / Pomerium)
|
|
# and set type=none here so the gateway terminates federated identity.
|
|
# See docs/architecture.md "Authenticating-gateway pattern".
|
|
#
|
|
# G-1 (P1): pre-G-1 the chart accepted server.auth.type=jwt and the
|
|
# certctl-server container silently routed every request through the
|
|
# api-key bearer middleware — silent auth downgrade. Post-G-1 the
|
|
# chart's `certctl.validateAuthType` template helper rejects any value
|
|
# outside {api-key, none} at template time. See
|
|
# docs/upgrade-to-v2-jwt-removal.md if you previously set type=jwt.
|
|
auth:
|
|
type: api-key
|
|
apiKey: "" # REQUIRED when type=api-key (set via --set or values override).
|
|
|
|
# Logging configuration
|
|
logging:
|
|
level: info # debug, info, warn, error
|
|
format: json # json or text
|
|
|
|
# SMTP configuration for email notifications (optional)
|
|
smtp:
|
|
enabled: false
|
|
host: ""
|
|
port: 587
|
|
username: ""
|
|
password: ""
|
|
fromAddress: ""
|
|
useTLS: true
|
|
|
|
# Certificate digest digest (periodic email summary)
|
|
digest:
|
|
enabled: false
|
|
interval: "24h"
|
|
recipients: []
|
|
# Example:
|
|
# - admin@example.com
|
|
# - ops@example.com
|
|
|
|
# Enrollment over Secure Transport (EST) configuration
|
|
est:
|
|
enabled: false
|
|
issuerID: "iss-local"
|
|
profileID: ""
|
|
|
|
# Rate limiting configuration
|
|
rateLimiting:
|
|
rps: 100 # Requests per second
|
|
burst: 200 # Burst capacity
|
|
|
|
# Network scanning configuration
|
|
networkScan:
|
|
enabled: false
|
|
interval: "6h"
|
|
|
|
# Certificate key generation mode
|
|
keygen:
|
|
mode: agent # Options: agent (production), server (demo with warning)
|
|
|
|
# CORS configuration
|
|
cors:
|
|
origins: "" # Comma-separated list, empty means deny all cross-origin requests
|
|
|
|
# Issuer connectors configuration
|
|
issuer:
|
|
local:
|
|
enabled: true
|
|
# For sub-CA mode, provide these paths:
|
|
# caCertPath: /path/to/ca.crt
|
|
# caKeyPath: /path/to/ca.key
|
|
|
|
acme:
|
|
enabled: false
|
|
directoryURL: ""
|
|
email: ""
|
|
challengeType: "http-01" # Options: http-01, dns-01, dns-persist-01
|
|
# DNS configuration (for dns-01 or dns-persist-01)
|
|
# dnsPresentScript: /path/to/dns-present.sh
|
|
# dnsCleanupScript: /path/to/dns-cleanup.sh
|
|
# dnsPropagationWait: "30s"
|
|
# dnsPersistIssuerDomain: "validation.example.com"
|
|
# EAB configuration (for ZeroSSL, Google Trust Services, etc.)
|
|
# eabKid: ""
|
|
# eabHmac: ""
|
|
|
|
stepca:
|
|
enabled: false
|
|
# rootCAPath: /path/to/root_ca.crt
|
|
# intermediateCAPath: /path/to/intermediate_ca.crt
|
|
# provisionerName: ""
|
|
# provisionerPassword: ""
|
|
|
|
openssl:
|
|
enabled: false
|
|
# signScript: /path/to/sign.sh
|
|
# revokeScript: /path/to/revoke.sh
|
|
# crlScript: /path/to/crl.sh
|
|
# timeoutSeconds: 30
|
|
|
|
# Notifier connectors configuration
|
|
notifiers:
|
|
slack:
|
|
enabled: false
|
|
# webhookUrl: ""
|
|
# channel: ""
|
|
# username: ""
|
|
# iconEmoji: ""
|
|
|
|
teams:
|
|
enabled: false
|
|
# webhookUrl: ""
|
|
|
|
pagerduty:
|
|
enabled: false
|
|
# routingKey: ""
|
|
# severity: warning
|
|
|
|
opsgenie:
|
|
enabled: false
|
|
# apiKey: ""
|
|
# priority: P3
|
|
|
|
# Additional environment variables
|
|
# Will be passed as-is to the server container
|
|
env: {}
|
|
# Example:
|
|
# CERTCTL_SCHEDULER_RENEWAL_CHECK_INTERVAL: "1h"
|
|
# CERTCTL_DATABASE_MAX_CONNS: "25"
|
|
|
|
# Additional volume mounts for custom configurations
|
|
# volumeMounts: []
|
|
# - name: ca-cert
|
|
# mountPath: /etc/ssl/certs/ca.crt
|
|
# subPath: ca.crt
|
|
|
|
# Additional volumes
|
|
# volumes: []
|
|
# - name: ca-cert
|
|
# secret:
|
|
# secretName: ca-cert
|
|
|
|
# ==============================================================================
|
|
# External Database Configuration (Bundle 3 closure / D2 + OPS-L2)
|
|
# ==============================================================================
|
|
# When postgresql.enabled=false, the chart skips the bundled StatefulSet +
|
|
# Secret + Service and instead consumes the URL below verbatim as the
|
|
# server's CERTCTL_DATABASE_URL. The URL embeds username, password,
|
|
# host, port, database, and sslmode — operators are responsible for
|
|
# rotating credentials in this string out-of-band (Kubernetes Secret +
|
|
# helm upgrade is the supported pattern).
|
|
#
|
|
# Recommended sslmode for managed Postgres (RDS, Cloud SQL, Azure DB):
|
|
# verify-full — PCI-DSS Req 4 v4.0 §2.2.5 compliant; requires CA bundle.
|
|
# Mount the CA via server.volumes / server.volumeMounts and
|
|
# set sslrootcert=/path/in/pod/ca.crt in the URL.
|
|
#
|
|
# Example values overrides:
|
|
# postgresql.enabled: false
|
|
# externalDatabase.url: "postgres://certctl:HUNTER2@db.example.com:5432/certctl?sslmode=verify-full"
|
|
#
|
|
# Migration from the legacy `server.env.CERTCTL_DATABASE_URL` workaround:
|
|
# both still work (env block overrides the helper-emitted Secret value at
|
|
# pod-spec level), but the new path renders cleaner manifests with no
|
|
# stranded postgres-* templates.
|
|
externalDatabase:
|
|
# Connection string used when postgresql.enabled=false.
|
|
# Required in that mode — see certctl.requiredSecrets helper.
|
|
url: ""
|
|
|
|
# ==============================================================================
|
|
# PostgreSQL Configuration
|
|
# ==============================================================================
|
|
postgresql:
|
|
# Enable/disable PostgreSQL (set to false if using external database)
|
|
enabled: true
|
|
|
|
# Image configuration
|
|
image:
|
|
repository: postgres
|
|
tag: "16-alpine"
|
|
pullPolicy: IfNotPresent
|
|
|
|
# Authentication
|
|
auth:
|
|
database: certctl
|
|
username: certctl
|
|
# REQUIRED — set via `--set postgresql.auth.password=<value>` or values override.
|
|
#
|
|
# WARNING (U-1): rotating this value after first deploy does NOT change the
|
|
# database password. The `postgres:16-alpine` image runs `initdb` only when
|
|
# /var/lib/postgresql/data is empty, so POSTGRES_PASSWORD is written into
|
|
# pg_authid exactly once — on the first boot of the StatefulSet's PVC.
|
|
# Subsequent rollouts pick up the new env value in the postgres container
|
|
# but the certctl-server container's CERTCTL_DATABASE_URL also picks up
|
|
# the new value, while pg_authid still expects the old one — leading to
|
|
# `pq: password authentication failed for user "certctl"` (SQLSTATE 28P01).
|
|
#
|
|
# The certctl-server emits guidance via internal/repository/postgres/db.go::
|
|
# wrapPingError when it sees SQLSTATE 28P01 at startup. To resolve in a
|
|
# Helm deployment:
|
|
# - Non-destructive (preferred for environments with data):
|
|
# kubectl exec -it <release>-postgres-0 -- \
|
|
# psql -U certctl -c "ALTER ROLE certctl PASSWORD '<new>';"
|
|
# then update the secret/values to match and let the certctl-server
|
|
# pod restart against the matching credential.
|
|
# - Destructive (DESTROYS DATA — only acceptable on dev/demo PVCs):
|
|
# helm uninstall <release> && \
|
|
# kubectl delete pvc -l app.kubernetes.io/name=certctl,app.kubernetes.io/component=postgres && \
|
|
# helm install <release> ... # PVC re-creates empty, initdb seeds new password
|
|
password: ""
|
|
|
|
# ─────────────────────────────────────────────────────────────────────
|
|
# Bundle B / Audit M-018 (PCI-DSS Req 4 / CWE-319): TLS to Postgres
|
|
# ─────────────────────────────────────────────────────────────────────
|
|
# postgresql.tls.mode is wired into the database-url sslmode parameter
|
|
# (see templates/_helpers.tpl::certctl.databaseURL).
|
|
#
|
|
# Acceptable values (lib/pq):
|
|
# disable — no TLS (default, preserves in-cluster pod-to-pod
|
|
# traffic on the K8s pod network).
|
|
# require — TLS required, no certificate verification.
|
|
# verify-ca — TLS required + verify CA chain.
|
|
# verify-full — TLS required + verify CA chain + verify hostname.
|
|
#
|
|
# PCI-DSS Req 4 v4.0 §2.2.5 requires verify-ca or verify-full when the
|
|
# database carries sensitive data crossing untrusted networks (RDS,
|
|
# Cloud SQL, cross-VPC, etc). The bundled Helm Postgres runs in the
|
|
# same pod network as certctl-server; sslmode=disable is acceptable
|
|
# there only when the cluster CNI provides L2/L3 encryption (Cilium
|
|
# WireGuard, Calico Wireguard, Tailscale operator, etc).
|
|
#
|
|
# When mode != disable AND tls.caSecretRef is set, the CA bundle is
|
|
# mounted at /etc/postgresql-ca/ca.crt and the server's PGSSLROOTCERT
|
|
# env points there. caSecretRef must reference an existing Secret with
|
|
# a "ca.crt" key.
|
|
tls:
|
|
mode: disable
|
|
# caSecretRef: "" # Secret with ca.crt key (required for verify-ca/verify-full)
|
|
|
|
# Storage configuration
|
|
storage:
|
|
size: 10Gi
|
|
storageClass: "" # Uses default StorageClass if empty
|
|
# deleteOnTermination: false # Keep data on Helm uninstall
|
|
|
|
# Resource requests and limits
|
|
resources:
|
|
requests:
|
|
cpu: 100m
|
|
memory: 256Mi
|
|
limits:
|
|
cpu: 500m
|
|
memory: 512Mi
|
|
|
|
# Pod security context
|
|
securityContext:
|
|
runAsNonRoot: true
|
|
runAsUser: 999
|
|
runAsGroup: 999
|
|
fsGroup: 999
|
|
|
|
# Liveness and readiness probes
|
|
livenessProbe:
|
|
exec:
|
|
command:
|
|
- /bin/sh
|
|
- -c
|
|
- pg_isready -U certctl -d certctl
|
|
initialDelaySeconds: 10
|
|
periodSeconds: 10
|
|
timeoutSeconds: 5
|
|
failureThreshold: 3
|
|
|
|
readinessProbe:
|
|
exec:
|
|
command:
|
|
- /bin/sh
|
|
- -c
|
|
- pg_isready -U certctl -d certctl
|
|
initialDelaySeconds: 5
|
|
periodSeconds: 5
|
|
timeoutSeconds: 3
|
|
failureThreshold: 2
|
|
|
|
# Service configuration
|
|
service:
|
|
type: ClusterIP
|
|
port: 5432
|
|
|
|
# PostgreSQL-specific settings
|
|
postgresqlConfig: {}
|
|
# Example:
|
|
# max_connections: "200"
|
|
# shared_buffers: "256MB"
|
|
|
|
# ==============================================================================
|
|
# Certctl Agent Configuration
|
|
# ==============================================================================
|
|
agent:
|
|
# Enable/disable agent deployment
|
|
enabled: true
|
|
|
|
# Deployment strategy: DaemonSet (recommended) or Deployment
|
|
kind: DaemonSet # Options: DaemonSet, Deployment
|
|
|
|
# Image configuration
|
|
image:
|
|
repository: ghcr.io/certctl-io/certctl-agent
|
|
tag: "" # defaults to Chart.appVersion
|
|
pullPolicy: IfNotPresent
|
|
|
|
# Number of replicas (for Deployment kind; ignored for DaemonSet)
|
|
replicas: 1
|
|
|
|
# Resource requests and limits
|
|
resources:
|
|
requests:
|
|
cpu: 50m
|
|
memory: 64Mi
|
|
limits:
|
|
cpu: 200m
|
|
memory: 256Mi
|
|
|
|
# Pod security context
|
|
securityContext:
|
|
runAsNonRoot: true
|
|
runAsUser: 1000
|
|
runAsGroup: 1000
|
|
fsGroup: 1000
|
|
readOnlyRootFilesystem: true
|
|
allowPrivilegeEscalation: false
|
|
capabilities:
|
|
drop:
|
|
- ALL
|
|
|
|
# Agent name (can be overridden per pod via StatefulSet ordinals)
|
|
name: "" # If empty, uses release name
|
|
|
|
# Key storage directory
|
|
keyDir: /var/lib/certctl/keys
|
|
|
|
# Certificate discovery directories (comma-separated)
|
|
discoveryDirs: ""
|
|
# Example: "/etc/ssl/certs,/etc/pki/tls"
|
|
|
|
# Node selector for agent pods (for DaemonSet)
|
|
nodeSelector: {}
|
|
# Example:
|
|
# node-role.kubernetes.io/worker: "true"
|
|
|
|
# Tolerations for agent pods
|
|
tolerations: []
|
|
# Example:
|
|
# - key: node-role
|
|
# operator: Equal
|
|
# value: worker
|
|
# effect: NoSchedule
|
|
|
|
# Affinity rules
|
|
affinity: {}
|
|
|
|
# Additional environment variables
|
|
env: {}
|
|
|
|
# ==============================================================================
|
|
# Ingress Configuration
|
|
# ==============================================================================
|
|
ingress:
|
|
enabled: false
|
|
className: ""
|
|
annotations: {}
|
|
# kubernetes.io/ingress.class: nginx
|
|
|
|
# Optional cert-manager integration for the public-facing Ingress cert.
|
|
# This is completely independent of server.tls.* — the Ingress terminates
|
|
# an *additional* TLS hop between the internet and the in-cluster Service.
|
|
# Leave disabled unless an Ingress is exposing certctl to the outside world.
|
|
certManager:
|
|
enabled: false
|
|
issuerRef:
|
|
name: "" # e.g. "letsencrypt-prod"
|
|
kind: ClusterIssuer # ClusterIssuer or Issuer
|
|
hosts:
|
|
- host: certctl.local
|
|
paths:
|
|
- path: /
|
|
pathType: Prefix
|
|
tls: []
|
|
# - secretName: certctl-tls
|
|
# hosts:
|
|
# - certctl.local
|
|
|
|
# ==============================================================================
|
|
# Service Account Configuration
|
|
# ==============================================================================
|
|
serviceAccount:
|
|
create: true
|
|
annotations: {}
|
|
name: "" # defaults to release name if empty
|
|
|
|
# ==============================================================================
|
|
# RBAC Configuration
|
|
# ==============================================================================
|
|
rbac:
|
|
create: true
|
|
|
|
# ==============================================================================
|
|
# Kubernetes Secrets Target Connector (PREVIEW — Bundle 3 closure / C3)
|
|
# ==============================================================================
|
|
# Bundle 3 audit closure (C3): the connector framework at
|
|
# internal/connector/target/k8ssecret/ ships the Config + interface +
|
|
# 14 unit tests, but the production K8s client at
|
|
# k8ssecret.go::realK8sClient is documented as "a stub placeholder for
|
|
# the real k8s.io/client-go implementation". The repo does not import
|
|
# k8s.io/client-go (verified via `grep -n "client-go" go.mod`), so the
|
|
# connector cannot deploy to a real cluster today.
|
|
#
|
|
# Setting kubernetesSecrets.enabled=true wires up the RBAC verbs the
|
|
# real client will need (get/create/update/patch/delete on Secrets)
|
|
# without making the connector functional — operators trying to use it
|
|
# get the stub's error and a pointer to this note.
|
|
#
|
|
# Status: PREVIEW. Production client lands when the cluster-management
|
|
# bundle ships (tracked in WORKSPACE-ROADMAP.md). Until then,
|
|
# in-cluster deploys use the file-based connectors (NGINX, Apache,
|
|
# HAProxy, etc.) via a Pod-mounted Secret + DaemonSet agent.
|
|
kubernetesSecrets:
|
|
enabled: false
|
|
|
|
# ==============================================================================
|
|
# Pod Disruption Budget (for HA deployments)
|
|
# ==============================================================================
|
|
podDisruptionBudget:
|
|
enabled: false
|
|
minAvailable: 1
|
|
# maxUnavailable: 1
|
|
|
|
# ==============================================================================
|
|
# Monitoring Configuration
|
|
# ==============================================================================
|
|
# Bundle 3 closure (D5): the ServiceMonitor template at
|
|
# templates/servicemonitor.yaml renders when both monitoring.enabled=true
|
|
# AND monitoring.serviceMonitor.enabled=true. The endpoint scrapes
|
|
# /api/v1/metrics/prometheus, which is rbac-gated on `metrics.read` —
|
|
# operators MUST provide a bearer token via
|
|
# monitoring.serviceMonitor.bearerTokenSecret pointing at a Secret with
|
|
# an API key holding that permission. Without the token, scrapes 401.
|
|
monitoring:
|
|
enabled: false
|
|
# Prometheus ServiceMonitor
|
|
serviceMonitor:
|
|
enabled: false
|
|
interval: 30s
|
|
scrapeTimeout: 10s
|
|
# Additional labels applied to the ServiceMonitor metadata.
|
|
# labels: {}
|
|
# Bearer-token Secret reference (required when the certctl server's
|
|
# /api/v1/metrics/prometheus endpoint is gated by api-key auth).
|
|
# Example:
|
|
# bearerTokenSecret:
|
|
# name: certctl-prometheus-key
|
|
# key: api-key
|
|
# bearerTokenSecret: {}
|
|
# TLS config for the scrape endpoint. The certctl server presents
|
|
# the same TLS cert the rest of the chart uses; insecureSkipVerify
|
|
# defaults to true so demos work out of the box. Production deploys
|
|
# should pin the CA via caFile or ca.secret.
|
|
# tlsConfig:
|
|
# caFile: /etc/prometheus/secrets/certctl-ca/ca.crt
|
|
# serverName: certctl-server
|
|
# tlsConfig: {}
|
|
# Optional relabeling for the scrape job.
|
|
# relabelings: []
|
|
|
|
# ==============================================================================
|
|
# Network Policy (Bundle 3 closure / D11)
|
|
# ==============================================================================
|
|
# Default off so existing deploys don't suddenly lose network reach.
|
|
# When enabled, restricts the server pod to:
|
|
# - Ingress: from in-namespace agent pods only.
|
|
# - Egress: kube-dns + bundled Postgres (if enabled).
|
|
# Operators add CA / OIDC / SMTP egress via extraEgress.
|
|
networkPolicy:
|
|
enabled: false
|
|
# Additional Ingress rules merged into the policy. Each entry is a
|
|
# raw networking.k8s.io/v1 NetworkPolicyIngressRule.
|
|
extraIngress: []
|
|
# Additional Egress rules merged into the policy. Common operator
|
|
# need: 443/TCP to an OIDC issuer, 443/TCP to a public CA endpoint,
|
|
# 25/TCP to an SMTP relay.
|
|
# Example:
|
|
# extraEgress:
|
|
# - to:
|
|
# - ipBlock:
|
|
# cidr: 0.0.0.0/0
|
|
# except:
|
|
# - 10.0.0.0/8
|
|
# ports:
|
|
# - protocol: TCP
|
|
# port: 443
|
|
extraEgress: []
|
|
|
|
# ==============================================================================
|
|
# Advanced Configuration
|
|
# ==============================================================================
|
|
|
|
# Node affinity for server pods
|
|
nodeAffinity: {}
|
|
|
|
# Pod affinity for server pods
|
|
podAffinity: {}
|
|
|
|
# Pod anti-affinity for server pods (for HA)
|
|
podAntiAffinity: {}
|
|
# Example:
|
|
# podAntiAffinity:
|
|
# preferredDuringSchedulingIgnoredDuringExecution:
|
|
# - weight: 100
|
|
# podAffinityTerm:
|
|
# labelSelector:
|
|
# matchExpressions:
|
|
# - key: app.kubernetes.io/name
|
|
# operator: In
|
|
# values:
|
|
# - certctl
|
|
# topologyKey: kubernetes.io/hostname
|
|
|
|
# Custom labels for all resources
|
|
customLabels: {}
|
|
|
|
# Custom annotations for all resources
|
|
customAnnotations: {}
|