autojanet/skills/securing-k8s-service/SKILL.md
Zoë cc74ad0bd0
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
fix: use library/ Harbor project, add skills, fix pipeline secrets
- .woodpecker.yaml: image paths -> library/autojanet-{agent,dispatcher}
- .woodpecker.yaml: secret names RS_HARBOR_USER / RS_HARBOR_PASS (global)
- container/Dockerfile: restore COPY skills/, skills/ populated from opencode config
- skills/: 84 opencode skills bundled into image
- k8s/manifests: update image refs to library/
2026-05-30 15:43:14 -07:00

5 KiB

name description
securing-k8s-service Use when finishing a new Kubernetes service deployment or auditing an existing one for security hardening on homelab k3s or cloud EKS clusters.

Securing a Kubernetes Service

Overview

Work through each section in order. If you can only do some: follow the priority order at the bottom — the top items give the most security improvement per minute of effort.


1. ServiceAccount (RBAC)

Every app gets its own ServiceAccount. Never use default.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: <app>
  namespace: <app>
automountServiceAccountToken: false  # disable unless app calls k8s API

If app needs k8s API access, create a minimal Role and bind it:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: <app>-role
  namespace: <app>
rules:
  - apiGroups: [""]
    resources: ["configmaps"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: <app>-rolebinding
  namespace: <app>
subjects:
  - kind: ServiceAccount
    name: <app>
roleRef:
  kind: Role
  name: <app>-role
  apiGroup: rbac.authorization.k8s.io

2. Pod Security Context

Set on spec.template.spec (pod-level) AND each container.

# Pod-level (spec.template.spec.securityContext)
securityContext:
  runAsNonRoot: true
  runAsUser: 1000       # check image docs for correct UID
  runAsGroup: 1000
  fsGroup: 1000
  seccompProfile:
    type: RuntimeDefault

# Container-level (each container's securityContext)
securityContext:
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop: ["ALL"]
    add: []  # only add if explicitly required (e.g., NET_BIND_SERVICE for :80)

readOnlyRootFilesystem: true — if the app writes to disk, mount writable paths via emptyDir:

volumes:
  - name: tmp
    emptyDir: {}
  - name: cache
    emptyDir: {}
volumeMounts:
  - name: tmp
    mountPath: /tmp
  - name: cache
    mountPath: /var/cache

Common paths that need emptyDir: /tmp, /var/cache, /var/run, /home/<user>/.config


3. Resource Limits

Always set both requests AND limits.

resources:
  requests:
    cpu: "100m"
    memory: "128Mi"
  limits:
    cpu: "500m"      # throttles (not kills) — set generously or omit if unsure
    memory: "512Mi"  # OOMKill if exceeded — set to ~2-3x steady-state usage

Check actual usage after deploy:

kubectl top pods -n <namespace>

4. NetworkPolicy (Cilium)

Default deny all, then add exceptions.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: <app>-netpol
  namespace: <app>
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: <app>
  policyTypes: [Ingress, Egress]
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: traefik
      ports:
        - port: <app-port>
  egress:
    # DNS — almost always needed
    - to: []
      ports:
        - port: 53
          protocol: UDP
    # External HTTPS — add if app calls external APIs
    - to: []
      ports:
        - port: 443
    # Specific namespace (e.g., database)
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: <db-namespace>
      ports:
        - port: 5432

5. Secret Handling

  • Never put secrets in values.yaml or ConfigMaps — use ExternalSecret → OpenBao
  • Set argocd.argoproj.io/sync-wave: "-1" on ExternalSecrets so they exist before the Deployment
  • Prefer secretKeyRef in env vars over mounted files (unless app requires file format)

6. Image Security

  • Pin image tags: image:v1.2.3 not image:latest
  • Check registry.ctz.fyi Harbor scans for vulnerability reports
  • For custom images: use minimal base (distroless, alpine, scratch)
  • Set imagePullPolicy: IfNotPresent (not Always, unless actively testing)

Quick Audit (Existing Deployments)

# Security contexts
kubectl get deploy <name> -n <ns> -o jsonpath='{.spec.template.spec.securityContext}'
kubectl get deploy <name> -n <ns> -o jsonpath='{.spec.template.spec.containers[0].securityContext}'

# Resource limits
kubectl get deploy <name> -n <ns> -o jsonpath='{.spec.template.spec.containers[0].resources}'

# ServiceAccount
kubectl get deploy <name> -n <ns> -o jsonpath='{.spec.template.spec.serviceAccountName}'

# Default SA automount (should be false or unset)
kubectl get sa default -n <ns> -o jsonpath='{.automountServiceAccountToken}'

# NetworkPolicies
kubectl get networkpolicy -n <ns>

Priority Order

If you can only do some of these, start here:

  1. Non-root + no privilege escalation — most impactful, easy to add
  2. Resource limits — prevents noisy-neighbor and OOM cascade
  3. Dedicated ServiceAccount + no automount — limits blast radius
  4. NetworkPolicy — isolates a compromised pod
  5. readOnlyRootFilesystem — hardens against post-compromise persistence