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:
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.