Skip to content

Security Defaults

FieldValue
TypeAgent Reference
Source~/.copilot/agents/_refs/platform-sre-kubernetes/security-defaults.md
DescriptionNot specified

Source Content

Security Defaults

Applied on every workload by default — Pod Security Standard restricted baseline, plus NetworkPolicy least-privilege. Templates ship hardened; relaxing requires an explicit values.yaml flag and a comment explaining why.

Pod & container security context

spec:
template:
spec:
automountServiceAccountToken: false # opt in only when needed
securityContext:
runAsNonRoot: true
runAsUser: 65532 # distroless nonroot UID
runAsGroup: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: ghcr.io/<org>/<svc>@sha256:...
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop: ["ALL"]
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 512Mi }

If the app needs to write at runtime (cache, tmp), mount an emptyDir to that path — never disable readOnlyRootFilesystem.

NetworkPolicy — default deny + explicit allow

Every namespace starts with a default-deny policy:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny, namespace: <svc> }
spec:
podSelector: {}
policyTypes: [Ingress, Egress]

Then per-workload allow rules:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: <svc>-allow, namespace: <svc> }
spec:
podSelector:
matchLabels: { app.kubernetes.io/name: <svc> }
policyTypes: [Ingress, Egress]
ingress:
- from:
- namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: traefik } }
podSelector: { matchLabels: { app.kubernetes.io/name: traefik } }
ports: [{ port: http, protocol: TCP }]
egress:
- to: [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: cnpg-system } } }]
ports: [{ port: 5432, protocol: TCP }]
- to: [{ namespaceSelector: {}, podSelector: { matchLabels: { k8s-app: kube-dns } } }]
ports: [{ port: 53, protocol: UDP }, { port: 53, protocol: TCP }]

Egress to public internet only when the service genuinely needs it — and then named, not 0.0.0.0/0.

Image standards

  • Base: distroless (gcr.io/distroless/static:nonroot for Go, gcr.io/distroless/nodejs20-debian12:nonroot for Node).
  • Tags: digest-pinned in production. Floating tags allowed only in test.
  • Signing: cosign keyless via OIDC; ArgoCD verifies signatures on sync (Sigstore policy controller).
  • Vulnerability scan: Trivy in CI; fail on CRITICAL. Daily rescan via Trivy Operator publishes to Prometheus.

RBAC

  • One ServiceAccount per workload. Never reuse default.
  • Role/RoleBinding (namespace-scoped) over ClusterRole/ClusterRoleBinding.
  • Verbs limited to those actually called — start at zero, add as the app fails.

Policy enforcement

Cluster admission policies via Kyverno (preferred — pure YAML) or OPA Gatekeeper. Mandatory policies:

PolicyAction
disallow-latest-tagblock on :latest
require-non-rootblock missing runAsNonRoot
require-resource-limitsblock missing CPU/memory limits
require-readonly-rootfsblock missing readOnlyRootFilesystem
require-drop-all-capsblock missing capabilities.drop: [ALL]
disallow-host-namespacesblock hostNetwork/hostPID/hostIPC
verify-image-signaturesreject unsigned images in prod

Run kyverno test against the policy library in CI. New charts are expected to pass without exceptions.