Skip to content

Commit c08d5c2

Browse files
feat: add certMount.mode=perNode for live cert rotation (#20)
Add certMount configuration to the nebula DaemonSet supporting two modes: "shared" (default, unchanged init-container pattern) and "perNode" (direct Secret mount enabling live cert rotation via K8s Secret volume auto-updates). perNode mode is designed for fleet overlay topology where each cluster has one overlay identity. Secret name derived from release fullname, not K8s node names. Closes #19 Co-authored-by: privsim <excaliberswake@pm.me>
1 parent a0c4f6c commit c08d5c2

File tree

4 files changed

+172
-0
lines changed

4 files changed

+172
-0
lines changed

helm/disentangle/templates/nebula-daemonset.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{{- if .Values.nebula.enabled }}
2+
{{- $certMode := .Values.nebula.certMount.mode | default "shared" }}
23
apiVersion: apps/v1
34
kind: DaemonSet
45
metadata:
@@ -19,6 +20,7 @@ spec:
1920
spec:
2021
hostNetwork: true
2122
dnsPolicy: ClusterFirstWithHostNet
23+
{{- if eq $certMode "shared" }}
2224
initContainers:
2325
- name: cert-selector
2426
image: "{{ .Values.nebula.initImage.repository }}:{{ .Values.nebula.initImage.tag }}"
@@ -35,6 +37,7 @@ spec:
3537
readOnly: true
3638
- name: selected-certs
3739
mountPath: /selected
40+
{{- end }}
3841
containers:
3942
- name: nebula
4043
image: "{{ .Values.nebula.image.repository }}:{{ .Values.nebula.image.tag }}"
@@ -67,13 +70,19 @@ spec:
6770
- name: nebula-config
6871
configMap:
6972
name: {{ include "disentangle.fullname" . }}-nebula-config
73+
{{- if eq $certMode "shared" }}
7074
- name: all-certs
7175
secret:
7276
secretName: {{ .Values.nebula.certSecretName }}
7377
- name: selected-certs
7478
emptyDir:
7579
medium: Memory
7680
sizeLimit: 1Mi
81+
{{- else if eq $certMode "perNode" }}
82+
- name: selected-certs
83+
secret:
84+
secretName: {{ .Values.nebula.certMount.secretName | default (printf "%s%s" (.Values.nebula.certMount.secretPrefix | default "nebula-cert-") (include "disentangle.fullname" .)) }}
85+
{{- end }}
7786
- name: tun
7887
hostPath:
7988
path: /dev/net/tun

helm/disentangle/tests/nebula_daemonset_test.yaml

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,127 @@ tests:
206206
- lengthEqual:
207207
path: spec.template.spec.volumes
208208
count: 4
209+
210+
# --- certMount.mode=shared (explicit) tests ---
211+
212+
- it: should behave identically when certMount.mode is explicitly shared
213+
set:
214+
nebula.enabled: true
215+
nebula.certMount.mode: "shared"
216+
asserts:
217+
- lengthEqual:
218+
path: spec.template.spec.initContainers
219+
count: 1
220+
- lengthEqual:
221+
path: spec.template.spec.volumes
222+
count: 4
223+
- contains:
224+
path: spec.template.spec.volumes
225+
content:
226+
name: selected-certs
227+
emptyDir:
228+
medium: Memory
229+
sizeLimit: 1Mi
230+
231+
# --- certMount.mode=perNode tests ---
232+
233+
- it: perNode mode should not have initContainers
234+
set:
235+
nebula.enabled: true
236+
nebula.certMount.mode: "perNode"
237+
asserts:
238+
- isNull:
239+
path: spec.template.spec.initContainers
240+
241+
- it: perNode mode should have three volumes (no all-certs)
242+
set:
243+
nebula.enabled: true
244+
nebula.certMount.mode: "perNode"
245+
asserts:
246+
- lengthEqual:
247+
path: spec.template.spec.volumes
248+
count: 3
249+
250+
- it: perNode mode selected-certs volume should be a Secret (not emptyDir)
251+
set:
252+
nebula.enabled: true
253+
nebula.certMount.mode: "perNode"
254+
asserts:
255+
- contains:
256+
path: spec.template.spec.volumes
257+
content:
258+
name: selected-certs
259+
secret:
260+
secretName: nebula-cert-test-release-disentangle
261+
262+
- it: perNode mode should derive secretName from prefix + fullname by default
263+
set:
264+
nebula.enabled: true
265+
nebula.certMount.mode: "perNode"
266+
asserts:
267+
- contains:
268+
path: spec.template.spec.volumes
269+
content:
270+
name: selected-certs
271+
secret:
272+
secretName: nebula-cert-test-release-disentangle
273+
274+
- it: perNode mode should use custom secretName when set
275+
set:
276+
nebula.enabled: true
277+
nebula.certMount.mode: "perNode"
278+
nebula.certMount.secretName: "my-custom-nebula-secret"
279+
asserts:
280+
- contains:
281+
path: spec.template.spec.volumes
282+
content:
283+
name: selected-certs
284+
secret:
285+
secretName: my-custom-nebula-secret
286+
287+
- it: perNode mode should use custom secretPrefix when secretName is empty
288+
set:
289+
nebula.enabled: true
290+
nebula.certMount.mode: "perNode"
291+
nebula.certMount.secretPrefix: "overlay-cert-"
292+
asserts:
293+
- contains:
294+
path: spec.template.spec.volumes
295+
content:
296+
name: selected-certs
297+
secret:
298+
secretName: overlay-cert-test-release-disentangle
299+
300+
- it: perNode mode should preserve the same volumeMounts on the main container
301+
set:
302+
nebula.enabled: true
303+
nebula.certMount.mode: "perNode"
304+
asserts:
305+
- lengthEqual:
306+
path: spec.template.spec.containers[0].volumeMounts
307+
count: 3
308+
- contains:
309+
path: spec.template.spec.containers[0].volumeMounts
310+
content:
311+
name: selected-certs
312+
mountPath: /etc/nebula/certs
313+
readOnly: true
314+
315+
- it: perNode mode should still have nebula-config and tun volumes
316+
set:
317+
nebula.enabled: true
318+
nebula.certMount.mode: "perNode"
319+
asserts:
320+
- contains:
321+
path: spec.template.spec.volumes
322+
content:
323+
name: nebula-config
324+
configMap:
325+
name: test-release-disentangle-nebula-config
326+
- contains:
327+
path: spec.template.spec.volumes
328+
content:
329+
name: tun
330+
hostPath:
331+
path: /dev/net/tun
332+
type: CharDevice

helm/disentangle/values.schema.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,29 @@
554554
"description": "Name of the Kubernetes Secret containing Nebula certificates (created by 'launch mesh add', SOPS-encrypted).",
555555
"default": "nebula-certs"
556556
},
557+
"certMount": {
558+
"type": "object",
559+
"description": "Certificate mount mode for the Nebula sidecar.",
560+
"additionalProperties": false,
561+
"properties": {
562+
"mode": {
563+
"type": "string",
564+
"description": "Certificate mount mode. 'shared' uses init-container to select from shared Secret. 'perNode' mounts a dedicated Secret directly (enables live cert rotation).",
565+
"enum": ["shared", "perNode"],
566+
"default": "shared"
567+
},
568+
"secretName": {
569+
"type": "string",
570+
"description": "Exact Secret name for perNode mode. If empty, derived from secretPrefix + release fullname.",
571+
"default": ""
572+
},
573+
"secretPrefix": {
574+
"type": "string",
575+
"description": "Prefix for deriving Secret name in perNode mode when secretName is empty.",
576+
"default": "nebula-cert-"
577+
}
578+
}
579+
},
557580
"staticHostMap": {
558581
"type": "object",
559582
"description": "Maps VPN IPs to real addresses for lighthouse discovery. Keys are VPN IPs, values are real host:port addresses.",

helm/disentangle/values.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,22 @@ nebula:
127127
# Certificate secret name (created by launch mesh add, SOPS-encrypted)
128128
# Expected keys: ca.crt, <nodeName>.crt, <nodeName>.key for each K8s node
129129
certSecretName: "nebula-certs"
130+
# Certificate mount mode:
131+
# "shared" - (default) init-container copies per-node certs from a single
132+
# shared Secret (certSecretName) into an emptyDir. Requires pod
133+
# restart to pick up rotated certs.
134+
# "perNode" - each release mounts its own Secret directly at /etc/nebula/certs.
135+
# Secret name = certMount.secretName or secretPrefix + fullname.
136+
# Enables live cert rotation without pod restart.
137+
# Designed for fleet overlay topology (one overlay identity per cluster).
138+
certMount:
139+
mode: "shared"
140+
# Only used when mode=perNode. Override to set the exact Secret name.
141+
# Expected Secret keys: ca.crt, host.crt, host.key
142+
secretName: ""
143+
# Only used when mode=perNode and secretName is empty. Prefix concatenated
144+
# with the Helm release fullname to derive the Secret name.
145+
secretPrefix: "nebula-cert-"
130146
# Static host map: maps VPN IPs to real addresses for lighthouse discovery
131147
staticHostMap: {}
132148
# "10.42.0.1": "lighthouse.disentangle.network:4242"

0 commit comments

Comments
 (0)