Skip to content
Merged
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
9 changes: 9 additions & 0 deletions authbridge/authlib/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ type TLSBridgeConfig struct {
// ca.crt is the trust cert handed to the agent. cert-manager Secret key
// conventions, so only the directory is configured.
CADir string `yaml:"ca_dir" json:"ca_dir"`
// GenerateCA, when true, makes the bridge mint and persist a self-signed CA
// into CADir (tls.crt/tls.key/ca.crt) if those files are absent, instead of
// failing at startup. For standalone / demo use only — in-cluster the CA is
// a mounted cert-manager Secret and this stays false so a missing Secret
// fails loudly rather than silently forging leaves under an untrusted CA.
// CADir should persist across restarts: on ephemeral storage (e.g. an
// emptyDir) a fresh CA is minted each boot, so clients must re-trust the
// new ca.crt.
GenerateCA bool `yaml:"generate_ca" json:"generate_ca"`
// UpstreamCABundle is an extra-roots PEM file for re-origination (private-CA
// origins the agent trusts); empty == system roots only.
UpstreamCABundle string `yaml:"upstream_ca_bundle" json:"upstream_ca_bundle"`
Expand Down
136 changes: 122 additions & 14 deletions authbridge/authlib/tlsbridge/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"fmt"
"math/big"
"os"
"path/filepath"
"time"
)

Expand All @@ -31,12 +32,14 @@ type staticSource struct {
func (s *staticSource) Issuer() (*x509.Certificate, crypto.Signer) { return s.cert, s.key }
func (s *staticSource) CACertPEM() []byte { return s.certPEM }

// NewEphemeralSource generates an in-memory self-signed CA. Used as the
// standalone / no-cert-manager fallback and in tests.
func NewEphemeralSource() (CASource, error) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
// genSelfSignedCA mints a fresh in-memory self-signed ECDSA-P256 CA and returns
// the parsed cert, its signer, and both PEM encodings (cert + PKCS#8 key).
// Shared by NewEphemeralSource (in-memory only) and NewGeneratedFileSource
// (which persists the PEM to disk).
func genSelfSignedCA() (cert *x509.Certificate, key crypto.Signer, certPEM, keyPEM []byte, err error) {
ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, fmt.Errorf("tlsbridge: generate CA key: %w", err)
return nil, nil, nil, nil, fmt.Errorf("tlsbridge: generate CA key: %w", err)
}
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Expand All @@ -47,19 +50,32 @@ func NewEphemeralSource() (CASource, error) {
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
BasicConstraintsValid: true,
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key)
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, ecKey.Public(), ecKey)
if err != nil {
return nil, fmt.Errorf("tlsbridge: self-sign CA: %w", err)
return nil, nil, nil, nil, fmt.Errorf("tlsbridge: self-sign CA: %w", err)
}
cert, err := x509.ParseCertificate(der)
cert, err = x509.ParseCertificate(der)
if err != nil {
return nil, fmt.Errorf("tlsbridge: parse self-signed CA: %w", err)
return nil, nil, nil, nil, fmt.Errorf("tlsbridge: parse self-signed CA: %w", err)
}
return &staticSource{
cert: cert,
key: key,
certPEM: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}),
}, nil
keyDER, err := x509.MarshalPKCS8PrivateKey(ecKey)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("tlsbridge: marshal CA key: %w", err)
}
certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
keyPEM = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})
return cert, ecKey, certPEM, keyPEM, nil
}

// NewEphemeralSource generates an in-memory self-signed CA. Used as the
// standalone / no-cert-manager fallback and in tests. NewGeneratedFileSource is
// the persisted variant used by the demo/standalone binary path (generate_ca).
func NewEphemeralSource() (CASource, error) {
cert, key, certPEM, _, err := genSelfSignedCA()
if err != nil {
return nil, err
}
return &staticSource{cert: cert, key: key, certPEM: certPEM}, nil
}

// NewFileSource loads a CA (tls.crt/tls.key) from disk — the cert-manager /
Expand Down Expand Up @@ -108,6 +124,98 @@ func NewFileSource(certPath, keyPath string) (CASource, error) {
return &staticSource{cert: cert, key: key, certPEM: certPEM}, nil
}

