Skip to content

Security

Pod Security Admission

All three workloads scale-sentry runs (the controller Deployment, the loadgen Job, and the observer sidecar Job) are compatible with the Pod Security Admission Restricted profile out of the box. You can safely label any namespace the controller or its Jobs run in with:

metadata:
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest

Every pod the chart and controller render sets:

Field Value Why
runAsNonRoot true Distroless nonroot base image runs as UID 65532.
seccompProfile.type RuntimeDefault Required by Restricted; uses the container runtime default.
allowPrivilegeEscalation false Required by Restricted.
capabilities.drop [ALL] Required by Restricted.
readOnlyRootFilesystem true Loadgen writes its result file to a shared emptyDir, never the root fs.

The chart exposes controller.podSecurityContext and controller.securityContext Helm values for the controller Deployment. The loadgen and observer pod specs are constructed by the controller in Go, with the same hardening applied unconditionally (see internal/controller/job.go).

RBAC

scale-sentry installs three separate identities with the minimum verbs each one needs.

Controller ClusterRole (cluster-scoped)

The controller manager runs cluster-wide because ScaleValidation Jobs can target Deployments in any namespace. It needs:

API Group Resource Verbs Reason
validation.scale-sentry.ek.co scalevalidations get, list, watch, create, update, patch Reconcile the CR.
validation.scale-sentry.ek.co scalevalidations/status get, update, patch Write phase, conditions, and the verdict report.
validation.scale-sentry.ek.co scalevalidations/finalizers update Garbage-collect Jobs when a CR is deleted.
batch jobs get, list, watch, create, delete Spawn and clean up the loadgen / observer Job.
apps deployments, replicasets, statefulsets get, list, watch Resolve targetRef and the AutoDiscoverProbe path.
apps */scale for those three get Read the target's replica count and pod selector.
"" (core) pods get, list, watch Discover loadgen + observer pods to surface progress.
"" (core) pods/log get Stream observer logs into the controller for the verdict.
"" (core) configmaps get Read user-supplied CA bundles for TLS targets.
"" (core) events create, patch Emit human-readable events on the CR.
coordination.k8s.io leases full lease lifecycle Leader election when controller.replicaCount > 1.

Source: config/rbac/role.yaml, generated by controller-gen from kubebuilder markers.

Observer Role (namespace-scoped)

The observer Job runs alongside the workload it is validating, so it gets a Role (not a ClusterRole) bound only inside that namespace.

Resource Verbs Reason
apps/{deployments,replicasets,statefulsets}/scale get Read the target's pod selector. The workload is never read whole.
pods get, list Map endpoints back to pod identities.
discovery.k8s.io/endpointslices list, watch Detect EndpointSlice updates for leakage analysis.
autoscaling/horizontalpodautoscalers get, list Watch HPA decisions, measure scale-up latency.

The observer needs this Role in every namespace that runs validations, not just the one the chart was installed into. List the extras and the chart renders a full set (ServiceAccount, Role, RoleBinding) into each, and adds every ServiceAccount as a subject on the cAdvisor ClusterRoleBinding:

observer:
  namespaces:
    - staging
    - payments

The release namespace is always covered, so the default is unchanged. A ScaleValidation in a namespace missing from the list produces an observer that cannot read the target's scale subresource, pods, EndpointSlices or HPA, and the run degrades to an Unknown verdict for reasons unrelated to the workload.

For a non-Helm install, apply config/rbac/observer_role.yaml into each namespace with kubectl apply -n <ns> -f. It stays the canonical source of the rules, and just observer-rbac-check diffs it against the chart so the two cannot drift.

Observer ClusterRole (cAdvisor proxy)

The observer scrapes per-pod CFS throttle counters from the kubelet's /metrics/cadvisor endpoint, proxied through the apiserver:

Resource Verbs Reason
nodes/proxy get Read-only scrape of cAdvisor metrics through the apiserver proxy.

This is the only cluster-scoped permission the observer holds. It replaces a previous design that needed pods/exec (create) cluster-wide, a substantially higher-privilege approach.

Loadgen (zero Kubernetes API access)

The loadgen container has no Kubernetes RBAC. It opens HTTP connections to the target Service over the cluster network and writes its result file to a shared emptyDir volume. It runs under the observer ServiceAccount only because it shares the pod, the loadgen process never talks to the API server.

Verifying signatures

Every released image and the OCI chart are signed with cosign keyless signing. The signing identity is the GitHub Actions OIDC token bound to this repository's release workflow, so there are no keys to distribute. Verify provenance before installing:

cosign verify ghcr.io/ethan-kane-ops/scale-sentry:v0.2.0 \
  --certificate-identity-regexp 'https://github.com/ethan-kane-ops/scale-sentry/.+' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

The same command verifies the loadgen and observer images, and the chart at ghcr.io/ethan-kane-ops/charts/scale-sentry.

Reporting a vulnerability

Report vulnerabilities privately via GitHub Security Advisory. See SECURITY.md for the full disclosure policy.