Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 62 additions & 8 deletions internal/credbundle/credbundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,30 +25,84 @@ import (
"encoding/pem"
"fmt"
"os"
"sync"
)

// Loader reads a private key and certificate chain from a credential bundle file as written by the
// Kubernetes Pod Certificates mechanism.
//
// Returns a function that can be used as GetCertificate in a tls.Config
// Returns a function that can be used as GetCertificate in a tls.Config. The parsed bundle is
// cached: each handshake stats the file and re-reads it only when the file has changed, so
// pod-certificate rotations are picked up on the next handshake without paying the read and
// parse cost when nothing changed.
func Loader(path string) func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
// TODO: Introduce caching.
c := &certCache{path: path}
return func(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
return Parse(path)
return c.get()
}
}

// ClientLoader is the client-side counterpart to Loader. It returns a function
// suitable for use as GetClientCertificate in a tls.Config, re-reading the
// bundle on each handshake so that in-place pod-certificate rotations are
// picked up.
// suitable for use as GetClientCertificate in a tls.Config, caching the parsed
// bundle in the same way so that pod-certificate rotations are picked up on
// the next handshake.
func ClientLoader(path string) func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
// TODO: Introduce caching.
c := &certCache{path: path}
return func(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) {
return Parse(path)
return c.get()
}
}

// certCache holds the parse of a credential bundle file together with the stat
// of the file it was parsed from, so unchanged files are not re-parsed on
// every TLS handshake.
type certCache struct {
path string

mu sync.Mutex
// fi is the stat of path taken just before cert was parsed; nil until the
// first successful parse. cert is served while a fresh stat of path still
// matches it.
fi os.FileInfo
cert *tls.Certificate
}

// get returns the parsed bundle, re-reading the file only when it has changed
// since the last successful parse.
//
// A change is any difference in file identity (os.SameFile: device and inode
// on Unix), modification time, or size. The kubelet rotates projected volume
// contents, including pod certificates, by writing a fresh timestamped
// directory and atomically swapping a symlink to it, so a rotation always
// surfaces here as a change of file identity; mtime and size additionally
// cover in-place rewrites, whose mid-write states a reader may also observe.
//
// The file can still change between the stat and the read below. The parse of
// the newer content is then stored against the older stat, so the next call
// sees a stat mismatch and re-reads; the cache never lags a rotation by more
// than one handshake. Errors leave the previous entry in place and are
// returned to the caller: handshakes fail exactly as they would without
// caching, and every later call retries until a parse succeeds.
func (c *certCache) get() (*tls.Certificate, error) {
c.mu.Lock()
defer c.mu.Unlock()

fi, err := os.Stat(c.path)
if err != nil {
return nil, fmt.Errorf("while examining credential bundle: %w", err)
}
if c.cert != nil && os.SameFile(c.fi, fi) && fi.ModTime().Equal(c.fi.ModTime()) && fi.Size() == c.fi.Size() {
return c.cert, nil
}

cert, err := Parse(c.path)
if err != nil {
return nil, err
}
c.fi, c.cert = fi, cert
return cert, nil
}

// Parse reads a private key and certificate chain from a credential bundle file as written by the
// Kubernetes Pod Certificates mechanism.
func Parse(bundlePath string) (*tls.Certificate, error) {
Expand Down
Loading
Loading