// NewGeneratedFileSource mints a fresh self-signed CA and persists it into the
// directory of certPath: the signing key at keyPath (0600), the CA cert at
// certPath (0644), and — when trustPath is non-empty — a copy of the CA cert at
// trustPath (0644, the "ca.crt" clients load into their trust store, e.g. via
// NODE_EXTRA_CA_CERTS). Returns a CASource backed by the freshly minted
// material. Used only on the generate_ca standalone/demo path.
func NewGeneratedFileSource(certPath, keyPath, trustPath string) (CASource, error) {
cert, key, certPEM, keyPEM, err := genSelfSignedCA()
if err != nil {
return nil, err
}
if err := os.MkdirAll(filepath.Dir(certPath), 0o755); err != nil {
return nil, fmt.Errorf("tlsbridge: create ca_dir: %w", err)
}
// Each file is written atomically (temp + rename) so a reader or a
// subsequent boot never observes a half-written cert or key. The three
// writes are still sequential, so a crash or error between them leaves an
// INCOMPLETE set — EnsureFileSource treats that as "regenerate" rather than
// letting an orphaned tls.key wedge the next boot. Key first at 0600 (a
// signing key must never be world-readable); cert and ca.crt at 0644.
if err := atomicWriteFile(keyPath, keyPEM, 0o600); err != nil {
return nil, fmt.Errorf("tlsbridge: write CA key %s: %w", keyPath, err)
}
if err := atomicWriteFile(certPath, certPEM, 0o644); err != nil {
return nil, fmt.Errorf("tlsbridge: write CA cert %s: %w", certPath, err)
}
if trustPath != "" {
if err := atomicWriteFile(trustPath, certPEM, 0o644); err != nil {
return nil, fmt.Errorf("tlsbridge: write trust cert %s: %w", trustPath, err)
}
}
return &staticSource{cert: cert, key: key, certPEM: certPEM}, nil
}

// EnsureFileSource loads the signing CA (tls.crt/tls.key) from caDir. With
// generate=false it is exactly NewFileSource — a missing or invalid CA fails
// loud, so an operator-mounted cert-manager Secret is never silently replaced.
//
// With generate=true it self-heals an INCOMPLETE on-disk set: when any of
// tls.crt, tls.key, or ca.crt is missing it mints a fresh CA and persists all
// three (generated=true). This closes two failure modes of a partial write (a
// run killed or erroring between the three writes): an orphaned tls.key that
// would otherwise wedge every subsequent boot (NewFileSource fails on the
// missing cert → fatal), and a missing ca.crt trust anchor that loads fine yet
// leaves clients unable to verify the forged leaves. A COMPLETE set is never
// regenerated — it is loaded, and a complete-but-invalid cert/key still fails
// loud via NewFileSource so a real Secret is not overwritten.
func EnsureFileSource(caDir string, generate bool) (src CASource, generated bool, err error) {
certPath := filepath.Join(caDir, "tls.crt")
keyPath := filepath.Join(caDir, "tls.key")
trustPath := filepath.Join(caDir, "ca.crt")
complete := fileExists(certPath) && fileExists(keyPath) && fileExists(trustPath)
if generate && !complete {
src, err = NewGeneratedFileSource(certPath, keyPath, trustPath)
return src, err == nil, err
}
src, err = NewFileSource(certPath, keyPath)
return src, false, err
}

// fileExists reports whether path exists and is statable.
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}

// atomicWriteFile writes data to a temp file in the same directory and renames
// it into place — atomic on POSIX, so a reader or the next boot never sees a
// half-written cert or key. Mirrors authlib/spiffe.atomicWrite (unexported
// there; kept local to avoid coupling the bridge to the spiffe package).
func atomicWriteFile(path string, data []byte, mode os.FileMode) error {
dir := filepath.Dir(path)
tmp, err := os.CreateTemp(dir, ".tmp-"+filepath.Base(path)+"-*")
if err != nil {
return err
}
tmpName := tmp.Name()
defer func() { _ = os.Remove(tmpName) }() // best-effort; a no-op after a successful rename
if _, err := tmp.Write(data); err != nil {
_ = tmp.Close()
return err
}
if err := tmp.Chmod(mode); err != nil {
_ = tmp.Close()
return err
}
if err := tmp.Close(); err != nil {
return err
}
return os.Rename(tmpName, path)
}

// parsePrivateKey accepts PKCS#8, PKCS#1 (RSA) and SEC1 (EC) DER.
func parsePrivateKey(der []byte) (crypto.Signer, error) {
if k, err := x509.ParsePKCS8PrivateKey(der); err == nil {
Expand Down
Loading
Loading