Skip to content

Commit 85cd50a

Browse files
fix(config): harden kubernetes workload defaults (#248)
- Add default-deny NetworkPolicies for traefik, registry, and operator namespaces with scoped allow rules; remove broad HTTP/HTTPS egress from runtime namespace policies and add scoped sentinel + registry egress - Fix PSS labels: registry and mcp-sentinel namespaces use baseline enforce to accommodate hostPath/root-init exceptions; mcp-runtime uses restricted - Harden Traefik with non-root unprivileged ports (8000/8443), read-only rootfs, seccomp RuntimeDefault, forwardedHeaders.insecure=false, and probes - Harden registry deployment with seccomp, readOnlyRootFilesystem, drop ALL, and /tmp emptyDir volume - Add resource defaults and TCP probes to desiredDeployment; ensure same-namespace ingress is always allowed in platform NetworkPolicies - Disable trustForwardHeader on registry and sentinel forwardAuth middlewares Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2dfbaba commit 85cd50a

17 files changed

Lines changed: 652 additions & 42 deletions

config/ingress/base/kustomization.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ kind: Kustomization
33
resources:
44
- traefik.yaml
55
- dynamic-config.yaml
6+
- networkpolicy.yaml
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
apiVersion: networking.k8s.io/v1
2+
kind: NetworkPolicy
3+
metadata:
4+
name: traefik-default-deny
5+
namespace: traefik
6+
spec:
7+
podSelector: {}
8+
policyTypes:
9+
- Ingress
10+
- Egress
11+
---
12+
apiVersion: networking.k8s.io/v1
13+
kind: NetworkPolicy
14+
metadata:
15+
name: traefik-allow-ingress
16+
namespace: traefik
17+
spec:
18+
podSelector:
19+
matchLabels:
20+
app: traefik
21+
policyTypes:
22+
- Ingress
23+
ingress:
24+
- ports:
25+
- protocol: TCP
26+
port: 8000
27+
- protocol: TCP
28+
port: 8443
29+
---
30+
apiVersion: networking.k8s.io/v1
31+
kind: NetworkPolicy
32+
metadata:
33+
name: traefik-allow-egress
34+
namespace: traefik
35+
spec:
36+
podSelector:
37+
matchLabels:
38+
app: traefik
39+
policyTypes:
40+
- Egress
41+
egress:
42+
- to:
43+
- namespaceSelector:
44+
matchLabels:
45+
kubernetes.io/metadata.name: kube-system
46+
podSelector:
47+
matchLabels:
48+
k8s-app: kube-dns
49+
ports:
50+
- protocol: UDP
51+
port: 53
52+
- protocol: TCP
53+
port: 53
54+
- to:
55+
- namespaceSelector:
56+
matchLabels:
57+
kubernetes.io/metadata.name: registry
58+
- namespaceSelector:
59+
matchLabels:
60+
kubernetes.io/metadata.name: mcp-sentinel
61+
- namespaceSelector:
62+
matchLabels:
63+
kubernetes.io/metadata.name: mcp-servers
64+
- namespaceSelector:
65+
matchLabels:
66+
kubernetes.io/metadata.name: mcp-servers-org
67+
- namespaceSelector:
68+
matchLabels:
69+
kubernetes.io/metadata.name: mcp-servers-public
70+
- namespaceSelector:
71+
matchLabels:
72+
platform.mcpruntime.org/managed: "true"
73+
ports:
74+
- protocol: TCP
75+
port: 80
76+
- protocol: TCP
77+
port: 3000
78+
- protocol: TCP
79+
port: 5000
80+
- protocol: TCP
81+
port: 8080
82+
- protocol: TCP
83+
port: 8081
84+
- protocol: TCP
85+
port: 8082
86+
# Traefik watches Ingress and TLS Secret objects through the Kubernetes API.
87+
- ports:
88+
- protocol: TCP
89+
port: 443

config/ingress/base/traefik.yaml

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,30 @@ apiVersion: v1
22
kind: Namespace
33
metadata:
44
name: traefik
5+
labels:
6+
pod-security.kubernetes.io/enforce: restricted
7+
pod-security.kubernetes.io/audit: restricted
8+
pod-security.kubernetes.io/warn: restricted
59
---
610
apiVersion: v1
711
kind: Namespace
812
metadata:
913
name: registry
14+
labels:
15+
# hostpath overlays use a root chown initContainer; keep that exception
16+
# visible while enforcing the strongest compatible default.
17+
pod-security.kubernetes.io/enforce: baseline
18+
pod-security.kubernetes.io/audit: restricted
19+
pod-security.kubernetes.io/warn: restricted
1020
---
1121
apiVersion: v1
1222
kind: Namespace
1323
metadata:
1424
name: mcp-sentinel
25+
labels:
26+
pod-security.kubernetes.io/enforce: baseline
27+
pod-security.kubernetes.io/audit: restricted
28+
pod-security.kubernetes.io/warn: restricted
1529
---
1630
apiVersion: v1
1731
kind: Namespace
@@ -214,11 +228,11 @@ spec:
214228
ports:
215229
- name: web
216230
port: 80
217-
targetPort: 80
231+
targetPort: 8000
218232
protocol: TCP
219233
- name: websecure
220234
port: 443
221-
targetPort: 443
235+
targetPort: 8443
222236
protocol: TCP
223237
selector:
224238
app: traefik
@@ -239,34 +253,40 @@ spec:
239253
app: traefik
240254
spec:
241255
serviceAccountName: traefik
256+
securityContext:
257+
seccompProfile:
258+
type: RuntimeDefault
242259
containers:
243260
- name: traefik
244261
image: traefik:v2.10
245262
securityContext:
246263
allowPrivilegeEscalation: false
247-
readOnlyRootFilesystem: false
264+
readOnlyRootFilesystem: true
248265
runAsNonRoot: true
249266
runAsUser: 65534
250267
capabilities:
251268
drop:
252269
- ALL
253-
add:
254-
- NET_BIND_SERVICE
255270
args:
256271
- --providers.kubernetesingress=true
257272
- --providers.kubernetesingress.namespaces=registry,mcp-sentinel,mcp-servers,mcp-servers-org,mcp-servers-public
258-
- --entrypoints.web.address=:80
273+
- --entrypoints.web.address=:8000
274+
- --entrypoints.web.forwardedHeaders.insecure=false
259275
- --entrypoints.web.http.redirections.entryPoint.to=websecure
260276
- --entrypoints.web.http.redirections.entryPoint.scheme=https
261-
- --entrypoints.websecure.address=:443
277+
- --entrypoints.websecure.address=:8443
278+
- --entrypoints.websecure.forwardedHeaders.insecure=false
262279
- --entrypoints.websecure.http.tls=true
280+
- --entrypoints.traefik.address=:8080
281+
- --ping=true
282+
- --ping.entryPoint=traefik
263283
- --providers.file.filename=/etc/traefik/dynamic/dynamic.yml
264284
- --providers.file.watch=true
265285
ports:
266286
- name: web
267-
containerPort: 80
287+
containerPort: 8000
268288
- name: websecure
269-
containerPort: 443
289+
containerPort: 8443
270290
- name: admin
271291
containerPort: 8080
272292
volumeMounts:
@@ -280,6 +300,18 @@ spec:
280300
limits:
281301
cpu: 500m
282302
memory: 256Mi
303+
readinessProbe:
304+
httpGet:
305+
path: /ping
306+
port: admin
307+
initialDelaySeconds: 5
308+
periodSeconds: 10
309+
livenessProbe:
310+
httpGet:
311+
path: /ping
312+
port: admin
313+
initialDelaySeconds: 15
314+
periodSeconds: 20
283315
volumes:
284316
- name: traefik-dynamic
285317
configMap:

config/ingress/overlays/http/deployment-args.patch.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44
- --providers.kubernetesingress=true
55
- --providers.kubernetesingress.namespaces=registry,mcp-sentinel,mcp-servers,mcp-servers-org,mcp-servers-public
66
- --entrypoints.web.address=:8000
7+
- --entrypoints.web.forwardedHeaders.insecure=false
78
- --entrypoints.websecure.address=:8443
9+
- --entrypoints.websecure.forwardedHeaders.insecure=false
10+
- --entrypoints.traefik.address=:8080
11+
- --ping=true
12+
- --ping.entryPoint=traefik
813
- --providers.file.filename=/etc/traefik/dynamic/dynamic.yml
914
- --providers.file.watch=true
1015
- op: replace

config/ingress/overlays/prod/traefik-no-redirect.yaml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
# Traefik base enables HTTP to HTTPS on :80, which breaks HTTP-01 ACME challenges
2-
# (Let's Encrypt). Use plain HTTP on :80; HTTPS on :443 remains.
1+
# Traefik base enables HTTP to HTTPS, which breaks HTTP-01 ACME challenges
2+
# (Let's Encrypt). Use plain HTTP; HTTPS remains on the secure entrypoint.
33
apiVersion: apps/v1
44
kind: Deployment
55
metadata:
@@ -13,8 +13,9 @@ spec:
1313
args:
1414
- --providers.kubernetesingress=true
1515
- --providers.kubernetesingress.namespaces=registry,mcp-sentinel,mcp-servers,mcp-servers-org,mcp-servers-public
16-
- --entrypoints.web.address=:80
17-
- --entrypoints.websecure.address=:443
16+
- --entrypoints.web.address=:8000
17+
- --entrypoints.websecure.address=:8443
1818
- --entrypoints.websecure.http.tls=true
19+
- --ping=true
1920
- --providers.file.filename=/etc/traefik/dynamic/dynamic.yml
2021
- --providers.file.watch=true

config/manager/kustomization.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
resources:
22
- manager.yaml
3+
- networkpolicy.yaml
34
- pdb.yaml

config/manager/manager.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ apiVersion: v1
22
kind: Namespace
33
metadata:
44
name: mcp-runtime
5+
labels:
6+
pod-security.kubernetes.io/enforce: restricted
7+
pod-security.kubernetes.io/audit: restricted
8+
pod-security.kubernetes.io/warn: restricted
59
---
610
apiVersion: apps/v1
711
kind: Deployment
@@ -19,6 +23,7 @@ spec:
1923
control-plane: controller-manager
2024
spec:
2125
serviceAccountName: mcp-runtime-operator-controller-manager
26+
automountServiceAccountToken: true
2227
securityContext:
2328
runAsNonRoot: true
2429
seccompProfile:

config/manager/networkpolicy.yaml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
apiVersion: networking.k8s.io/v1
2+
kind: NetworkPolicy
3+
metadata:
4+
name: mcp-runtime-default-deny
5+
namespace: mcp-runtime
6+
spec:
7+
podSelector: {}
8+
policyTypes:
9+
- Ingress
10+
- Egress
11+
---
12+
apiVersion: networking.k8s.io/v1
13+
kind: NetworkPolicy
14+
metadata:
15+
name: mcp-runtime-allow-egress
16+
namespace: mcp-runtime
17+
spec:
18+
podSelector:
19+
matchLabels:
20+
control-plane: controller-manager
21+
policyTypes:
22+
- Egress
23+
egress:
24+
- to:
25+
- namespaceSelector:
26+
matchLabels:
27+
kubernetes.io/metadata.name: kube-system
28+
podSelector:
29+
matchLabels:
30+
k8s-app: kube-dns
31+
ports:
32+
- protocol: UDP
33+
port: 53
34+
- protocol: TCP
35+
port: 53
36+
# The operator reconciles cluster resources through the Kubernetes API.
37+
- ports:
38+
- protocol: TCP
39+
port: 443
40+
- to:
41+
- namespaceSelector:
42+
matchLabels:
43+
kubernetes.io/metadata.name: registry
44+
ports:
45+
- protocol: TCP
46+
port: 5000

config/registry/base/deployment.yaml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ spec:
2020
runAsUser: 1000
2121
runAsGroup: 1000
2222
fsGroup: 1000
23+
seccompProfile:
24+
type: RuntimeDefault
2325
containers:
2426
- name: registry
2527
image: registry:2.8.3
@@ -30,7 +32,10 @@ spec:
3032
securityContext:
3133
allowPrivilegeEscalation: false
3234
runAsNonRoot: true
33-
readOnlyRootFilesystem: false
35+
readOnlyRootFilesystem: true
36+
capabilities:
37+
drop:
38+
- ALL
3439
env:
3540
- name: REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY
3641
value: /var/lib/registry
@@ -39,6 +44,8 @@ spec:
3944
volumeMounts:
4045
- name: registry-storage
4146
mountPath: /var/lib/registry
47+
- name: tmp
48+
mountPath: /tmp
4249
resources:
4350
requests:
4451
cpu: 100m
@@ -62,3 +69,5 @@ spec:
6269
- name: registry-storage
6370
persistentVolumeClaim:
6471
claimName: registry-storage
72+
- name: tmp
73+
emptyDir: {}

config/registry/base/kustomization.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ resources:
66
- deployment.yaml
77
- service.yaml
88
- ingress.yaml
9+
- networkpolicy.yaml
910
- pdb.yaml

0 commit comments

Comments
 (0)