diff --git a/deployment/helm/node-feature-discovery/Chart.lock b/deployment/helm/node-feature-discovery/Chart.lock new file mode 100644 index 000000000..a539c7695 --- /dev/null +++ b/deployment/helm/node-feature-discovery/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: spire + repository: https://spiffe.github.io/helm-charts-hardened/ + version: 0.24.1 +digest: sha256:f3b4dc973a59682bf3aa5ca9b53322f57935dd093081e82a37b8082e00becbe9 +generated: "2024-12-20T16:52:40.180416+01:00" diff --git a/deployment/helm/node-feature-discovery/Chart.yaml b/deployment/helm/node-feature-discovery/Chart.yaml index 553fc3c07..c18e36c77 100644 --- a/deployment/helm/node-feature-discovery/Chart.yaml +++ b/deployment/helm/node-feature-discovery/Chart.yaml @@ -13,3 +13,8 @@ keywords: - node-labels type: application version: 0.2.1 +dependencies: + - name: spire + version: 0.24.1 + repository: https://spiffe.github.io/helm-charts-hardened/ + condition: spire.enabled diff --git a/deployment/helm/node-feature-discovery/charts/spire-0.24.1.tgz b/deployment/helm/node-feature-discovery/charts/spire-0.24.1.tgz new file mode 100644 index 000000000..2ed7d582e Binary files /dev/null and b/deployment/helm/node-feature-discovery/charts/spire-0.24.1.tgz differ diff --git a/deployment/helm/node-feature-discovery/templates/master.yaml b/deployment/helm/node-feature-discovery/templates/master.yaml index 7756b9e9f..1352171ce 100644 --- a/deployment/helm/node-feature-discovery/templates/master.yaml +++ b/deployment/helm/node-feature-discovery/templates/master.yaml @@ -145,11 +145,25 @@ spec: {{- with .Values.master.extraArgs }} {{- toYaml . | nindent 12 }} {{- end }} + {{- if .Values.spire.enabled }} + - "-enable-spiffe" + {{- end }} volumeMounts: + {{- if .Values.spire.enabled }} + - name: spire-agent-socket + mountPath: /run/spire/agent-sockets/api.sock + readOnly: true + {{- end }} - name: nfd-master-conf mountPath: "/etc/kubernetes/node-feature-discovery" readOnly: true volumes: + {{- if .Values.spire.enabled }} + - name: spire-agent-socket + hostPath: + path: /run/spire/agent-sockets/api.sock + type: Socket + {{- end }} - name: nfd-master-conf configMap: name: {{ include "node-feature-discovery.fullname" . }}-master-conf diff --git a/deployment/helm/node-feature-discovery/templates/worker.yaml b/deployment/helm/node-feature-discovery/templates/worker.yaml index b8399f098..a5a0134ea 100644 --- a/deployment/helm/node-feature-discovery/templates/worker.yaml +++ b/deployment/helm/node-feature-discovery/templates/worker.yaml @@ -110,10 +110,18 @@ spec: {{- with .Values.worker.extraArgs }} {{- toYaml . | nindent 8 }} {{- end }} + {{- if .Values.spire.enabled }} + - "-enable-spiffe" + {{- end }} ports: - containerPort: {{ .Values.worker.port | default "8080"}} name: http volumeMounts: + {{- if .Values.spire.enabled }} + - name: spire-agent-socket + mountPath: /run/spire/agent-sockets/api.sock + readOnly: true + {{- end }} - name: host-boot mountPath: "/host-boot" readOnly: true @@ -144,6 +152,12 @@ spec: mountPath: "/etc/kubernetes/node-feature-discovery" readOnly: true volumes: + {{- if .Values.spire.enabled }} + - name: spire-agent-socket + hostPath: + path: /run/spire/agent-sockets/api.sock + type: Socket + {{- end }} - name: host-boot hostPath: path: "/boot" diff --git a/deployment/helm/node-feature-discovery/values.yaml b/deployment/helm/node-feature-discovery/values.yaml index d71b7bb5b..b9951fdbe 100644 --- a/deployment/helm/node-feature-discovery/values.yaml +++ b/deployment/helm/node-feature-discovery/values.yaml @@ -1,9 +1,9 @@ image: - repository: gcr.io/k8s-staging-nfd/node-feature-discovery + repository: docker.io/ahmedgrati/node-feature-discovery # This should be set to 'IfNotPresent' for released version pullPolicy: Always # tag, if defined will use the given image tag, else Chart.AppVersion will be used - # tag + tag: v0.18.0-devel-105-gb1d33c2b2-dirty imagePullSecrets: [] nameOverride: "" @@ -574,3 +574,57 @@ prometheus: enable: false scrapeInterval: 10s labels: {} + +spire: + enabled: true + global: + spire: + clusterName: "nfd" + trustDomain: "nfd.k8s-sigs.io" + system: + name: "spire-system" + create: false + server: + name: "spire-server" + create: false + spire-agent: + nameOverride: "spire-agent" + kubeletConnectByHostname: "true" + server: + address: "nfd-spire-server.nfd" + workloadAttestors: + unix: + enabled: true + spire-server: + nameOverride: "spire-server" + controllerManager: + enabled: true + identities: + clusterStaticEntries: + node: + parentID: spiffe://nfd.k8s-sigs.io/spire/server + spiffeID: spiffe://nfd.k8s-sigs.io/root + selectors: + - k8s_psat:agent_ns:nfd + - k8s_psat:agent_sa:nfd-agent + - k8s_psat:cluster:nfd + nfd: + parentID: spiffe://nfd.k8s-sigs.io/root + spiffeID: spiffe://nfd.k8s-sigs.io/worker + selectors: + - k8s:pod-label:app.kubernetes.io/name:node-feature-discovery + + + caSubject: + commonName: "nfd.k8s-sigs.io" + country: "US" + organization: "SPIFFE" + + upstream: + enabled: false + spiffe-csi-driver: + enabled: false + spiffe-oidc-discovery-provider: + enabled: false + tornjak-frontend: + enabled: false diff --git a/docs/reference/master-commandline-reference.md b/docs/reference/master-commandline-reference.md index e485c7841..feb005d57 100644 --- a/docs/reference/master-commandline-reference.md +++ b/docs/reference/master-commandline-reference.md @@ -306,3 +306,19 @@ Example: ```bash nfd-master -resync-period=2h ``` + +### -enable-spiffe + +the `-enable-spiffe` flag enables SPIFFE verification for the created NodeFeature +objects created by the worker. When enabled, master verifies the signature that +is put on the annotations part of the NodeFeature object, and updates +Kubernetes nodes if the signature is verified. The feature should be enabled, +after deploying SPIFFE, and you can do it through the Helm chart. + +Default: false. + +Example: + +```bash +nfd-master -enable-spiffe +``` diff --git a/docs/reference/worker-commandline-reference.md b/docs/reference/worker-commandline-reference.md index 6d281e154..6f4511c14 100644 --- a/docs/reference/worker-commandline-reference.md +++ b/docs/reference/worker-commandline-reference.md @@ -273,3 +273,19 @@ Default: 0 Comma-separated list of `pattern=N` settings for file-filtered logging. Default: *empty* + +### -enable-spiffe + +the `-enable-spiffe` flag enables signing NodeFeature spec on the worker side +and puts the signature in the annotations side of the NodeFeature object. +The signature is verified afterwards by the master. The feature +should be enabled, after deploying SPIFFE, and you can do it through +the Helm chart. + +Default: false. + +Example: + +```bash +nfd-master -enable-spiffe +``` diff --git a/go.mod b/go.mod index 4969bc977..8dafdb239 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/prometheus/client_golang v1.21.0 github.com/smartystreets/goconvey v1.8.1 github.com/spf13/cobra v1.9.1 + github.com/spiffe/go-spiffe/v2 v2.5.0 github.com/stretchr/testify v1.10.0 github.com/vektra/errors v0.0.0-20140903201135-c64d83aba85a golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 @@ -71,6 +72,7 @@ require ( github.com/euank/go-kmsg-parser v2.0.0+incompatible // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-jose/go-jose/v4 v4.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect @@ -131,6 +133,7 @@ require ( github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/zeebo/errs v1.4.0 // indirect go.etcd.io/etcd/api/v3 v3.5.16 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.16 // indirect go.etcd.io/etcd/client/v3 v3.5.16 // indirect diff --git a/go.sum b/go.sum index 0000c319a..c055dca82 100644 --- a/go.sum +++ b/go.sum @@ -76,6 +76,8 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/ github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= +github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -123,27 +125,6 @@ github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/Z github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= -github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= -github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= -github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= -github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= -github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= -github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= -github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= -github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= -github.com/googleapis/gax-go/v2 v2.10.0/go.mod h1:4UOEnMCrxsSqQ940WnTiD6qJ63le2ev3xfyagutxiPw= -github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= -github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= @@ -276,6 +257,8 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -283,7 +266,6 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -301,6 +283,8 @@ github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chq github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= go.etcd.io/etcd/api/v3 v3.5.16 h1:WvmyJVbjWqK4R1E+B12RRHz3bRGy9XVfh++MgbN+6n0= diff --git a/pkg/nfd-master/nfd-master.go b/pkg/nfd-master/nfd-master.go index 1240ebb52..029893a59 100644 --- a/pkg/nfd-master/nfd-master.go +++ b/pkg/nfd-master/nfd-master.go @@ -51,12 +51,6 @@ import ( "sigs.k8s.io/yaml" nfdclientset "sigs.k8s.io/node-feature-discovery/api/generated/clientset/versioned" - klogutils "sigs.k8s.io/node-feature-discovery/pkg/utils/klog" - spiffe "sigs.k8s.io/node-feature-discovery/pkg/utils/spiffe" - - taintutils "k8s.io/kubernetes/pkg/util/taints" - "sigs.k8s.io/yaml" - "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1" nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1" "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/nodefeaturerule" @@ -64,11 +58,12 @@ import ( nfdfeatures "sigs.k8s.io/node-feature-discovery/pkg/features" "sigs.k8s.io/node-feature-discovery/pkg/utils" klogutils "sigs.k8s.io/node-feature-discovery/pkg/utils/klog" + spiffe "sigs.k8s.io/node-feature-discovery/pkg/utils/spiffe" "sigs.k8s.io/node-feature-discovery/pkg/version" ) // SocketPath specifies Spiffe Socket Path -const SocketPath = "unix:///run/spire/sockets/agent.sock" +const SocketPath = "unix:///run/spire/agent-sockets/api.sock" // Labels are a Kubernetes representation of discovered features. type Labels map[string]string @@ -219,12 +214,6 @@ func NewNfdMaster(opts ...NfdMasterOption) (NfdMaster, error) { nfd.updaterPool = newUpdaterPool(nfd) - spiffeClient, err := spiffe.NewSpiffeClient(SocketPath) - if err != nil { - return nfd, err - } - nfd.spiffeClient = spiffeClient - return nfd, nil } @@ -308,6 +297,14 @@ func (m *nfdMaster) Run() error { } } + if m.config.EnableSpiffe { + spiffeClient, err := spiffe.NewSpiffeClient(SocketPath) + if err != nil { + return err + } + m.spiffeClient = spiffeClient + } + httpMux := http.NewServeMux() // Register to metrics server @@ -642,6 +639,14 @@ func (m *nfdMaster) getAndMergeNodeFeatures(nodeName string) (*nfdv1alpha1.NodeF return filteredObjs[i].Namespace < filteredObjs[j].Namespace }) + // If spiffe is enabled, we should filter out the non verified NFD objects + if m.config.EnableSpiffe { + filteredObjs, err = m.getVerifiedNFDObjects(filteredObjs) + if err != nil { + return &nfdv1alpha1.NodeFeature{}, err + } + } + if len(filteredObjs) > 0 { // Merge in features // @@ -697,55 +702,6 @@ func (m *nfdMaster) nfdAPIUpdateOneNode(cli k8sclient.Interface, node *corev1.No return fmt.Errorf("failed to merge NodeFeature objects for node %q: %w", node.Name, err) } - // Sort our objects - sort.Slice(objs, func(i, j int) bool { - // Objects in our nfd namespace gets into the beginning of the list - if objs[i].Namespace == m.namespace && objs[j].Namespace != m.namespace { - return true - } - if objs[i].Namespace != m.namespace && objs[j].Namespace == m.namespace { - return false - } - // After the nfd namespace, sort objects by their name - if objs[i].Name != objs[j].Name { - return objs[i].Name < objs[j].Name - } - // Objects with the same name are sorted by their namespace - return objs[i].Namespace < objs[j].Namespace - }) - - // If spiffe is enabled, we should filter out the non verified NFD objects - if m.config.EnableSpiffe { - objs, err = m.getVerifiedNFDObjects(objs) - if err != nil { - return err - } - } - - klog.V(1).InfoS("processing of node initiated by NodeFeature API", "nodeName", node.Name) - - features := nfdv1alpha1.NewNodeFeatureSpec() - - if len(objs) > 0 { - // Merge in features - // - // NOTE: changing the rule api to support handle multiple objects instead - // of merging would probably perform better with lot less data to copy. - features = objs[0].Spec.DeepCopy() - if m.config.AutoDefaultNs { - features.Labels = addNsToMapKeys(features.Labels, nfdv1alpha1.FeatureLabelNs) - } - for _, o := range objs[1:] { - s := o.Spec.DeepCopy() - if m.config.AutoDefaultNs { - s.Labels = addNsToMapKeys(s.Labels, nfdv1alpha1.FeatureLabelNs) - } - s.MergeInto(features) - } - - klog.V(4).InfoS("merged nodeFeatureSpecs", "newNodeFeatureSpec", utils.DelayedDumper(features)) - } - // Update node labels et al. This may also mean removing all NFD-owned // labels (et al.), for example in the case no NodeFeature objects are // present. @@ -1469,16 +1425,22 @@ func (m *nfdMaster) getVerifiedNFDObjects(objs []*v1alpha1.NodeFeature) ([]*v1al } for _, obj := range objs { - isSignatureVerified, err := spiffe.VerifyDataSignature(obj.Spec, obj.Annotations["signature"], workerPrivateKey, workerPublicKey) + spiffeObj := spiffe.SpiffeObject{ + Spec: obj.Spec, + Name: obj.Name, + Namespace: obj.Namespace, + Labels: obj.Labels, + } + isSignatureVerified, err := spiffe.VerifyDataSignature(spiffeObj, obj.Annotations["signature"], workerPrivateKey, workerPublicKey) if err != nil { return nil, fmt.Errorf("failed to verify NodeFeature signature: %w", err) } if isSignatureVerified { - klog.InfoS("NodeFeature verified", "NodeFeature name", obj.Name) + klog.InfoS("NodeFeature verified", "nodefeature", klog.KObj(obj)) verifiedObjects = append(verifiedObjects, obj) } else { - klog.InfoS("NodeFeature not verified, skipping...", "NodeFeature name", obj.Name) + klog.InfoS("NodeFeature not verified, skipping...", "nodefeature", klog.KObj(obj)) } } return verifiedObjects, nil diff --git a/pkg/nfd-worker/nfd-worker.go b/pkg/nfd-worker/nfd-worker.go index e98127d0f..c547bba2f 100644 --- a/pkg/nfd-worker/nfd-worker.go +++ b/pkg/nfd-worker/nfd-worker.go @@ -17,8 +17,6 @@ limitations under the License. package nfdworker import ( - "crypto/tls" - "crypto/x509" b64 "encoding/base64" "encoding/json" "fmt" @@ -67,7 +65,7 @@ import ( ) // SocketPath specifies Spiffe Socket Path -const SocketPath = "unix:///run/spire/sockets/agent.sock" +const SocketPath = "unix:///run/spire/agent-sockets/api.sock" // NfdWorker is the interface for nfd-worker daemon type NfdWorker interface { @@ -198,12 +196,6 @@ func NewNfdWorker(opts ...NfdWorkerOption) (NfdWorker, error) { nfd.k8sClient = cli } - spiffeClient, err := spiffe.NewSpiffeClient(SocketPath) - if err != nil { - return nfd, err - } - nfd.spiffeClient = spiffeClient - return nfd, nil } @@ -326,6 +318,14 @@ func (w *nfdWorker) Run() error { httpMux.Handle("/metrics", promhttp.HandlerFor(promRegistry, promhttp.HandlerOpts{})) registerVersion(version.Get()) + if w.config.Core.EnableSpiffe { + spiffeClient, err := spiffe.NewSpiffeClient(SocketPath) + if err != nil { + return err + } + w.spiffeClient = spiffeClient + } + err = w.runFeatureDiscovery() if err != nil { return err @@ -654,6 +654,7 @@ func (m *nfdWorker) updateNodeFeatureObject(labels Labels) error { Annotations: map[string]string{nfdv1alpha1.WorkerVersionAnnotation: version.Get()}, Labels: map[string]string{nfdv1alpha1.NodeFeatureObjNodeNameLabel: nodename}, OwnerReferences: m.ownerReference, + Namespace: namespace, }, Spec: nfdv1alpha1.NodeFeatureSpec{ Features: *features, @@ -664,8 +665,7 @@ func (m *nfdWorker) updateNodeFeatureObject(labels Labels) error { // If Spiffe is enabled, we add the signature to the annotations section if m.config.Core.EnableSpiffe { - err = m.signNodeFeatureCR(nfr) - if err != nil { + if err = m.signNodeFeatureCR(nfr); err != nil { return err } } @@ -761,7 +761,13 @@ func (m *nfdWorker) signNodeFeatureCR(nfr *nfdv1alpha1.NodeFeature) error { return fmt.Errorf("error while getting worker keys: %w", err) } - signature, err := spiffe.SignData(nfr.Spec, workerPrivateKey) + spiffeObject := spiffe.SpiffeObject{ + Spec: nfr.Spec, + Name: nfr.Name, + Namespace: nfr.Namespace, + Labels: nfr.Labels, + } + signature, err := spiffe.SignData(spiffeObject, workerPrivateKey) if err != nil { return fmt.Errorf("failed to sign CRD data using Spiffe: %w", err) diff --git a/pkg/utils/spiffe/spiffe.go b/pkg/utils/spiffe/spiffe.go index 542f4bde1..40faef809 100644 --- a/pkg/utils/spiffe/spiffe.go +++ b/pkg/utils/spiffe/spiffe.go @@ -3,7 +3,9 @@ Copyright 2024 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -11,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package utils +package spiffe import ( "context" @@ -25,15 +27,26 @@ import ( "fmt" "github.com/spiffe/go-spiffe/v2/workloadapi" + + nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1" ) +type SpiffeObject struct { + Spec nfdv1alpha1.NodeFeatureSpec + Name string + Namespace string + Labels map[string]string +} + // WorkerSpiffeID is the SpiffeID of the worker -const WorkerSpiffeID = "spiffe://nfd.com/worker" +const WorkerSpiffeID = "spiffe://nfd.k8s-sigs.io/worker" type SpiffeClient struct { WorkloadApiClient workloadapi.Client } +var hash_signature_cache = map[string][]byte{} + func NewSpiffeClient(socketPath string) (*SpiffeClient, error) { spiffeClient := SpiffeClient{} workloadApiClient, err := workloadapi.New(context.Background(), workloadapi.WithAddr(socketPath)) @@ -44,33 +57,38 @@ func NewSpiffeClient(socketPath string) (*SpiffeClient, error) { return &spiffeClient, nil } -func SignData(data interface{}, privateKey crypto.Signer) ([]byte, error) { +func SignData(data SpiffeObject, privateKey crypto.Signer) ([]byte, error) { stringifyData, err := json.Marshal(data) if err != nil { return []byte{}, err } dataHash := sha256.Sum256([]byte(stringifyData)) + if signature, ok := hash_signature_cache[string(dataHash[:])]; ok { + return signature, nil + } + var signedData []byte switch t := privateKey.(type) { case *rsa.PrivateKey: - signedData, err := rsa.SignPKCS1v15(rand.Reader, privateKey.(*rsa.PrivateKey), crypto.SHA256, dataHash[:]) + signedData, err = rsa.SignPKCS1v15(rand.Reader, privateKey.(*rsa.PrivateKey), crypto.SHA256, dataHash[:]) if err != nil { return []byte{}, err } - return signedData, nil case *ecdsa.PrivateKey: - signedData, err := ecdsa.SignASN1(rand.Reader, privateKey.(*ecdsa.PrivateKey), dataHash[:]) + signedData, err = ecdsa.SignASN1(rand.Reader, privateKey.(*ecdsa.PrivateKey), dataHash[:]) if err != nil { return []byte{}, err } - return signedData, nil default: return nil, fmt.Errorf("unknown private key type: %v", t) } + + hash_signature_cache[string(dataHash[:])] = signedData + return signedData, nil } -func VerifyDataSignature(data interface{}, signedData string, privateKey crypto.Signer, publicKey crypto.PublicKey) (bool, error) { +func VerifyDataSignature(data SpiffeObject, signedData string, privateKey crypto.Signer, publicKey crypto.PublicKey) (bool, error) { stringifyData, err := json.Marshal(data) if err != nil { return false, err @@ -83,10 +101,6 @@ func VerifyDataSignature(data interface{}, signedData string, privateKey crypto. dataHash := sha256.Sum256([]byte(stringifyData)) - if err != nil { - return false, err - } - switch t := privateKey.(type) { case *rsa.PrivateKey: err = rsa.VerifyPKCS1v15(publicKey.(*rsa.PublicKey), crypto.SHA256, dataHash[:], decodedSignature) @@ -112,7 +126,7 @@ func (s *SpiffeClient) GetWorkerKeys() (crypto.Signer, crypto.PublicKey, error) for _, svid := range svids { if svid.ID.String() == WorkerSpiffeID { - return svid.PrivateKey, svid.PrivateKey.Public, nil + return svid.PrivateKey, svid.PrivateKey.Public(), nil } } diff --git a/pkg/utils/spiffe/spiffe_test.go b/pkg/utils/spiffe/spiffe_test.go index 8fc9f17e2..3fe6a5a02 100644 --- a/pkg/utils/spiffe/spiffe_test.go +++ b/pkg/utils/spiffe/spiffe_test.go @@ -3,7 +3,9 @@ Copyright 2024 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -11,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package utils +package spiffe import ( "crypto" @@ -76,20 +78,31 @@ func TestVerify(t *testing.T) { } for _, tt := range tc { - signedData, err := SignData(spec, tt.privateKey) + spiffeObj := SpiffeObject{ + Spec: spec, + Name: "test", + Namespace: "test", + Labels: map[string]string{ + "random": "test", + }, + } + signedData, err := SignData(spiffeObj, tt.privateKey) assert.NoError(t, err) - isVerified, err := VerifyDataSignature(spec, b64.StdEncoding.EncodeToString(signedData), tt.privateKey, tt.publicKey) + isVerified, err := VerifyDataSignature(spiffeObj, b64.StdEncoding.EncodeToString(signedData), tt.privateKey, tt.publicKey) assert.NoError(t, err) assert.True(t, isVerified) signedData = append(signedData, "random"...) - isVerified, err = VerifyDataSignature(spec, b64.StdEncoding.EncodeToString(signedData), tt.privateKey, tt.publicKey) + isVerified, err = VerifyDataSignature(spiffeObj, b64.StdEncoding.EncodeToString(signedData), tt.privateKey, tt.publicKey) if tt.wantErr { assert.Error(t, err) } else { assert.False(t, isVerified) } + + // invalidateCache + hash_signature_cache = map[string][]byte{} } } @@ -113,7 +126,56 @@ func TestSignData(t *testing.T) { } for _, tt := range tc { - _, err := SignData(spec, tt.privateKey) + spiffeObj := SpiffeObject{ + Spec: spec, + Name: "test", + Namespace: "test", + Labels: map[string]string{ + "random": "test", + }, + } + _, err := SignData(spiffeObj, tt.privateKey) assert.NoError(t, err) + + // invalidate cache + hash_signature_cache = map[string][]byte{} + } +} + +func TestSignCached(t *testing.T) { + rsaPrivateKey, _ := mockWorkerRSAPrivateKey() + ecdsaPrivateKey, _ := mockWorkerECDSAPrivateKey() + spec := mockNFRSpec() + + tc := []struct { + name string + privateKey crypto.Signer + }{ + { + name: "RSA Keys", + privateKey: rsaPrivateKey, + }, + { + name: "ECDSA Keys", + privateKey: ecdsaPrivateKey, + }, + } + + for _, tt := range tc { + spiffeObj := SpiffeObject{ + Spec: spec, + Name: "test", + Namespace: "test", + Labels: map[string]string{ + "random": "test", + }, + } + firstSignature, err := SignData(spiffeObj, tt.privateKey) + assert.NoError(t, err) + + secondSignature, err := SignData(spiffeObj, tt.privateKey) + assert.NoError(t, err) + + assert.Equal(t, string(firstSignature[:]), string(secondSignature[:])) } }