1
0
Fork 0
mirror of https://github.com/kubernetes-sigs/node-feature-discovery.git synced 2025-03-31 04:04:51 +00:00
This commit is contained in:
TessaIO 2025-03-24 14:12:53 +01:00 committed by GitHub
commit 0fd78507f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 579 additions and 4 deletions

View file

@ -73,6 +73,8 @@ func main() {
args.Overrides.ResyncPeriod = overrides.ResyncPeriod
case "nfd-api-parallelism":
args.Overrides.NfdApiParallelism = overrides.NfdApiParallelism
case "enable-spiffe":
args.Overrides.EnableSpiffe = overrides.EnableSpiffe
}
})
@ -140,6 +142,8 @@ func initFlags(flagset *flag.FlagSet) (*master.Args, *master.ConfigOverrideArgs)
flagset.Var(overrides.ResyncPeriod, "resync-period", "Specify the NFD API controller resync period.")
overrides.NfdApiParallelism = flagset.Int("nfd-api-parallelism", 10, "Defines the maximum number of goroutines responsible of updating nodes. "+
"Can be used for the throttling mechanism.")
overrides.EnableSpiffe = flagset.Bool("enable-spiffe", false,
"Enables the Spiffe signature verification of created CRDs. This is still an EXPERIMENTAL feature.")
return args, overrides
}

View file

@ -93,6 +93,8 @@ func parseArgs(flags *flag.FlagSet, osArgs ...string) *worker.Args {
args.Overrides.LabelSources = overrides.LabelSources
case "no-owner-refs":
args.Overrides.NoOwnerRefs = overrides.NoOwnerRefs
case "enable-spiffe":
args.Overrides.EnableSpiffe = overrides.EnableSpiffe
}
})
@ -131,6 +133,8 @@ func initFlags(flagset *flag.FlagSet) (*worker.Args, *worker.ConfigOverrideArgs)
flagset.Var(overrides.LabelSources, "label-sources",
"Comma separated list of label sources. Special value 'all' enables all sources. "+
"Prefix the source name with '-' to disable it.")
overrides.EnableSpiffe = flagset.Bool("enable-spiffe", false,
"Enables the Spiffe signature verification of created CRDs. This is still an EXPERIMENTAL feature.")
return args, overrides
}

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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
```

View file

@ -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
```

3
go.mod
View file

