Skip to content

Commit e24d0d4

Browse files
guilhermoccNeniel
authored andcommitted
Serving cert file OIDC provider (spiffe#4190)
* Add disk cert manager Signed-off-by: Guilherme Carvalho <[email protected]> Signed-off-by: Guilherme Carvalho <[email protected]> Signed-off-by: Neniel <[email protected]>
1 parent 509fdc2 commit e24d0d4

File tree

10 files changed

+1080
-65
lines changed

10 files changed

+1080
-65
lines changed

pkg/common/telemetry/names.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,12 @@ const (
198198
// to add clarity
199199
CallerPath = "caller_path"
200200

201+
// CertFilePath tags a certificate file path used for TLS connections.
202+
CertFilePath = "cert_file_path"
203+
204+
// KeyFilePath tags a key file path used for TLS connections.
205+
KeyFilePath = "key_file_path"
206+
201207
// CGroupPath tags a linux CGroup path, most likely for use in attestation
202208
CGroupPath = "cgroup_path"
203209

support/oidc-discovery-provider/README.md

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ The configuration file is **required** by the provider. It contains
3434
| Key | Type | Required? | Description | Default |
3535
|-------------------------|---------|----------------|------------------------------------------------------------------------|----------|
3636
| `acme` | section | required[1] | Provides the ACME configuration. | |
37+
| `serving_cert_file` | section | required[1][4] | Provides the serving certificate configuration. | |
3738
| `allow_insecure_scheme` | string | optional[3] | Serves OIDC configuration response with HTTP url. | `false` |
3839
| `domains` | strings | required | One or more domains the provider is being served from. | |
3940
| `experimental` | section | optional | The experimental options that are subject to change or removal. | |
@@ -56,13 +57,13 @@ The configuration file is **required** by the provider. It contains
5657

5758
#### Considerations for Unix platforms
5859

59-
[1]: One of `acme` or `listen_socket_path` must be defined.
60+
[1]: One of `acme`, `serving_cert_file` or `listen_socket_path` must be defined.
6061

6162
[3]: The `allow_insecure_scheme` should only be used in a local development environment for testing purposes. It only works in conjunction with `insecure_addr` or `listen_socket_path`.
6263

6364
#### Considerations for Windows platforms
6465

65-
[1]: One of `acme` or `listen_named_pipe_name` must be defined.
66+
[1]: One of `acme`, `serving_cert_file` or `listen_named_pipe_name` must be defined.
6667

6768
[3]: The `allow_insecure_scheme` should only be used in a local development environment for testing purposes. It only works in conjunction with `insecure_addr` or `listen_named_pipe_name`.
6869

@@ -77,6 +78,8 @@ will be rejected. Likewise, when ACME is used, the `domains` list contains the
7778
allowed domains for which certificates will be obtained. The TLS handshake
7879
will terminate if another domain is requested.
7980

81+
[4]: SPIRE OIDC Discovery provider monitors and reloads the files provided in the `serving_cert_file` configuration at runtime.
82+
8083
#### ACME Section
8184

8285
| Key | Type | Required? | Description | Default |
@@ -86,6 +89,15 @@ will terminate if another domain is requested.
8689
| `email` | string | required | The email address used to register with the ACME service | |
8790
| `tos_accepted` | bool | required | Indicates explicit acceptance of the ACME service Terms of Service. Must be true. | |
8891

92+
#### Serving Certificate Section
93+
94+
| Key | Type | Required? | Description | Default |
95+
|----------------------|----------|-----------|--------------------------------------------------------------------|----------|
96+
| `cert_file_path` | string | required | The certificate file path, the file must contain PEM encoded data. | |
97+
| `key_file_path` | string | required | The private key file path, the file must contain PEM encoded data. | |
98+
| `file_sync_interval` | duration | optional | Controls how frequently the service polls the files for changes. | 1 minute |
99+
| `addr` | string | optional | Exposes the service on the given address. | :443 |
100+
89101
#### Server API Section
90102

91103
| Key | Type | Required? | Description | Default |
@@ -130,7 +142,7 @@ Both states respond with a 200 OK status code for success or 500 Internal Server
130142

131143
### Examples (Unix platforms)
132144

133-
#### Server API
145+
#### Server API and ACME
134146

135147
```hcl
136148
log_level = "debug"
@@ -145,7 +157,7 @@ server_api {
145157
}
146158
```
147159

148-
#### Workload API
160+
#### Workload API and ACME
149161

150162
```hcl
151163
log_level = "debug"
@@ -161,6 +173,35 @@ workload_api {
161173
}
162174
```
163175

176+
#### Server API and Serving Certificate
177+
178+
```hcl
179+
log_level = "debug"
180+
domains = ["mypublicdomain.test"]
181+
serving_cert_file {
182+
cert_file_path = "/some/path/on/disk/to/cert.pem"
183+
key_file_path = "/some/path/on/disk/to/key.pem"
184+
}
185+
server_api {
186+
address = "unix:///tmp/spire-server/private/api.sock"
187+
}
188+
```
189+
190+
#### Workload API and Serving Certificate
191+
192+
```hcl
193+
log_level = "debug"
194+
domains = ["mypublicdomain.test"]
195+
serving_cert_file {
196+
cert_file_path = "/some/path/on/disk/to/cert.pem"
197+
key_file_path = "/some/path/on/disk/to/key.pem"
198+
}
199+
workload_api {
200+
socket_path = "/tmp/spire-agent/public/api.sock"
201+
trust_domain = "domain.test"
202+
}
203+
```
204+
164205
#### Listening on a Unix Socket
165206

166207
The following configuration has the OIDC Discovery Provider listen for requests
@@ -200,7 +241,7 @@ daemon off;
200241

201242
### Examples (Windows)
202243

203-
#### Server API
244+
#### Server API and ACME
204245

205246
```hcl
206247
log_level = "debug"
@@ -217,7 +258,7 @@ server_api {
217258
}
218259
```
219260

220-
#### Workload API
261+
#### Workload API and ACME
221262

222263
```hcl
223264
log_level = "debug"
@@ -235,6 +276,39 @@ workload_api {
235276
}
236277
```
237278

279+
#### Server API and Serving Certificate
280+
281+
```hcl
282+
log_level = "debug"
283+
domains = ["mypublicdomain.test"]
284+
serving_cert_file {
285+
cert_file_path = "c:\\some\\path\\on\\disk\\to\\cert.pem"
286+
key_file_path = "c:\\some\\path\\on\\disk\\to\\key.pem"
287+
}
288+
server_api {
289+
experimental {
290+
named_pipe_name = "\\spire-server\\private\\api"
291+
}
292+
}
293+
```
294+
295+
#### Workload API and Serving Certificate
296+
297+
```hcl
298+
log_level = "debug"
299+
domains = ["mypublicdomain.test"]
300+
serving_cert_file {
301+
cert_file_path = "c:\\some\\path\\on\\disk\\to\\cert.pem"
302+
key_file_path = "c:\\some\\path\\on\\disk\\to\\key.pem"
303+
}
304+
workload_api {
305+
experimental {
306+
named_pipe_name = "\\spire-agent\\public\\api"
307+
}
308+
trust_domain = "domain.test"
309+
}
310+
```
311+
238312
#### Listening on a Named Pipe
239313

240314
The following configuration has the OIDC Discovery Provider listen for requests
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"crypto/x509"
7+
"errors"
8+
"fmt"
9+
"io/fs"
10+
"os"
11+
"sync"
12+
"time"
13+
14+
"github.com/andres-erbsen/clock"
15+
"github.com/sirupsen/logrus"
16+
)
17+
18+
// DiskCertManager is a certificate manager that loads certificates from disk, and watches for changes.
19+
type DiskCertManager struct {
20+
certFilePath string
21+
keyFilePath string
22+
certLastModified time.Time
23+
keyLastModified time.Time
24+
fileSyncInterval time.Duration
25+
certMtx sync.RWMutex
26+
cert *tls.Certificate
27+
clk clock.Clock
28+
log logrus.FieldLogger
29+
}
30+
31+
func NewDiskCertManager(config *ServingCertFileConfig, clk clock.Clock, log logrus.FieldLogger) (*DiskCertManager, error) {
32+
if config == nil {
33+
return nil, errors.New("missing serving cert file configuration")
34+
}
35+
36+
if clk == nil {
37+
clk = clock.New()
38+
}
39+
40+
dm := &DiskCertManager{
41+
certFilePath: config.CertFilePath,
42+
keyFilePath: config.KeyFilePath,
43+
fileSyncInterval: config.FileSyncInterval,
44+
log: log,
45+
clk: clk,
46+
}
47+
48+
if err := dm.loadCert(); err != nil {
49+
return nil, fmt.Errorf("failed to load certificate: %w", err)
50+
}
51+
52+
return dm, nil
53+
}
54+
55+
// TLSConfig returns a TLS configuration that uses the provided certificate stored on disk.
56+
func (m *DiskCertManager) TLSConfig() *tls.Config {
57+
return &tls.Config{
58+
GetCertificate: m.getCertificate,
59+
NextProtos: []string{
60+
"h2", "http/1.1", // enable HTTP/2
61+
},
62+
MinVersion: tls.VersionTLS12,
63+
}
64+
}
65+
66+
// getCertificate is called by the TLS stack when a new TLS connection is established.
67+
func (m *DiskCertManager) getCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
68+
m.certMtx.RLock()
69+
defer m.certMtx.RUnlock()
70+
cert := m.cert
71+
72+
return cert, nil
73+
}
74+
75+
// WatchFileChanges starts a file watcher to watch for changes to the cert and key files.
76+
func (m *DiskCertManager) WatchFileChanges(ctx context.Context) {
77+
m.log.WithField("interval", m.fileSyncInterval).Info("Started watching certificate files")
78+
ticker := m.clk.Ticker(m.fileSyncInterval)
79+
defer ticker.Stop()
80+
for {
81+
select {
82+
case <-ctx.Done():
83+
m.log.Info("Stopping file watcher")
84+
return
85+
case <-ticker.C:
86+
m.syncCertificateFiles()
87+
}
88+
}
89+
}
90+
91+
// syncCertificateFiles checks if the cert and key files have been modified, and reloads the certificate if necessary.
92+
func (m *DiskCertManager) syncCertificateFiles() {
93+
certFileInfo, keyFileInfo, err := m.getFilesInfo()
94+
if err != nil {
95+
return
96+
}
97+
98+
if certFileInfo.ModTime() != m.certLastModified || keyFileInfo.ModTime() != m.keyLastModified {
99+
m.log.Info("File change detected, reloading certificate and key...")
100+
101+
if err := m.loadCert(); err != nil {
102+
m.log.Errorf("Failed to load certificate: %v", err)
103+
} else {
104+
m.certLastModified = certFileInfo.ModTime()
105+
m.keyLastModified = keyFileInfo.ModTime()
106+
m.log.Info("Loaded provided certificate with success")
107+
}
108+
}
109+
}
110+
111+
// loadCert read the certificate and key files, and load the x509 certificate to memory.
112+
func (m *DiskCertManager) loadCert() error {
113+
cert, err := tls.LoadX509KeyPair(m.certFilePath, m.keyFilePath)
114+
if err != nil {
115+
return err
116+
}
117+
118+
cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
119+
if err != nil {
120+
return err
121+
}
122+
123+
m.certMtx.Lock()
124+
defer m.certMtx.Unlock()
125+
126+
m.cert = &cert
127+
128+
return nil
129+
}
130+
131+
// getFilesInfo returns the file info of the cert and key files, or error if the files are unreadable or do not exist.
132+
func (m *DiskCertManager) getFilesInfo() (os.FileInfo, os.FileInfo, error) {
133+
certFileInfo, err := m.getFileInfo(m.certFilePath)
134+
if err != nil {
135+
return nil, nil, err
136+
}
137+
138+
keyFileInfo, err := m.getFileInfo(m.keyFilePath)
139+
if err != nil {
140+
return nil, nil, err
141+
}
142+
143+
return certFileInfo, keyFileInfo, nil
144+
}
145+
146+
// getFileInfo returns the file info of the given path, or error if the file is unreadable or does not exist.
147+
func (m *DiskCertManager) getFileInfo(path string) (os.FileInfo, error) {
148+
fileInfo, err := os.Stat(path)
149+
if err != nil {
150+
errFs := new(fs.PathError)
151+
switch {
152+
case errors.Is(err, fs.ErrNotExist) && errors.As(err, &errFs):
153+
m.log.Errorf("Failed to get file info, file path %q does not exist anymore; please check if the path is correct", errFs.Path)
154+
default:
155+
m.log.Errorf("Failed to get file info: %v", err)
156+
}
157+
return nil, err
158+
}
159+
160+
return fileInfo, nil
161+
}

0 commit comments

Comments
 (0)