Skip to content

Commit 7607d00

Browse files
authored
Merge pull request #514 from dragonflydb/network-policy-update
fix(security): harden default NetworkPolicy for client and admin ports
2 parents c35d5d8 + efb1033 commit 7607d00

10 files changed

Lines changed: 222 additions & 45 deletions

File tree

charts/dragonfly-operator/templates/deployment.yaml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ spec:
6767
{{- toYaml .Values.rbacProxy.resources | nindent 12 }}
6868
{{- end }}
6969
- name: manager
70+
command:
71+
- /manager
7072
args:
7173
- --leader-elect
7274
{{- if .Values.dragonflyImage }}
@@ -78,8 +80,11 @@ spec:
7880
{{- with .Values.manager.extraArgs}}
7981
{{- toYaml . | nindent 12 }}
8082
{{- end }}
81-
command:
82-
- /manager
83+
env:
84+
- name: POD_NAMESPACE
85+
valueFrom:
86+
fieldRef:
87+
fieldPath: metadata.namespace
8388
securityContext:
8489
{{- toYaml .Values.manager.securityContext | nindent 12 }}
8590
image: "{{ .Values.manager.image.repository }}:{{ .Values.manager.image.tag | default .Chart.AppVersion }}"

cmd/main.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,20 @@ func main() {
145145

146146
defer eventBroadcaster.Shutdown()
147147

148+
operatorNamespace := getOperatorNamespace()
149+
if operatorNamespace != "" {
150+
setupLog.Info(fmt.Sprintf("Operator namespace: %s", operatorNamespace))
151+
} else {
152+
setupLog.Info("Operator namespace could not be determined; admin port NetworkPolicy will be restricted to same-namespace only")
153+
}
154+
148155
if err = (&controller.DragonflyReconciler{
149156
Reconciler: controller.Reconciler{
150157
Client: mgr.GetClient(),
151158
Scheme: mgr.GetScheme(),
152159
EventRecorder: eventRecorder,
153160
DefaultDragonflyImage: dragonflyImage,
161+
OperatorNamespace: operatorNamespace,
154162
},
155163
}).SetupWithManager(mgr); err != nil {
156164
setupLog.Error(err, "unable to create controller", "controller", "Dragonfly")
@@ -163,6 +171,7 @@ func main() {
163171
Scheme: mgr.GetScheme(),
164172
EventRecorder: eventRecorder,
165173
DefaultDragonflyImage: dragonflyImage,
174+
OperatorNamespace: operatorNamespace,
166175
},
167176
}).SetupWithManager(mgr); err != nil {
168177
setupLog.Error(err, "unable to create controller", "controller", "Health")
@@ -227,3 +236,21 @@ func addNamespacesToOpts(namespaces string, ops *ctrl.Options) error {
227236
}
228237
return nil
229238
}
239+
240+
const saNamespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
241+
242+
func getOperatorNamespace() string {
243+
return resolveOperatorNamespace(saNamespaceFile)
244+
}
245+
246+
func resolveOperatorNamespace(saFile string) string {
247+
if ns, ok := os.LookupEnv("POD_NAMESPACE"); ok && ns != "" {
248+
return ns
249+
}
250+
if data, err := os.ReadFile(saFile); err == nil {
251+
if ns := strings.TrimSpace(string(data)); ns != "" {
252+
return ns
253+
}
254+
}
255+
return ""
256+
}

