Hardening
Two lists: what the charts already do, and what is left to you. The second list is short but it is not empty, and we would rather name it than let you assume otherwise.
Hardened by default
Every application service runs with this security context, and it is not opt-in.
podSecurityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
| Control | Status |
|---|---|
| Runs as non-root | Yes, uid 65532 |
| Read-only root filesystem | Yes |
| Privilege escalation | Disabled |
| Linux capabilities | All dropped |
| Privileged containers | None |
| Host network, PID or IPC | None |
| Host path mounts | None |
| Node access | None |
Several services defaulted to writing into a relative path under their working directory, which fails outright on a read-only root. Each one now gets an explicit writable volume mounted where it expects to write. That is why the values file carries per-service volume mounts: they are the cost of this control, not decoration. If you add a service-level override that removes one, that service will fail to start.
Pod Security Admission
The application pods satisfy the restricted profile. Label the namespace to enforce it:
kubectl label namespace gen0sec \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=latest
Do not label gen0sec-system restricted yet. The Postgres and Kafka pods there are created by
upstream operators and have not been assessed against that profile. Use baseline if you need
something, and tell us if you require restricted there.
What you should add
None of these ship, and all of them are worth doing.
NetworkPolicies
No NetworkPolicy ships with the charts. On a cluster without default-deny, that means the platform namespaces are as open as everything else.
Add default-deny in both namespaces, then allow exactly these:
| From | To | Port |
|---|---|---|
gen0sec services | core-pooler.gen0sec-system | 5432 |
| Migration job | core.gen0sec-system | 5432 |
gen0sec services | core-kafka-bootstrap.gen0sec-system | 9092 |
gen0sec services | rustfs-svc.gen0sec-system | 9000 |
gen0sec services | dragonfly.gen0sec-system | 6379 |
| Ingress controller | gen0sec services | Service ports |
| Relay pod | api.gen0sec.com:443 | 443, egress |
Egress default-deny is the high-value one here. The relay is meant to be the only pod with a route out. A NetworkPolicy is what turns that from a design intention into an enforced property, and it is the single strongest control you can add to this deployment.
TLS everywhere it is not already
| Hop | Today | Worth adding |
|---|---|---|
| Ingress to users and agents | TLS you terminate | Already yours |
| Relay to Gen0Sec | TLS | Nothing to do |
| Services to Postgres | TLS to the pooler | Nothing to do |
| Services to the object store | Plain HTTP, in-cluster | A service mesh, if your threat model includes the pod network |
| Services to the cache | Plain, in-cluster, no credential in the URL | Enable cache authentication, then move the connection string into each service's secret |
Resource limits on the four workloads that have none
The object store, the Kafka brokers, the ingress cache and the Postgres operator declare no CPU or memory requests. That makes them BestEffort or Burstable, so the kubelet evicts them first under node pressure. For the object store, holding your data, that is the wrong priority order.
Set requests on them, or set a namespace LimitRange so nothing lands unbounded.
See Sizing.
Secrets from an external manager
make-db-secrets.sh is fine for evaluation. In production, have your secret manager produce secrets
of the same shape so credentials never exist as plain Kubernetes secrets you created by hand.
Every name and key is listed in Secrets.
Settings you should never enable
Two environment variables exist and both disable a security control. Neither is set by the charts, and neither appears anywhere in the install procedure.
| Setting | What it does | When it is acceptable |
|---|---|---|
TLS_INSECURE_SKIP_VERIFY | Stops the relay verifying the upstream certificate | Never in production. It defeats the protection on your only outbound connection |
MIRROR_TLS_VERIFY=false | Stops the bundler verifying your registry's certificate when pushing | Only for a plain-HTTP registry on a trusted network, and preferably not then |
If you find either one set in a running deployment, treat it as a finding.
Verifying the posture
Confirm no pod runs as root or privileged:
kubectl -n gen0sec get pods -o json | jq -r '
.items[] | .metadata.name + " runAsNonRoot=" +
((.spec.securityContext.runAsNonRoot // false) | tostring)'
Confirm nothing holds cluster-admin:
kubectl get clusterrolebinding -o json \
| jq -r '.items[] | select(.roleRef.name == "cluster-admin") | .metadata.name'
Confirm only the relay can reach the internet, once you have added egress policy:
kubectl -n gen0sec run netcheck --rm -it --restart=Never \
--image=$REGISTRY/gen0sec/busybox:1.36.1-glibc -- wget -T5 -qO- https://example.com ; echo "exit=$?"
A non-zero exit is the result you want.