Skip to main content

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]
ControlStatus
Runs as non-rootYes, uid 65532
Read-only root filesystemYes
Privilege escalationDisabled
Linux capabilitiesAll dropped
Privileged containersNone
Host network, PID or IPCNone
Host path mountsNone
Node accessNone
Read-only root filesystems were not free

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:

FromToPort
gen0sec servicescore-pooler.gen0sec-system5432
Migration jobcore.gen0sec-system5432
gen0sec servicescore-kafka-bootstrap.gen0sec-system9092
gen0sec servicesrustfs-svc.gen0sec-system9000
gen0sec servicesdragonfly.gen0sec-system6379
Ingress controllergen0sec servicesService ports
Relay podapi.gen0sec.com:443443, 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

HopTodayWorth adding
Ingress to users and agentsTLS you terminateAlready yours
Relay to Gen0SecTLSNothing to do
Services to PostgresTLS to the poolerNothing to do
Services to the object storePlain HTTP, in-clusterA service mesh, if your threat model includes the pod network
Services to the cachePlain, in-cluster, no credential in the URLEnable 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.

SettingWhat it doesWhen it is acceptable
TLS_INSECURE_SKIP_VERIFYStops the relay verifying the upstream certificateNever in production. It defeats the protection on your only outbound connection
MIRROR_TLS_VERIFY=falseStops the bundler verifying your registry's certificate when pushingOnly 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.