cmd/main_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
)
8+
9+
func TestResolveOperatorNamespace(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
envValue *string // nil = unset, "" = set but empty
13+
fileBody string // written to a temp file; empty string means no file
14+
want string
15+
}{
16+
{
17+
name: "env var set",
18+
envValue: strPtr("operator-ns"),
19+
want: "operator-ns",
20+
},
21+
{
22+
name: "env var takes precedence over file",
23+
envValue: strPtr("from-env"),
24+
fileBody: "from-file",
25+
want: "from-env",
26+
},
27+
{
28+
name: "falls back to SA file when env unset",
29+
fileBody: "sa-namespace\n",
30+
want: "sa-namespace",
31+
},
32+
{
33+
name: "trims whitespace from SA file",
34+
fileBody: " my-ns \n",
35+
want: "my-ns",
36+
},
37+
{
38+
name: "empty env var ignored, falls back to file",
39+
envValue: strPtr(""),
40+
fileBody: "fallback-ns",
41+
want: "fallback-ns",
42+
},
43+
{
44+
name: "returns empty when nothing available",
45+
want: "",
46+
},
47+
{
48+
name: "empty SA file returns empty",
49+
fileBody: " \n",
50+
want: "",
51+
},
52+
}
53+
54+
for _, tt := range tests {
55+
t.Run(tt.name, func(t *testing.T) {
56+
t.Setenv("POD_NAMESPACE", "")
57+
os.Unsetenv("POD_NAMESPACE")
58+
59+
if tt.envValue != nil {
60+
t.Setenv("POD_NAMESPACE", *tt.envValue)
61+
}
62+
63+
saFile := filepath.Join(t.TempDir(), "namespace")
64+
if tt.fileBody != "" {
65+
if err := os.WriteFile(saFile, []byte(tt.fileBody), 0o600); err != nil {
66+
t.Fatal(err)
67+
}
68+
}
69+
70+
got := resolveOperatorNamespace(saFile)
71+
if got != tt.want {
72+
t.Errorf("resolveOperatorNamespace() = %q, want %q", got, tt.want)
73+
}
74+
})
75+
}
76+
}
77+
78+
func strPtr(s string) *string { return &s }

config/manager/manager.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ spec:
7272
- --leader-elect
7373
image: controller:latest
7474
name: manager
75+
env:
76+
- name: POD_NAMESPACE
77+
valueFrom:
78+
fieldRef:
79+
fieldPath: metadata.namespace
7580
securityContext:
7681
allowPrivilegeEscalation: false
7782
capabilities:

internal/controller/base_controller.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type Reconciler struct {
3232
Scheme *runtime.Scheme
3333
EventRecorder record.EventRecorder
3434
DefaultDragonflyImage string
35+
OperatorNamespace string
3536
}
3637

3738
func (r *Reconciler) getDragonflyInstance(ctx context.Context, namespacedName types.NamespacedName, log logr.Logger) (*DragonflyInstance, error) {
@@ -49,5 +50,6 @@ func (r *Reconciler) getDragonflyInstance(ctx context.Context, namespacedName ty
4950
scheme: r.Scheme,
5051
eventRecorder: r.EventRecorder,
5152
defaultDragonflyImage: r.DefaultDragonflyImage,
53+
operatorNamespace: r.OperatorNamespace,
5254
}, nil
5355
}

internal/controller/dragonfly_instance.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ type DragonflyInstance struct {
5454
scheme *runtime.Scheme
5555
eventRecorder record.EventRecorder
5656
defaultDragonflyImage string
57+
operatorNamespace string
5758
redisClients map[string]*redis.Client
5859
}
5960

