Skip to content

Commit ecebef9

Browse files
committed
[RFC-0014] spiffe: mtls building blocks
Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>
1 parent 2bc5cec commit ecebef9

8 files changed

Lines changed: 723 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
### Authentication & Security
1616
- **[github.com/fluxcd/pkg/auth](./auth)** - OIDC-based authentication with cloud providers (AWS, Azure, GCP)
1717
- **[github.com/fluxcd/pkg/masktoken](./masktoken)** - Token redaction utilities for secure logging
18+
- **[github.com/fluxcd/pkg/spiffe](./spiffe)** - SPIFFE Workload API X.509-SVID utilities for mutual TLS
1819
- **[github.com/fluxcd/pkg/ssh](./ssh)** - SSH host key scanning and management
1920

2021
### Artifact Storage & Serving

‎spiffe/controller_flags.go‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
Copyright 2026 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package spiffe
18+
19+
// Controller flags for authorizing peers in inter-controller communication.
20+
// These flags are repeatable. Each value is parsed by
21+
// cryptov1.ParseSPIFFEID and may be an exact SPIFFE ID, a SPIFFE ID prefix, a
22+
// SPIFFE trust domain or the `spiffe://self` symbol.
23+
const (
24+
// ControllerFlagAuthorizedArtifactClientSPIFFEID defines the flag for the
25+
// SPIFFE IDs authorized to fetch artifacts from an artifact server.
26+
ControllerFlagAuthorizedArtifactClientSPIFFEID = "authorized-artifact-client-spiffe-id"
27+
28+
// ControllerFlagAuthorizedArtifactServerSPIFFEID defines the flag for the
29+
// SPIFFE IDs of artifact servers authorized by an artifact client.
30+
ControllerFlagAuthorizedArtifactServerSPIFFEID = "authorized-artifact-server-spiffe-id"
31+
32+
// ControllerFlagAuthorizedNotifierSPIFFEID defines the flag for the
33+
// SPIFFE IDs authorized to send events to an event server.
34+
ControllerFlagAuthorizedNotifierSPIFFEID = "authorized-notifier-spiffe-id"
35+
36+
// ControllerFlagAuthorizedEventServerSPIFFEID defines the flag for the
37+
// SPIFFE IDs of event servers authorized by an event client.
38+
ControllerFlagAuthorizedEventServerSPIFFEID = "authorized-event-server-spiffe-id"
39+
40+
// ControllerFlagAuthorizedReceiverSPIFFEID defines the flag for the
41+
// SPIFFE IDs authorized to send requests to a receiver server.
42+
ControllerFlagAuthorizedReceiverSPIFFEID = "authorized-receiver-spiffe-id"
43+
44+
// ControllerFlagAuthorizedKubeAPIServerSPIFFEID defines the flag for the
45+
// SPIFFE IDs of Kubernetes API servers authorized by a client.
46+
ControllerFlagAuthorizedKubeAPIServerSPIFFEID = "authorized-kube-apiserver-spiffe-id"
47+
)

‎spiffe/doc.go‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
Copyright 2026 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package spiffe provides helpers for securing communication with SPIFFE
18+
// X.509-SVIDs acquired from the SPIFFE Workload API.
19+
package spiffe

