Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions labs/lab7/k8s/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: juice-shop
namespace: juice-shop
labels: { app: juice-shop }
spec:
replicas: 1
selector:
matchLabels: { app: juice-shop }
template:
metadata:
labels: { app: juice-shop }
spec:
serviceAccountName: juice-shop-sa
automountServiceAccountToken: false
# Pod-level hardening (satisfies PSS 'restricted').
securityContext:
runAsNonRoot: true
runAsUser: 1000 # Juice Shop's default UID
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
initContainers:
# readOnlyRootFilesystem is immutable, but Juice Shop writes its SQLite DB
# into /juice-shop/data (which ALSO holds read-only seed under data/static)
# and copies files into /juice-shop/ftp at startup. A plain emptyDir would
# hide the seed, so we seed the emptyDirs from the image first, using the
# image's own node entrypoint (no shell needed on distroless).
- name: seed-writable-dirs
image: bkimminich/juice-shop@sha256:fd58bdc9745416afce8184ee0666278a436574633ea7880365153a63bfd418b0
args:
- "-e"
- "const fs=require('fs');for(const d of ['data','ftp'])fs.cpSync('/juice-shop/'+d,'/seed/'+d,{recursive:true});"
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests: { cpu: "50m", memory: "64Mi" }
limits: { cpu: "250m", memory: "256Mi" }
volumeMounts:
- { name: data, mountPath: /seed/data }
- { name: ftp, mountPath: /seed/ftp }
containers:
- name: juice-shop
image: bkimminich/juice-shop@sha256:fd58bdc9745416afce8184ee0666278a436574633ea7880365153a63bfd418b0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3000
# Container-level hardening (satisfies PSS 'restricted').
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests: { cpu: "50m", memory: "256Mi" }
limits: { cpu: "500m", memory: "512Mi" }
# Writable emptyDir volumes (seeded above) exactly where Juice Shop writes,
# leaving the rest of root read-only: /tmp scratch, /juice-shop/data (SQLite
# DB + seed), /juice-shop/ftp (startup file copies).
volumeMounts:
- { name: tmp, mountPath: /tmp }
- { name: data, mountPath: /juice-shop/data }
- { name: ftp, mountPath: /juice-shop/ftp }
readinessProbe:
httpGet: { path: /, port: 3000 }
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 24
livenessProbe:
httpGet: { path: /, port: 3000 }
initialDelaySeconds: 90
periodSeconds: 20
volumes:
- name: tmp
emptyDir: {}
- name: data
emptyDir: {}
- name: ftp
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: juice-shop
namespace: juice-shop
spec:
selector: { app: juice-shop }
ports:
- { port: 3000, targetPort: 3000 }
13 changes: 13 additions & 0 deletions labs/lab7/k8s/namespace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: v1
kind: Namespace
metadata:
name: juice-shop
labels:
# Pod Security Standards — Lecture 7 slide 11.
# enforce blocks creation of violating pods; warn logs to kubectl; audit logs to the audit log.
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: latest
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: latest
30 changes: 30 additions & 0 deletions labs/lab7/k8s/networkpolicy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: juice-shop-default-deny
namespace: juice-shop
spec:
# Selects the Juice Shop pods; both directions are default-deny unless allowed below.
podSelector:
matchLabels: { app: juice-shop }
policyTypes: [Ingress, Egress]
ingress:
# Allow app traffic to :3000 from ingress controllers / same namespace.
# (kubectl port-forward reaches the pod via the kubelet and is unaffected by NP.)
- from:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: ingress-nginx }
- podSelector: {}
ports:
- { protocol: TCP, port: 3000 }
egress:
# DNS to kube-system (CoreDNS) — UDP+TCP 53.
- to:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: kube-system }
ports:
- { protocol: UDP, port: 53 }
- { protocol: TCP, port: 53 }
# Outbound HTTPS only — nothing else.
- ports:
- { protocol: TCP, port: 443 }
9 changes: 9 additions & 0 deletions labs/lab7/k8s/serviceaccount.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: juice-shop-sa
namespace: juice-shop
# Anti-pattern fix (Lecture 7 slide 12 / slide 17): app pods should NOT get the
# default SA token mounted. A dedicated SA with token auto-mount disabled means
# a compromised container cannot talk to the Kubernetes API by default.
automountServiceAccountToken: false
43 changes: 43 additions & 0 deletions labs/lab7/policies/pod-hardening.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Conftest gate — refuse Deployments whose pod template is not hardened.
# Run: conftest test labs/lab7/k8s/deployment.yaml --policy labs/lab7/policies
#
# Conftest looks for `package main` by default. Written in Rego v1 syntax
# (if/contains), matching labs/lab9/policies/k8s-security.rego.
package main

# Helper: true if array arr contains value v
has_value(arr, v) if {
some i
arr[i] == v
}

# 1) pod-level runAsNonRoot must be true
deny contains msg if {
input.kind == "Deployment"
not input.spec.template.spec.securityContext.runAsNonRoot == true
msg := "pod securityContext.runAsNonRoot must be set to true"
}

# 2) every container must set readOnlyRootFilesystem: true
deny contains msg if {
input.kind == "Deployment"
c := input.spec.template.spec.containers[_]
not c.securityContext.readOnlyRootFilesystem == true
msg := sprintf("container %q must set readOnlyRootFilesystem: true", [c.name])
}

# 3) every container must set allowPrivilegeEscalation: false
deny contains msg if {
input.kind == "Deployment"
c := input.spec.template.spec.containers[_]
not c.securityContext.allowPrivilegeEscalation == false
msg := sprintf("container %q must set allowPrivilegeEscalation: false", [c.name])
}

# 4) every container must drop ALL capabilities
deny contains msg if {
input.kind == "Deployment"
c := input.spec.template.spec.containers[_]
not has_value(c.securityContext.capabilities.drop, "ALL")
msg := sprintf("container %q must drop ALL capabilities", [c.name])
}
Loading