@@ -616,7 +617,7 @@ func (dfi *DragonflyInstance) hasMasterRole(ctx context.Context, redisClient *re
616617

617618
// reconcileResources creates or updates the dragonfly resources
618619
func (dfi *DragonflyInstance) reconcileResources(ctx context.Context) error {
619-
dfResources, err := resources.GenerateDragonflyResources(dfi.df, dfi.defaultDragonflyImage)
620+
dfResources, err := resources.GenerateDragonflyResources(dfi.df, dfi.defaultDragonflyImage, dfi.operatorNamespace)
620621
if err != nil {
621622
return fmt.Errorf("failed to generate dragonfly resources")
622623
}

internal/resources/const.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ const (
100100
OperatorControlPlaneLabelKey = "control-plane"
101101
OperatorControlPlaneLabelValue = "controller-manager"
102102

103+
// KubernetesNamespaceLabelKey is the well-known label automatically set on
104+
// namespaces by Kubernetes >= 1.21, used to pin NetworkPolicy selectors.
105+
KubernetesNamespaceLabelKey = "kubernetes.io/metadata.name"
106+
103107
// Probe ConfigMap suffixes — appended to df.Name
104108
LivenessProbeConfigMapSuffix = "liveness-probe"
105109
ReadinessProbeConfigMapSuffix = "readiness-probe"

internal/resources/image_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func TestGenerateDragonflyResources_ImageResolution(t *testing.T) {
4444
},
4545
}
4646

47-
objs, err := GenerateDragonflyResources(df, tt.defaultImage)
47+
objs, err := GenerateDragonflyResources(df, tt.defaultImage, "")
4848
assert.NoError(t, err)
4949

5050
var sts *appsv1.StatefulSet

internal/resources/resources.go

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ func generateProbeConfigMap(df *resourcesv1.Dragonfly, name, key, script string)
105105

106106
// GenerateDragonflyResources returns the resources required for a Dragonfly
107107
// Instance
108-
func GenerateDragonflyResources(df *resourcesv1.Dragonfly, defaultDragonflyImage string) ([]client.Object, error) {
108+
func GenerateDragonflyResources(df *resourcesv1.Dragonfly, defaultDragonflyImage, operatorNamespace string) ([]client.Object, error) {
109109
var resources []client.Object
110110

111111
image := df.Spec.Image
@@ -617,7 +617,7 @@ func GenerateDragonflyResources(df *resourcesv1.Dragonfly, defaultDragonflyImage
617617
}
618618

619619
if isNetworkPolicyEnabled(df) {
620-
np := generateNetworkPolicy(df)
620+
np := generateNetworkPolicy(df, operatorNamespace)
621621
resources = append(resources, &np)
622622
}
623623

@@ -628,7 +628,7 @@ func isNetworkPolicyEnabled(df *resourcesv1.Dragonfly) bool {
628628
return df.Spec.NetworkPolicyEnabled == nil || *df.Spec.NetworkPolicyEnabled
629629
}
630630

631-
func generateNetworkPolicy(df *resourcesv1.Dragonfly) networkingv1.NetworkPolicy {
631+
func generateNetworkPolicy(df *resourcesv1.Dragonfly, operatorNamespace string) networkingv1.NetworkPolicy {
632632
protocolTCP := corev1.ProtocolTCP
633633

634634
instanceSelector := map[string]string{
@@ -637,13 +637,33 @@ func generateNetworkPolicy(df *resourcesv1.Dragonfly) networkingv1.NetworkPolicy
637637
KubernetesAppNameLabelKey: KubernetesAppName,
638638
}
639639

640+
sameNamespacePeer := networkingv1.NetworkPolicyPeer{
641+
PodSelector: &metav1.LabelSelector{},
642+
}
643+
640644
clientPortRule := networkingv1.NetworkPolicyIngressRule{
641645
Ports: []networkingv1.NetworkPolicyPort{
642646
{
643647
Protocol: &protocolTCP,
644648
Port: &intstr.IntOrString{Type: intstr.Int, IntVal: DragonflyPort},
645649
},
646650
},
651+
From: []networkingv1.NetworkPolicyPeer{sameNamespacePeer},
652+
}
653+
654+
operatorPeer := networkingv1.NetworkPolicyPeer{
655+
PodSelector: &metav1.LabelSelector{
656+
MatchLabels: map[string]string{
657+
OperatorControlPlaneLabelKey: OperatorControlPlaneLabelValue,
658+
},
659+
},
660+
}
661+
if operatorNamespace != "" {
662+
operatorPeer.NamespaceSelector = &metav1.LabelSelector{
663+
MatchLabels: map[string]string{
664+
KubernetesNamespaceLabelKey: operatorNamespace,
665+
},
666+
}
647667
}
648668

649669
adminPortRule := networkingv1.NetworkPolicyIngressRule{
@@ -654,14 +674,7 @@ func generateNetworkPolicy(df *resourcesv1.Dragonfly) networkingv1.NetworkPolicy
654674
},
655675
},
656676
From: []networkingv1.NetworkPolicyPeer{
657-
{
658-
PodSelector: &metav1.LabelSelector{
659-
MatchLabels: map[string]string{
660-
OperatorControlPlaneLabelKey: OperatorControlPlaneLabelValue,
661-
},
662-
},
663-
NamespaceSelector: &metav1.LabelSelector{},
664-
},
677+
operatorPeer,
665678
{
666679
PodSelector: &metav1.LabelSelector{
667680
MatchLabels: instanceSelector,
@@ -680,6 +693,7 @@ func generateNetworkPolicy(df *resourcesv1.Dragonfly) networkingv1.NetworkPolicy
680693
Port: &intstr.IntOrString{Type: intstr.Int, IntVal: df.Spec.MemcachedPort},
681694
},
682695
},
696+
From: []networkingv1.NetworkPolicyPeer{sameNamespacePeer},
683697
}
684698
ingressRules = append(ingressRules, memcachedPortRule)
685699
}

0 commit comments

Comments
 (0)