‎spiffe/fake_test.go‎

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/*
2+
Copyright 2026 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package spiffe_test
18+
19+
import (
20+
"crypto/ecdsa"
21+
"crypto/elliptic"
22+
"crypto/rand"
23+
"crypto/x509"
24+
"crypto/x509/pkix"
25+
"errors"
26+
"math/big"
27+
"net"
28+
"net/url"
29+
"os"
30+
"sync"
31+
"testing"
32+
"time"
33+
34+
"github.com/spiffe/go-spiffe/v2/proto/spiffe/workload"
35+
"github.com/spiffe/go-spiffe/v2/spiffeid"
36+
"github.com/spiffe/go-spiffe/v2/workloadapi"
37+
"google.golang.org/grpc"
38+
)
39+
40+
type testCA struct {
41+
cert *x509.Certificate
42+
key *ecdsa.PrivateKey
43+
}
44+
45+
func newTestCA(t *testing.T) *testCA {
46+
t.Helper()
47+
48+
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
49+
if err != nil {
50+
t.Fatalf("failed to generate CA key: %v", err)
51+
}
52+
tmpl := &x509.Certificate{
53+
SerialNumber: big.NewInt(1),
54+
Subject: pkix.Name{CommonName: "test-ca"},
55+
NotBefore: time.Now().Add(-time.Hour),
56+
NotAfter: time.Now().Add(time.Hour),
57+
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
58+
BasicConstraintsValid: true,
59+
IsCA: true,
60+
}
61+
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
62+
if err != nil {
63+
t.Fatalf("failed to create CA certificate: %v", err)
64+
}
65+
cert, err := x509.ParseCertificate(der)
66+
if err != nil {
67+
t.Fatalf("failed to parse CA certificate: %v", err)
68+
}
69+
return &testCA{cert: cert, key: key}
70+
}
71+
72+
func (ca *testCA) newCertificate(t *testing.T, id spiffeid.ID, serial int64) (*x509.Certificate, *ecdsa.PrivateKey) {
73+
t.Helper()
74+
75+
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
76+
if err != nil {
77+
t.Fatalf("failed to generate SVID key: %v", err)
78+
}
79+
uri, err := url.Parse(id.String())
80+
if err != nil {
81+
t.Fatalf("failed to parse SPIFFE ID: %v", err)
82+
}
83+
tmpl := &x509.Certificate{
84+
SerialNumber: big.NewInt(serial),
85+
NotBefore: time.Now().Add(-time.Hour),
86+
NotAfter: time.Now().Add(time.Hour),
87+
KeyUsage: x509.KeyUsageDigitalSignature,
88+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
89+
URIs: []*url.URL{uri},
90+
}
91+
der, err := x509.CreateCertificate(rand.Reader, tmpl, ca.cert, &key.PublicKey, ca.key)
92+
if err != nil {
93+
t.Fatalf("failed to create SVID certificate: %v", err)
94+
}
95+
cert, err := x509.ParseCertificate(der)
96+
if err != nil {
97+
t.Fatalf("failed to parse SVID certificate: %v", err)
98+
}
99+
return cert, key
100+
}
101+
102+
func (ca *testCA) newX509SVID(t *testing.T, id spiffeid.ID, serial int64) *workload.X509SVID {
103+
t.Helper()
104+
105+
cert, key := ca.newCertificate(t, id, serial)
106+
keyDER, err := x509.MarshalPKCS8PrivateKey(key)
107+
if err != nil {
108+
t.Fatalf("failed to marshal SVID key: %v", err)
109+
}
110+
return &workload.X509SVID{
111+
SpiffeId: id.String(),
112+
X509Svid: cert.Raw,
113+
X509SvidKey: keyDER,
114+
Bundle: ca.cert.Raw,
115+
}
116+
}
117+
118+
type fakeWorkloadAPI struct {
119+
workload.UnimplementedSpiffeWorkloadAPIServer
120+
121+
mtx sync.Mutex
122+
resp *workload.X509SVIDResponse
123+
updated chan struct{}
124+
}
125+
126+
func newFakeWorkloadAPI(svid *workload.X509SVID) *fakeWorkloadAPI {
127+
f := &fakeWorkloadAPI{updated: make(chan struct{})}
128+
f.setX509SVID(svid)
129+
return f
130+
}
131+
132+
func (f *fakeWorkloadAPI) setX509SVID(svid *workload.X509SVID) {
133+
f.mtx.Lock()
134+
defer f.mtx.Unlock()
135+
f.resp = &workload.X509SVIDResponse{Svids: []*workload.X509SVID{svid}}
136+
close(f.updated)
137+
f.updated = make(chan struct{})
138+
}
139+
140+
func (f *fakeWorkloadAPI) FetchX509SVID(_ *workload.X509SVIDRequest,
141+
stream grpc.ServerStreamingServer[workload.X509SVIDResponse]) error {
142+
for {
143+
f.mtx.Lock()
144+
resp := f.resp
145+
updated := f.updated
146+
f.mtx.Unlock()
147+
148+
if err := stream.Send(resp); err != nil {
149+
return err
150+
}
151+
152+
select {
153+
case <-updated:
154+
case <-stream.Context().Done():
155+
return nil
156+
}
157+
}
158+
}
159+
160+
func newSocketPath(t *testing.T) string {
161+
t.Helper()
162+
dir, err := os.MkdirTemp("", "flux-spiffe")
163+
if err != nil {
164+
t.Fatalf("failed to create temporary directory: %v", err)
165+
}
166+
t.Cleanup(func() { _ = os.RemoveAll(dir) })
167+
return dir + "/api.sock"
168+
}
169+
170+
func startWorkloadAPI(t *testing.T, socketPath string, handler *fakeWorkloadAPI) (stop func()) {
171+
t.Helper()
172+
173+
if err := os.Remove(socketPath); err != nil && !errors.Is(err, os.ErrNotExist) {
174+
t.Fatalf("failed to remove stale socket: %v", err)
175+
}
176+
listener, err := net.Listen("unix", socketPath)
177+
if err != nil {
178+
t.Fatalf("failed to listen on socket: %v", err)
179+
}
180+
server := grpc.NewServer()
181+
workload.RegisterSpiffeWorkloadAPIServer(server, handler)
182+
go func() { _ = server.Serve(listener) }()
183+
184+
var once sync.Once
185+
stop = func() {
186+
once.Do(func() {
187+
server.Stop()
188+
_ = listener.Close()
189+
_ = os.Remove(socketPath)
190+
})
191+
}
192+
t.Cleanup(stop)
193+
return stop
194+
}
195+
196+
type fastBackoff struct{}
197+
198+
func (fastBackoff) NewBackoff() workloadapi.Backoff {
199+
return fastBackoffInstance{}
200+
}
201+
202+
type fastBackoffInstance struct{}
203+
204+
func (fastBackoffInstance) Next() time.Duration { return 10 * time.Millisecond }
205+
func (fastBackoffInstance) Reset() {}

‎spiffe/go.mod‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module github.com/fluxcd/pkg/spiffe
2+
3+
go 1.27.0
4+
5+
require (
6+
github.com/fluxcd/pkg/apis/crypto v0.4.0
7+
github.com/onsi/gomega v1.43.0
8+
github.com/spiffe/go-spiffe/v2 v2.9.0
9+
google.golang.org/grpc v1.83.2
10+
)
11+
12+
replace github.com/fluxcd/pkg/apis/crypto => ../apis/crypto
13+
14+
require (
15+
github.com/Microsoft/go-winio v0.6.2 // indirect
16+
github.com/go-jose/go-jose/v4 v4.1.5 // indirect
17+
github.com/google/go-cmp v0.7.0 // indirect
18+
go.yaml.in/yaml/v3 v3.0.5 // indirect
19+
golang.org/x/net v0.61.0 // indirect
20+
golang.org/x/sys v0.49.0 // indirect
21+
golang.org/x/text v0.43.0 // indirect
22+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect
23+
google.golang.org/protobuf v1.36.12 // indirect
24+
)

‎spiffe/go.sum‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
2+
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
3+
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
4+
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
5+
github.com/go-jose/go-jose/v4 v4.1.5 h1:RjgjO2LOtWOJKUC5wpwY9LR3B3vwVAz6JS2YHfYU6eA=
6+
github.com/go-jose/go-jose/v4 v4.1.5/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
7+
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
8+
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
9+
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
10+
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
11+
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
12+
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
13+
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
14+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
15+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
16+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
17+
github.com/onsi/gomega v1.43.0 h1:VlG/1FxqNxhSO+lq/OHBNaaqwiBK/mO8JbVkX9Y+FeU=
18+
github.com/onsi/gomega v1.43.0/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg=
19+
github.com/spiffe/go-spiffe/v2 v2.9.0 h1:g5qTg3qIP1HXV+GzuyEfSJFVxUCpEP5Ui1OINVJHPB4=
20+
github.com/spiffe/go-spiffe/v2 v2.9.0/go.mod h1:w2CLWKLMTX/PPYUEUPv3ltH0RXsw5S8suwNF46w9/Aw=
21+
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
22+
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
23+
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
24+
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
25+
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
26+
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
27+
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
28+
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
29+
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
30+
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
31+
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
32+
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
33+
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
34+
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
35+
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
36+
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
37+
golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw=
38+
golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
39+
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
40+
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
41+
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
42+
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
43+
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
44+
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
45+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 h1:cYNAzI2sUwhmCcoj9TxvihSrqsxt6uIkj3rDRhSDmW4=
46+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
47+
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
48+
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
49+
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
50+
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=

0 commit comments

Comments
 (0)