@ -19,6 +19,7 @@ require (
github.com/prometheus/client_golang v1.21.1
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/net v0.37.0
@ -70,6 +71,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
@ -129,6 +131,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

6
go.sum
View file

@ -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=
@ -253,6 +255,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=
@ -277,6 +281,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=

View file

@ -51,15 +51,20 @@ import (
"sigs.k8s.io/yaml"
nfdclientset "sigs.k8s.io/node-feature-discovery/api/generated/clientset/versioned"
"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"
"sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/validate"
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/agent-sockets/api.sock"
// Labels are a Kubernetes representation of discovered features.
type Labels map[string]string
@ -92,6 +97,7 @@ type NFDConfig struct {
NfdApiParallelism int
Klog klogutils.KlogConfigOpts
Restrictions Restrictions
EnableSpiffe bool
}
// LeaderElectionConfig contains the configuration for leader election
@ -110,6 +116,7 @@ type ConfigOverrideArgs struct {
NoPublish *bool
ResyncPeriod *utils.DurationVal
NfdApiParallelism *int
EnableSpiffe *bool
}
// Args holds command line arguments
@ -149,7 +156,8 @@ type nfdMaster struct {
nfdClient nfdclientset.Interface
updaterPool *updaterPool
deniedNs
config *NFDConfig
config *NFDConfig
spiffeClient *spiffe.SpiffeClient
}
// NewNfdMaster creates a new NfdMaster server instance.
@ -247,7 +255,6 @@ func newDefaultConfig() *NFDConfig {
RetryPeriod: utils.DurationVal{Duration: time.Duration(2) * time.Second},
RenewDeadline: utils.DurationVal{Duration: time.Duration(10) * time.Second},
},
Klog: make(map[string]string),
Restrictions: Restrictions{
DisableLabels: false,
DisableExtendedResources: false,
@ -255,6 +262,8 @@ func newDefaultConfig() *NFDConfig {
AllowOverwrite: true,
DenyNodeFeatureLabels: false,
},
Klog: make(map[string]string),
EnableSpiffe: false,
}
}
@ -288,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
@ -622,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
//
@ -1190,6 +1215,9 @@ func (m *nfdMaster) configure(filepath string, overrides string) error {
if m.args.Overrides.NfdApiParallelism != nil {
c.NfdApiParallelism = *m.args.Overrides.NfdApiParallelism
}
if m.args.Overrides.EnableSpiffe != nil {
c.EnableSpiffe = *m.args.Overrides.EnableSpiffe
}
if c.NfdApiParallelism <= 0 {
return fmt.Errorf("the maximum number of concurrent labelers should be a non-zero positive number")
@ -1390,3 +1418,33 @@ func patchNode(cli k8sclient.Interface, nodeName string, patches []utils.JsonPat
func patchNodeStatus(cli k8sclient.Interface, nodeName string, patches []utils.JsonPatch) error {
return patchNode(cli, nodeName, patches, "status")
}
func (m *nfdMaster) getVerifiedNFDObjects(objs []*v1alpha1.NodeFeature) ([]*v1alpha1.NodeFeature, error) {
verifiedObjects := []*v1alpha1.NodeFeature{}
workerPrivateKey, workerPublicKey, err := m.spiffeClient.GetWorkerKeys()
if err != nil {
return verifiedObjects, err
}
for _, obj := range objs {
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", klog.KObj(obj))
verifiedObjects = append(verifiedObjects, obj)
} else {
klog.InfoS("NodeFeature not verified, skipping...", "nodefeature", klog.KObj(obj))
}
}
return verifiedObjects, nil
}

View file

@ -17,6 +17,7 @@ limitations under the License.
package nfdworker
import (
b64 "encoding/base64"
"encoding/json"
"fmt"
"net/http"
@ -47,6 +48,7 @@ import (
nfdclient "sigs.k8s.io/node-feature-discovery/api/generated/clientset/versioned"
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1"
"sigs.k8s.io/node-feature-discovery/pkg/utils"
spiffe "sigs.k8s.io/node-feature-discovery/pkg/utils/spiffe"
"sigs.k8s.io/node-feature-discovery/pkg/version"
"sigs.k8s.io/node-feature-discovery/source"
@ -64,6 +66,9 @@ import (
_ "sigs.k8s.io/node-feature-discovery/source/usb"
)
// SocketPath specifies Spiffe Socket Path
const SocketPath = "unix:///run/spire/agent-sockets/api.sock"
// NfdWorker is the interface for nfd-worker daemon
type NfdWorker interface {
Run() error
@ -85,6 +90,7 @@ type coreConfig struct {
Sources *[]string
LabelSources []string
SleepInterval utils.DurationVal
EnableSpiffe bool
}
type sourcesConfig map[string]source.Config
@ -111,6 +117,7 @@ type ConfigOverrideArgs struct {
NoOwnerRefs *bool
FeatureSources *utils.StringSliceVal
LabelSources *utils.StringSliceVal
EnableSpiffe *bool
}
type nfdWorker struct {
@ -124,6 +131,7 @@ type nfdWorker struct {
featureSources []source.FeatureSource
labelSources []source.LabelSource
ownerReference []metav1.OwnerReference
spiffeClient *spiffe.SpiffeClient
}
// This ticker can represent infinite and normal intervals.
@ -312,6 +320,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
@ -511,6 +527,9 @@ func (w *nfdWorker) configure(filepath string, overrides string) error {
if w.args.Overrides.LabelSources != nil {
c.Core.LabelSources = *w.args.Overrides.LabelSources
}
if w.args.Overrides.EnableSpiffe != nil {
c.Core.EnableSpiffe = *w.args.Overrides.EnableSpiffe
}
c.Core.sanitize()
@ -637,6 +656,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,
@ -645,6 +665,13 @@ func (m *nfdWorker) updateNodeFeatureObject(labels Labels) error {
}
klog.InfoS("creating NodeFeature object", "nodefeature", klog.KObj(nfr))
// If Spiffe is enabled, we add the signature to the annotations section
if m.config.Core.EnableSpiffe {
if err = m.signNodeFeatureCR(nfr); err != nil {
return err
}
}
nfrCreated, err := cli.NfdV1alpha1().NodeFeatures(namespace).Create(context.TODO(), nfr, metav1.CreateOptions{})
if err != nil {
return fmt.Errorf("failed to create NodeFeature object %q: %w", nfr.Name, err)
@ -663,6 +690,13 @@ func (m *nfdWorker) updateNodeFeatureObject(labels Labels) error {
Labels: labels,
}
if m.config.Core.EnableSpiffe {
err = m.signNodeFeatureCR(nfrUpdated)
if err != nil {
return err
}
}
if !apiequality.Semantic.DeepEqual(nfr, nfrUpdated) {
klog.InfoS("updating NodeFeature object", "nodefeature", klog.KObj(nfr))
nfrUpdated, err = cli.NfdV1alpha1().NodeFeatures(namespace).Update(context.TODO(), nfrUpdated, metav1.UpdateOptions{})
@ -720,3 +754,29 @@ func (c *sourcesConfig) UnmarshalJSON(data []byte) error {
return nil
}
// signNodeFeatureCR add the signature to the annotations of a given NodeFeature CR
func (m *nfdWorker) signNodeFeatureCR(nfr *nfdv1alpha1.NodeFeature) error {
workerPrivateKey, _, err := m.spiffeClient.GetWorkerKeys()
if err != nil {
return fmt.Errorf("error while getting worker keys: %w", err)
}
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)
}
encodedSignature := b64.StdEncoding.EncodeToString(signature)
nfr.ObjectMeta.Annotations["signature"] = encodedSignature
return nil
}

134
pkg/utils/spiffe/spiffe.go Normal file
View file

@ -0,0 +1,134 @@
/*
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.
See the License for the specific language governing permissions and
limitations under the License.
*/
package spiffe
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
b64 "encoding/base64"
"encoding/json"
"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.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))
if err != nil {
return nil, err
}
spiffeClient.WorkloadApiClient = *workloadApiClient
return &spiffeClient, nil
}
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[:])
if err != nil {
return []byte{}, err
}
case *ecdsa.PrivateKey:
signedData, err = ecdsa.SignASN1(rand.Reader, privateKey.(*ecdsa.PrivateKey), dataHash[:])
if err != nil {
return []byte{}, err
}
default:
return nil, fmt.Errorf("unknown private key type: %v", t)
}
hash_signature_cache[string(dataHash[:])] = signedData
return signedData, nil
}
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
}
decodedSignature, err := b64.StdEncoding.DecodeString(signedData)
if err != nil {
return false, err
}
dataHash := sha256.Sum256([]byte(stringifyData))
switch t := privateKey.(type) {
case *rsa.PrivateKey:
err = rsa.VerifyPKCS1v15(publicKey.(*rsa.PublicKey), crypto.SHA256, dataHash[:], decodedSignature)
if err != nil {
return false, err
}
return true, nil
case *ecdsa.PrivateKey:
verify := ecdsa.VerifyASN1(publicKey.(*ecdsa.PublicKey), dataHash[:], decodedSignature)
return verify, nil
default:
return false, fmt.Errorf("unknown private key type: %v", t)
}
}
func (s *SpiffeClient) GetWorkerKeys() (crypto.Signer, crypto.PublicKey, error) {
ctx := context.Background()
svids, err := s.WorkloadApiClient.FetchX509SVIDs(ctx)
if err != nil {
return nil, nil, err
}
for _, svid := range svids {
if svid.ID.String() == WorkerSpiffeID {
return svid.PrivateKey, svid.PrivateKey.Public(), nil
}
}
return nil, nil, fmt.Errorf("cannot sign data: spiffe ID %s is not found", WorkerSpiffeID)
}

View file

@ -0,0 +1,181 @@
/*
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.
See the License for the specific language governing permissions and
limitations under the License.
*/
package spiffe
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
b64 "encoding/base64"
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1"
)
func mockNFRSpec() v1alpha1.NodeFeatureSpec {
return v1alpha1.NodeFeatureSpec{
Features: v1alpha1.Features{
Flags: map[string]v1alpha1.FlagFeatureSet{
"test": {
Elements: map[string]v1alpha1.Nil{
"test2": {},
},
},
},
},
}
}
func mockWorkerECDSAPrivateKey() (*ecdsa.PrivateKey, *ecdsa.PublicKey) {
privateKey, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
return privateKey, &privateKey.PublicKey
}
func mockWorkerRSAPrivateKey() (*rsa.PrivateKey, *rsa.PublicKey) {
privateKey, _ := rsa.GenerateKey(rand.Reader, 4096)
return privateKey, &privateKey.PublicKey
}
func TestVerify(t *testing.T) {
rsaPrivateKey, rsaPublicKey := mockWorkerRSAPrivateKey()
ecdsaPrivateKey, ecdsaPublicKey := mockWorkerECDSAPrivateKey()
spec := mockNFRSpec()
tc := []struct {
name string
privateKey crypto.Signer
publicKey crypto.PublicKey
wantErr bool
}{
{
name: "RSA Keys",
privateKey: rsaPrivateKey,
publicKey: rsaPublicKey,
wantErr: true,
},
{
name: "ECDSA Keys",
privateKey: ecdsaPrivateKey,
publicKey: ecdsaPublicKey,
wantErr: false,
},
}
for _, tt := range tc {
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(spiffeObj, b64.StdEncoding.EncodeToString(signedData), tt.privateKey, tt.publicKey)
assert.NoError(t, err)
assert.True(t, isVerified)
signedData = append(signedData, "random"...)
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{}
}
}
func TestSignData(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",
},
}
_, 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[:]))
}
}