Skip to content

About

Kubernetes security lab: OPA Gatekeeper admission control, kubesec static analysis, ESO secret injection, LocalStack AWS — automated with Terraform

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

k3s Security Lab

A fully automated Kubernetes security lab that deploys a hardened nginx webapp on a local k3d cluster backed by LocalStack AWS services. Built to demonstrate layered security controls across static analysis, runtime admission control, secrets management, and infrastructure hardening — all repeatable from a single Terraform apply sequence.


Using this as your own — repoint Argo CD first

This lab is GitOps: Argo CD serves the webapp by pulling charts/webapp from a Git URL, not from your local clone. That URL is hardcoded to the original repo, so if you fork/clone this and make it your own, you must repoint Argo at your repo first — otherwise your edits never reach the running app (Argo keeps syncing the original public repo, which you can't push to).

  1. Update repoURL in argocd/webapp-application.yaml (and examples/argocd-webapp-demo.yaml) to your repo:
    repoURL: https://github.com/<your-user>/<your-repo>.git
    One-liner from the repo root:
    grep -rl 'leeclay95/k3s-security-lab' argocd examples | xargs sed -i 's#leeclay95/k3s-security-lab#<your-user>/<your-repo>#g'
  2. If your repo is private, register repo credentials with Argo CD (a repo Secret / PAT). The default setup assumes a public repo and clones anonymously — no credentials configured.
  3. Commit and push, then make deploy.

Everything else is generic and needs no change: Terraform, k3d, floci, the ECR account 000000000000 and us-east-1 (LocalStack/floci defaults), and the helper scripts are all created by the deploy, not tied to any account.


What This Proves

Layer Tool What it enforces
Static analysis kubesec Scores manifests before anything is deployed — CI gate
Image scanning trivy Image CVE-scanned in CI — fixable HIGH/CRITICAL block the merge
Admission control OPA Gatekeeper Cluster refuses insecure pods at apply time, no bypass possible
Secrets management External Secrets Operator Secrets never in manifests or Git; injected at runtime from Secrets Manager
IaC security tfsec Terraform scanned for misconfigurations — 0 HIGH findings
Image provenance ECR (LocalStack) Private registry with immutable tags; no public image pull at runtime
Encryption at rest KMS Secrets Manager and RDS encrypted with a customer-managed CMK
Least privilege IAM role assumption ESO assumes a scoped role; static creds can only call sts:AssumeRole
RBAC ServiceAccount + empty Role Pod identity with zero API permissions; no token automount

Architecture

┌─────────────────────────────────────────────────────────┐
│  LocalStack (localhost:4566)                            │
│                                                         │
│  ┌──────┐  ┌────────────────┐  ┌─────┐  ┌──────────┐  │
│  │ KMS  │  │Secrets Manager │  │ RDS │  │CloudWatch│  │
│  │ CMK  │  │webapp/secrets  │  │ pg  │  │+ SNS     │  │
│  └──┬───┘  │webapp/db-creds │  └─────┘  └──────────┘  │
│     │      └───────┬────────┘                          │
│     └──────────────┘ (encrypts)                        │
│                                                         │
│  ┌──────────────────────────────────────────────────┐  │
│  │ IAM                                              │  │
│  │  eso-user ──(sts:AssumeRole)──► eso-role         │  │
│  │                                  │               │  │
│  │                     GetSecretValue (exact ARNs)  │  │
│  └──────────────────────────────────────────────────┘  │
│                                                         │
│  ECR (port 5100): 000000000000.dkr.ecr.*.cloud:5100    │
└─────────────────────────────────────────────────────────┘
             │ secrets synced by ESO          │ image
             ▼                                ▼
┌────────────────────────────────────────────────────────┐
│  k3d cluster: webapp-test                              │
│                                                        │
│  ┌───────────────────────────────────────────────┐    │
│  │ gatekeeper-system (Helm chart)                │    │
│  │  Webhook: block-privileged, require-non-root, │    │
│  │  require-limits, block-host-ns, block-caps    │    │
│  └───────────────────────────────────────────────┘    │
│                                                        │
│  ┌───────────────────────────────────────────────┐    │
│  │ external-secrets (Helm chart)                 │    │
│  │  ESO controller → LocalStack STS → role creds │    │
│  │  SecretStore → Secrets Manager → K8s Secrets  │    │
│  └───────────────────────────────────────────────┘    │
│                                                        │
│  ┌───────────────────────────────────────────────┐    │
│  │ webapp namespace (Helm chart)                 │    │
│  │  webapp-sa (automountServiceAccountToken=false│    │
│  │  webapp pod (ECR image, readOnlyRootFilesystem│    │
│  │    env: SECRET_KEY, DB_PASSWORD, DB_URL       │    │
│  │    all from K8s Secrets managed by ESO        │    │
│  │  Gatekeeper policies (hook-ordered)           │    │
│  └───────────────────────────────────────────────┘    │
│                                                        │
│  NodePort 30080 → container 8080                       │
└────────────────────────────────────────────────────────┘

Ownership

Layer Owner Managed by
k3d cluster, floci prereqs (infra/secrets), image import Terraform terraform/ roots + Makefile
Gatekeeper, ESO, Argo CD controllers Terraform helm_release in terraform/cluster/
webapp workload — Deployment, Service, RBAC, ConfigMap, ESO config, Gatekeeper policies Argo CD Application webapp → charts/webapp from Git

Terraform installs Argo CD and plants the Application (argocd/webapp-application.yaml, applied by terraform/cluster/argocd.tf); from there Argo is the single reconciler of the webapp namespace and self-heals drift continuously. See Config drift.


Prerequisites

# Required tools
docker          # Docker Engine (k3d runs cluster nodes as containers)
k3d             # Local k3s clusters via Docker
kubectl         # Kubernetes CLI
terraform       # >= 1.3
helm            # Helm 3
kubesec         # Static manifest scanner
tfsec           # Terraform static analysis
trivy           # Container image CVE scanner (security-gates.sh Gate 4)
aws             # AWS CLI v2 (uses AWS_ENDPOINT_URL env var for LocalStack)

# LocalStack running with these services
curl -s http://localhost:4566/_localstack/health | jq '.services | keys'
# Required: secretsmanager, kms, iam, sts, ecr, rds, s3, cloudwatch, logs, sns

Docker must trust the floci ECR registry (HTTP)

The floci/LocalStack ECR endpoint (000000000000.dkr.ecr.us-east-1.localhost.localstack.cloud:5100) serves plain HTTP. Docker only pushes over HTTP to a registry it considers insecure — either the host resolves into 127.0.0.0/8 (Docker auto-trusts loopback) or it's listed in insecure-registries.

On most distros localhost.localstack.cloud resolves to 127.0.0.1, so it just works. On Ubuntu, systemd-resolved strips loopback answers from upstream DNS (rebinding protection), so the host no longer looks like loopback and the ecr_push fails mid-terraform apply with:

http: server gave HTTP response to HTTPS client

Fix once (needs sudo — edits the Docker daemon config and restarts it):

echo '{"insecure-registries":["000000000000.dkr.ecr.us-east-1.localhost.localstack.cloud:5100"]}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker

If /etc/docker/daemon.json already exists, merge the insecure-registries key instead of overwriting the file.

make deploy runs make preflight first, which detects this and prints the fix before the multi-minute Terraform apply. Skip it with SKIP_DOCKER_PREFLIGHT=1.


Apply Order

Three independent Terraform roots applied in sequence. Each root is isolated so secrets and infrastructure survive cluster destroy/recreate.

TL;DR: make deploy runs this entire chain in order for you — floci → secrets → infra → secrets (re-applied with KMS) → cluster (staged). Secret values default to the lab examples; override with make deploy DB_PASSWORD=… API_KEY=… SECRET_KEY=…. The manual steps below are the reference for what it automates and why the order matters (infra's IAM policy reads the webapp/secrets secret, so secrets must exist first).

1. Secrets (persistent — apply once)

cd terraform/secrets
terraform init
terraform apply -var="db_password=hunter2" -var="api_key=abc123" -var="secret_key=s3cr3t"

2. Infrastructure (persistent — apply once, re-apply to update)

cd terraform/infra
terraform init
terraform apply -var="db_password=hunter2"
terraform output kms_key_arn

Re-apply secrets with KMS encryption now that the key exists:

cd terraform/secrets
terraform apply -var="kms_key_arn=$(cd ../infra && terraform output -raw kms_key_arn)" -var="db_password=hunter2" -var="api_key=abc123" -var="secret_key=s3cr3t"

3. Cluster (disposable — destroy and recreate freely)

One command from the repo root — the Makefile runs the staged apply for you:

make deploy     # create cluster, install Helm charts (Gatekeeper, ESO, webapp), deploy webapp
make destroy    # tear down the cluster (secrets/ and infra/ survive)
make status     # pods, ExternalSecrets, Gatekeeper constraints
make url        # print the webapp URL

make deploy stages the apply internally because the Helm and Kubernetes providers validate against a live cluster at plan time: it creates the k3d cluster first (-target), then installs the Gatekeeper + ESO controllers, then runs the final unconstrained apply for the webapp and everything else.

To run it by hand instead:

cd terraform/cluster && terraform init
terraform apply -target=null_resource.k3d_cluster -target=time_sleep.cluster_ready
terraform apply

Destroy and recreate the cluster

make destroy removes only the k3d cluster — terraform/secrets and terraform/infra (LocalStack secrets, KMS, RDS, etc.) survive, so the recreate picks them straight back up.

make destroy            # tear down the cluster
make deploy             # bring it back (cluster + Helm charts + webapp)

make destroy && make deploy   # or both in one line

After it comes back up:

make status             # pods, ExternalSecrets, Gatekeeper constraints
make url                # prints http://localhost:30080

Note: LocalStack must be running before make deploy (docker ps | grep localstack), or ESO reports SecretSyncedError and the webapp pod won't receive its secrets.


Helm Chart — charts/webapp/

The webapp and all its supporting resources are packaged as a local Helm chart. This replaces the previous loose YAML files and null_resource + kubectl apply calls.

What's in the chart

Template Resources
deployment.yaml Hardened nginx-unprivileged deployment
service.yaml NodePort service (30080 → 8080)
serviceaccount.yaml ServiceAccount + zero-permission Role + RoleBinding
eso-aws-credentials.yaml AWS credentials Secret for ESO
eso-secretstore.yaml SecretStore with IAM role assumption
eso-externalsecret.yaml ExternalSecret for app secrets
eso-db-externalsecret.yaml ExternalSecret for DB credentials
gatekeeper-templates.yaml 5 ConstraintTemplates (Helm hook wt 0 / Argo sync-wave 0)
gatekeeper-constraints.yaml 5 Constraints (Helm hook wt 5 / Argo sync-wave 1)
values-argocd.yaml Values for the Argo CD-managed deployment (helmHooks: false)

Customizing values

Override defaults in values.yaml or pass via Terraform set {} blocks:

# Preview rendered templates
helm template webapp charts/webapp/ --debug

# Disable Gatekeeper policies (e.g., for testing)
helm template webapp charts/webapp/ --set gatekeeper.enabled=false

tfsec Results

tfsec ./terraform -m HIGH --concise-output
# No problems detected.

All four original HIGH findings resolved:

Finding Fix
ECR mutable tags image_tag_mutability = "IMMUTABLE"
IAM wildcard resource Exact secret ARNs via data source — no webapp/* glob
SNS unencrypted kms_master_key_id on topic
RDS storage unencrypted storage_encrypted = true with KMS CMK

Image CVE Scanning (trivy)

scripts/security-gates.sh Gate 4 (and the CI workflow) runs trivy image against the container image and fails on fixable HIGH/CRITICAL CVEs:

trivy image --severity HIGH,CRITICAL --ignore-unfixed docker.io/nginxinc/nginx-unprivileged:1.27

The deployed image is a floci/LocalStack ECR ref that CI can't reach, so the gate scans the identical upstream image it's retagged from (the ECR copy is a pull+tag+push with no rebuild — byte-for-byte the same layers). Override the target/threshold with SCAN_IMAGE, TRIVY_SEVERITY, TRIVY_IGNORE_UNFIXED. Evidence (trivy.json + trivy.txt) lands under security/reports/<ts>/. This is the enforced side of RA-5 / SI-2 — see security/COMPLIANCE.md.

The pinned lab image carries known base-OS CVEs (Debian libraries), so those are explicitly risk-accepted in security/trivy/.trivyignore.yaml — each CVE ID with a justification and an expiry date, not a blanket disable. The threshold stays strict: any new or unlisted CVE still fails the gate, and each acceptance expires to force a re-review or an image bump (the real fix).

Demo: make Gate 4 fail, then pass

Prove the gate actually bites by pulling one CVE out of the register, watching CI go red, then restoring it. CVE-2026-31789 (libssl3, CRITICAL) is used here — swap in any ID from .trivyignore.yaml.

git checkout main && git pull
git checkout -b test/trivy-fail-demo

# 1. Remove one CVE's block from the register (from its "- id:" line to the next entry)
python3 - <<'PY'
import re
cve = "CVE-2026-31789"
p = "security/trivy/.trivyignore.yaml"
lines = open(p).read().splitlines(keepends=True)
out, i = [], 0
while i < len(lines):
    if re.match(rf'^  - id:\s*{re.escape(cve)}\s*$', lines[i]):
        i += 1
        while i < len(lines) and not lines[i].startswith('  - id:'):
            i += 1
        continue
    out.append(lines[i]); i += 1
open(p, "w").writelines(out)
print("removed", cve)
PY

# 2. Confirm locally the gate now FAILS (note --exit-code 1 — without it trivy always exits 0)
trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 --ignorefile security/trivy/.trivyignore.yaml docker.io/nginxinc/nginx-unprivileged:1.27; echo "exit=$?"   # exit=1, CVE reappears

# 3. Push + open a PR — watch security-gates go RED
git commit -am "test: remove CVE-2026-31789 to demo Gate 4 failure"
git push -u origin test/trivy-fail-demo
gh pr create --fill
gh pr checks --watch     # FAILS (this command also exits non-zero — that's the red gate, not an error)

# 4. Restore the CVE, push again — watch it go GREEN
git checkout main -- security/trivy/.trivyignore.yaml
git commit -am "test: restore CVE-2026-31789 — Gate 4 green again"
git push
gh pr checks --watch     # PASSES

# 5. Discard the demo (main never changes) — or merge it if you want it in history
gh pr close test/trivy-fail-demo --delete-branch && git checkout main

The whole local run is available via ./scripts/security-gates.sh, which runs trivy with --exit-code 1 and flips Gate 4 to FAIL/PASS accordingly.


Testing Security Controls

Gatekeeper — test the admission policies

Five constraints are enforced (enforcementAction: deny) and scoped to the webapp namespace. Gatekeeper is an admission webhook, so violations are rejected at apply time — the object never reaches the cluster.

See what's active:

kubectl get constrainttemplates          # the 5 policy templates
kubectl get constraints                   # the 5 constraints + live violation counts

Test each policy — every command below should be denied by the webhook:

# 1. block-privileged — privileged container
kubectl run t1 -n webapp --image=nginx --overrides='{"spec":{"containers":[{"name":"t1","image":"nginx","securityContext":{"privileged":true}}]}}'

# 2. require-non-root — no runAsNonRoot
kubectl run t2 -n webapp --image=nginx
# denied: containers must set runAsNonRoot: true

# 3. require-resource-limits — no cpu/memory limits
kubectl run t3 -n webapp --image=nginx --overrides='{"spec":{"containers":[{"name":"t3","image":"nginx","securityContext":{"runAsNonRoot":true}}]}}'
# denied: container has no resource limits

# 4. block-host-namespaces — hostNetwork/hostPID/hostIPC
kubectl run t4 -n webapp --image=nginx --overrides='{"spec":{"hostNetwork":true,"containers":[{"name":"t4","image":"nginx"}]}}'

# 5. block-dangerous-caps — added SYS_ADMIN capability
kubectl run t5 -n webapp --image=nginx --overrides='{"spec":{"containers":[{"name":"t5","image":"nginx","securityContext":{"capabilities":{"add":["SYS_ADMIN"]}}}]}}'

Each returns Error from server: admission webhook "validation.gatekeeper.sh" denied the request: [<constraint>] <message>.

Confirm the compliant webapp still passes — Argo's deployment satisfies all five (non-root, limits, dropped caps, no host namespaces), so it stays Healthy:

kubectl get pods -n webapp                # webapp pod Running — it meets every policy

Inspect violations / audit (Gatekeeper also audits existing objects, not just new ones):

kubectl describe k8srequirelimits.constraints.gatekeeper.sh/require-resource-limits
kubectl get constraints -o custom-columns='NAME:.metadata.name,ACTION:.spec.enforcementAction,VIOLATIONS:.status.totalViolations'

These policies are part of the chart Argo owns. To change them, edit charts/webapp/templates/gatekeeper-*.yaml, commit, and Argo syncs the new policy — the GitOps path for security controls too.

Config drift — Argo CD self-heals out-of-band changes (GitOps)

The webapp is owned by Argo CD, not Terraform (see Ownership). Argo continuously reconciles the live webapp namespace against charts/webapp in Git: any out-of-band kubectl edit is reverted by selfHeal, and objects deleted from the cluster are recreated by prune — no terraform apply needed.

Accessing the apps

make access        # prints all URLs + how to log in
What How
webapp http://localhost:30080 (NodePort, host-mapped — survives pod rollovers)
Argo CD UI make argo-ui → https://localhost:8081, user admin
Argo admin password `kubectl --context k3d-webapp-test -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}'

The Argo UI serves HTTPS with a self-signed cert — accept the browser warning. After first login, change the admin password and delete the bootstrap secret.

Quick CLI drift tests

# scale out of band — Argo scales it back to the Git value (1)
kubectl scale deploy/webapp -n webapp --replicas=5
kubectl get deploy webapp -n webapp -w        # watch it return to 1 (Ctrl-C to stop)

# delete a managed object — prune recreates it from Git
kubectl delete svc webapp -n webapp
kubectl get svc -n webapp -w                  # watch it reappear

make argo-app                                 # Application flips OutOfSync -> Synced

Full page-drift walkthrough (see it in the browser)

Self-heal is fast, so to actually watch the drift, pause it first, then resume on demand:

# 0. baseline — the Git-declared page
curl -s http://localhost:30080 | grep -o '<title>.*</title>'     # Welcome to nginx!

# 1. PAUSE Argo's auto-revert so the drift holds
make argo-pause

# 2. mount the Matrix page over nginx's docroot on the LIVE Deployment (out-of-band)
kubectl apply -f examples/matrix-configmap.yaml
kubectl patch deployment webapp -n webapp --type=strategic -p '{"spec":{"template":{"spec":{"volumes":[{"name":"html","configMap":{"name":"matrix-index-html"}}]}}}}'
kubectl rollout status deploy/webapp -n webapp
curl -s http://localhost:30080 | grep -o '<title>.*</title>'     # the matrix has you
#    open http://localhost:30080 in the browser — the Matrix page STAYS (self-heal paused)
#    in the Argo UI the webapp app now shows OutOfSync (drift detected, not healed)

# 3. RESUME self-heal — Argo reverts the mount back to Git within seconds
make argo-resume
kubectl rollout status deploy/webapp -n webapp
curl -s http://localhost:30080 | grep -o '<title>.*</title>'     # Welcome to nginx! (reverted)

# 4. cleanup the leftover drift ConfigMap (Argo never tracked it, so it lingers)
kubectl delete -f examples/matrix-configmap.yaml

The GitOps way to actually change the page (vs. the drift above) is to edit charts/webapp/templates/configmap-index.yaml, commit, and push — Argo then deploys it as an approved change. The kubectl patch is the unapproved path, which is exactly why Argo reverts it.

GUI drift test

With make argo-ui running, open the webapp app tile: the resource tree shows each object's health, status flips to OutOfSync the instant you drift it, and back to Synced as Argo reconciles. The APP DIFF button shows the exact diff.

Self-heal latency: a single drift on a healthy app reverts within seconds. Argo applies an exponential self-heal backoff under repeated rapid drift, so during heavy testing a revert can take a minute or two — it always converges. make argo-pause / make argo-resume gives you deterministic control for demos.

Try it without a full migration — examples/argocd-webapp-demo.yaml runs the same chart under Argo in an isolated webapp-argo namespace (Gatekeeper disabled, nodePort 30081) so it's safe on a cluster where Terraform still owns webapp:

kubectl apply -f examples/argocd-webapp-demo.yaml
make argo-app                                # or: kubectl get application webapp-demo -n argocd
kubectl port-forward -n webapp-argo svc/webapp 8082:80   # 30081 isn't host-mapped
# cleanup: kubectl delete -f examples/argocd-webapp-demo.yaml && kubectl delete ns webapp-argo

Why this beats plan-time drift detection: Terraform only catches drift when someone runs terraform plan. Argo reconciles continuously, so unapproved kubectl changes are reverted on their own — the "approved changes only through Git" model. Terraform's job shrinks to day-0 platform (cluster, controllers, AWS bootstrap, image import).

kubesec — static manifest score

kubesec scan examples/hardened/deployment.yaml
# score: +12 (was -85 before hardening)

ESO — secret rotation test

# Update a secret value in LocalStack
aws --endpoint-url=http://localhost:4566 secretsmanager put-secret-value --secret-id webapp/secrets --secret-string '{"db_password":"newpass","api_key":"newkey","secret_key":"newval"}'

# ESO syncs within refreshInterval (1 minute) — watch it update
kubectl get secret webapp-secret -n webapp -w

Helm lifecycle

# View release status
helm list -n webapp

# Rollback to previous version
helm rollback webapp -n webapp

# Show diff before upgrade
helm diff upgrade webapp charts/webapp/ -n webapp

After a reboot: make reboot

A host reboot restarts the k3d node container, which breaks several things a plain kubectl get pods won't explain — and they compound:

Symptom Cause
Pods stuck ContainerCreating, failed to setup network ... /run/flannel/subnet.env flannel's subnet file lives on tmpfs (/run), wiped on node restart
ESO SecretSyncedError, lookup host.k3d.internal ... i/o timeout k3s strips host.k3d.internal from CoreDNS on restart
webapp ImagePullBackOff, dial tcp 127.0.0.1:5100: connection refused the image dropped out of the node's containerd; the node can't pull the floci ECR ref
ESO/webapp errors, floci unreachable floci container down / lost un-flushed data

make reboot diagnoses and auto-heals all of them (idempotent, safe to run anytime — it never touches Terraform state, floci secrets, or the infra/secrets roots):

make reboot

It checks and fixes, in order: floci up → cluster started → flannel (bounces the node if subnet.env is missing) → host.k3d.internal (durable coredns-custom mapping) → webapp image on the node (see below) → ESO re-sync → log shipper → webapp rollout, then prints status and the app HTTP code.

Image loading on modern Docker. If docker info shows the containerd-snapshotter image store (default on recent Docker/Ubuntu), k3d image import silently no-ops (its docker save output is an OCI layout the importer rejects: "content digest not found"). make deploy and make reboot both use scripts/ensure-webapp-image.sh, which verifies the image actually landed and falls back to pulling the identical upstream image into the node's containerd and retagging it. So the image step is reliable regardless of Docker's storage driver.

Key Commands

# App
curl http://localhost:30080

# Pod status + image
kubectl get pods -n webapp -o wide
kubectl get pod -n webapp -o jsonpath='{.items[0].spec.containers[0].image}'

# Secrets synced by ESO
kubectl get externalsecret -n webapp
kubectl get secret webapp-secret webapp-db-secret -n webapp

# Gatekeeper constraints
kubectl get constraints
kubectl get constrainttemplates

# Helm releases
helm list -A

# IAM role assumption (what ESO does each sync)
aws --endpoint-url=http://localhost:4566 sts assume-role --role-arn arn:aws:iam::000000000000:role/eso-role --role-session-name test

# ECR login
aws --endpoint-url=http://localhost:4566 ecr get-login-password | docker login --username AWS --password-stdin 000000000000.dkr.ecr.us-east-1.localhost.localstack.cloud:5100

Project Structure

.
├── charts/
│   └── webapp/                      # Helm chart — all K8s resources for the webapp
│       ├── Chart.yaml
│       ├── values.yaml              # Parameterized defaults (image, resources, ESO, Gatekeeper)
│       └── templates/
│           ├── _helpers.tpl
│           ├── deployment.yaml      # Hardened nginx deployment
│           ├── service.yaml         # NodePort 30080 → container 8080
│           ├── serviceaccount.yaml  # webapp-sa + zero-permission Role + RoleBinding
│           ├── eso-aws-credentials.yaml
│           ├── eso-secretstore.yaml
│           ├── eso-externalsecret.yaml
│           ├── eso-db-externalsecret.yaml
│           ├── gatekeeper-templates.yaml   # 5 ConstraintTemplates (hook weight 0)
│           └── gatekeeper-constraints.yaml # 5 Constraints (hook weight 5)
├── examples/
│   ├── hardened/                    # Reference YAML — the hardened manifests for kubesec scoring
│   │   ├── deployment.yaml
│   │   ├── service.yaml
│   │   ├── serviceaccount.yaml
│   │   └── namespace.yaml
│   ├── insecure/
│   │   └── secret-bad.yaml          # Teaching example: plaintext secrets (DO NOT USE)
│   └── reference/
│       └── gatekeeper-policies.yaml # Combined template+constraint reference file
├── registries.yaml                  # k3d registry mirror config for LocalStack ECR
└── terraform/
    ├── secrets/                     # Persistent — Secrets Manager secrets (prevent_destroy)
    ├── infra/                       # Persistent — KMS, IAM, ECR, RDS, CloudWatch, SNS
    └── cluster/                     # Disposable — k3d cluster + 3 Helm releases
        ├── cluster.tf               # k3d cluster create/destroy
        ├── gatekeeper.tf            # Gatekeeper Helm chart
        ├── eso.tf                   # ESO Helm chart (with extraEnv for LocalStack)
        ├── webapp.tf                # Webapp Helm chart (single helm_release)
        ├── providers.tf             # Helm + null + time providers
        └── outputs.tf               # App URL, Helm release name

About

Kubernetes security lab: OPA Gatekeeper admission control, kubesec static analysis, ESO secret injection, LocalStack AWS — automated with Terraform

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages