Documentation
¶
Overview ¶
Package vtls implements VORTEX's TLS layer (build plan M2.4): an encrypted certificate store, a local development CA, an ACME (Let's Encrypt/ZeroSSL) manager, and a unified entry point. It is named vtls — not tls — to avoid shadowing the standard library's crypto/tls, which it uses heavily.
Index ¶
- Constants
- func BuildCAPool(caCert *tls.Certificate) (*x509.CertPool, error)
- func ExtractSPIFFEID(cert *x509.Certificate) (string, error)
- func ValidateSPIFFEID(spiffeURI, expectedTrustDomain string) error
- type ACMEConfig
- type ACMEManager
- type LocalCA
- type MTLSConfig
- type Manager
- type ManagerConfig
- type NodeIdentity
- type RotationConfig
- type RotationManager
- type Store
Constants ¶
const ( LetsEncryptProductionURL = "https://acme-v02.api.letsencrypt.org/directory" LetsEncryptStagingURL = "https://acme-staging-v02.api.letsencrypt.org/directory" )
ACME directory endpoints.
const ZeroSSLDirectoryURL = "https://acme.zerossl.com/v2/DV90"
ZeroSSL ACME directory endpoint (DV, 90-day certs).
Variables ¶
This section is empty.
Functions ¶
func BuildCAPool ¶
func BuildCAPool(caCert *tls.Certificate) (*x509.CertPool, error)
BuildCAPool returns an x509.CertPool containing the cluster CA certificate.
func ExtractSPIFFEID ¶
func ExtractSPIFFEID(cert *x509.Certificate) (string, error)
ExtractSPIFFEID returns the SPIFFE URI carried in cert's URI SANs. It errors if the certificate has no SPIFFE URI.
func ValidateSPIFFEID ¶
ValidateSPIFFEID checks that spiffeURI is a well-formed spiffe://<domain>/node/<id> identity whose trust domain matches expectedTrustDomain.
Types ¶
type ACMEConfig ¶
type ACMEConfig struct {
// Email is the ACME account contact. Required.
Email string
// DirectoryURL is the ACME directory endpoint. Defaults to Let's Encrypt
// production (or staging when Staging is true).
DirectoryURL string
// Store persists issued certificates (in addition to autocert's own cache).
Store *Store
// RenewBefore is the lead time before expiry to renew. Default 30 days.
RenewBefore time.Duration
// Staging uses the Let's Encrypt staging environment.
Staging bool
// Logger receives renewal diagnostics; defaults to slog.Default.
Logger *slog.Logger
}
ACMEConfig configures an ACMEManager.
type ACMEManager ¶
type ACMEManager struct {
// contains filtered or unexported fields
}
ACMEManager obtains and renews certificates via the ACME protocol, backed by golang.org/x/crypto/acme/autocert. Issued certificates are also mirrored into the VORTEX encrypted Store so the rest of the system has a single source of truth and can drive renewal scheduling.
func NewACMEManager ¶
func NewACMEManager(cfg ACMEConfig) (*ACMEManager, error)
NewACMEManager validates cfg and constructs an ACMEManager.
func (*ACMEManager) GetCertificate ¶
func (m *ACMEManager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
GetCertificate is the tls.Config.GetCertificate callback. It first serves a valid cached cert from the Store, then falls back to autocert (which performs ACME issuance), mirroring any newly obtained cert into the Store.
func (*ACMEManager) HTTPHandler ¶
func (m *ACMEManager) HTTPHandler(fallback http.Handler) http.Handler
HTTPHandler returns the HTTP-01 challenge handler. It must be mounted on port 80 so the ACME CA can validate domain control. Non-challenge requests fall through to fallback (or 404 if fallback is nil).
func (*ACMEManager) StartRenewalLoop ¶
func (m *ACMEManager) StartRenewalLoop(ctx context.Context)
StartRenewalLoop runs a background goroutine that, every 12 hours, renews any stored certificate within RenewBefore of expiry. It never panics; all errors are logged and the loop continues until ctx is cancelled.
type LocalCA ¶
type LocalCA struct {
// contains filtered or unexported fields
}
LocalCA is a self-signed certificate authority for dev/staging use (config tls.provider = "internal"). It issues short-lived leaf certificates signed by a long-lived CA, persisting both in the encrypted Store.
func NewLocalCA ¶
NewLocalCA loads an existing CA from store or generates a new one and persists it.
func (*LocalCA) CACert ¶
func (ca *LocalCA) CACert() *x509.Certificate
CACert returns the CA certificate, for installing as a trust anchor.
type MTLSConfig ¶
type MTLSConfig struct {
RotationMgr *RotationManager
TrustDomain string
MinVersion uint16
Logger *slog.Logger
}
MTLSConfig builds mutual-TLS server and client configurations for the identity mesh. Both sides require and verify a peer certificate that chains to the cluster CA and carries a SPIFFE URI in the expected trust domain. The local certificate is read from the RotationManager on every handshake via GetCertificate/GetClientCertificate, so a rotated cert is picked up atomically without restarting listeners.
func NewMTLSConfig ¶
func NewMTLSConfig(cfg MTLSConfig) (*MTLSConfig, error)
NewMTLSConfig validates and returns an MTLSConfig with defaults applied.
func (*MTLSConfig) ClientTLSConfig ¶
func (m *MTLSConfig) ClientTLSConfig() *tls.Config
ClientTLSConfig returns a *tls.Config that presents this node's certificate and verifies the server by SPIFFE identity. Hostname verification is disabled (InsecureSkipVerify) because nodes are identified by SPIFFE URI, not DNS name; full chain + identity verification is enforced in verifyPeer instead.
func (*MTLSConfig) ServerTLSConfig ¶
func (m *MTLSConfig) ServerTLSConfig() *tls.Config
ServerTLSConfig returns a *tls.Config that requires a client certificate and verifies it (chain + SPIFFE identity) via verifyPeer.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager is the single TLS entry point. It wraps either a LocalCA (internal provider) or an ACMEManager (letsencrypt/zerossl) and exposes a hardened *tls.Config plus the ACME challenge handler and background renewal.
func NewManager ¶
func NewManager(cfg ManagerConfig) (*Manager, error)
NewManager builds a Manager from cfg, creating the certificate store and the provider-specific backend.
func (*Manager) ChallengeHandler ¶
ChallengeHandler returns the ACME HTTP-01 challenge handler, or nil for the internal (LocalCA) provider which needs no challenge.
func (*Manager) LocalCA ¶
LocalCA returns the underlying LocalCA, or nil if the provider is not "internal". Useful for trust-anchor installation and tests.
func (*Manager) StartBackground ¶
StartBackground starts the manager's background loops: ACME renewal (ACME providers only) and 24h session ticket key rotation for every config issued by TLSConfig. Non-blocking; both loops stop when ctx is cancelled.
func (*Manager) TLSConfig ¶
TLSConfig returns a hardened *tls.Config: GetCertificate routes to the provider, the minimum version is enforced, and only forward-secret AEAD ECDHE cipher suites are offered for TLS 1.2 (no TLS_RSA_*, RC4, 3DES, or non-ECDHE CBC). TLS 1.3 cipher suites are fixed by the standard library. Every returned config is registered for 24h session ticket key rotation (see StartBackground).
type ManagerConfig ¶
type ManagerConfig struct {
// Provider selects the certificate source: "letsencrypt", "zerossl", or
// "internal" (LocalCA).
Provider string
// ACME configures the ACME path (used for letsencrypt/zerossl). Email is
// required for those providers.
ACME ACMEConfig
// StorePath is where certificates are stored.
StorePath string
// StoreKey is the encryption key for the store. Required.
StoreKey []byte
// MinVersion is "TLS1.2" (default) or "TLS1.3".
MinVersion string
}
ManagerConfig configures the unified TLS Manager.
type NodeIdentity ¶
type NodeIdentity struct {
NodeID string // 16-char hex, SHA-256 derived
SPIFFEURI string // spiffe://<trust-domain>/node/<id>
TrustDomain string // <cluster-name>.vortex (or vortex.local)
}
NodeIdentity is a VORTEX node's SPIFFE-style identity used for the mTLS identity mesh. The node ID is derived deterministically from the cluster name and hostname so it is stable across restarts, and the trust domain is cluster-scoped so a cert from one cluster cannot authenticate to another.
func NewNodeIdentity ¶
func NewNodeIdentity(clusterName string) (*NodeIdentity, error)
NewNodeIdentity derives this node's identity from the cluster name and the host's name. The node ID is the first 16 hex characters of SHA-256(clusterName + "/" + hostname); the trust domain is clusterName + ".vortex" (or "vortex.local" when clusterName is empty).
func (*NodeIdentity) IssueNodeCert ¶
func (n *NodeIdentity) IssueNodeCert(ca *tls.Certificate, lifetime time.Duration) (*tls.Certificate, error)
IssueNodeCert issues an ECDSA P-256 node certificate carrying this identity's SPIFFE URI SAN, signed by the cluster CA. The CA must carry both its parsed leaf certificate (or a parseable DER) and its private key. The issued cert is usable for both client and server auth (mTLS peers act as both).
func (*NodeIdentity) URISANs ¶
func (n *NodeIdentity) URISANs() []*url.URL
URISANs returns the SPIFFE URI as a parsed *url.URL slice for the x509 certificate URIs field.
type RotationConfig ¶
type RotationConfig struct {
// ClusterName scopes the trust domain and CA subject. Required.
ClusterName string
// Store persists the cluster CA and node cert (the M2.4 vtls.Store).
Store *Store
// CertLifetime is the validity of issued node certs. Default 24h.
CertLifetime time.Duration
// RotateAt is the remaining-lifetime threshold that triggers rotation.
// Default 4h.
RotateAt time.Duration
// Logger receives rotation events; defaults to slog.Default.
Logger *slog.Logger
}
RotationConfig configures a RotationManager.
type RotationManager ¶
type RotationManager struct {
// contains filtered or unexported fields
}
RotationManager owns the cluster CA and rotates this node's mTLS identity certificate. The current cert is held behind an atomic pointer so it can be swapped under live traffic without restarting listeners: the mTLS config's GetCertificate/GetClientCertificate callbacks read Current() on each handshake, so established connections are unaffected and in-flight handshakes always get a valid cert.
func NewRotationManager ¶
func NewRotationManager(cfg RotationConfig) (*RotationManager, error)
NewRotationManager loads or generates the cluster CA, loads or issues this node's cert, and returns a ready manager. It does not start the rotation loop — call StartRotation for that.
func (*RotationManager) ClusterCA ¶
func (r *RotationManager) ClusterCA() *tls.Certificate
ClusterCA returns the cluster CA certificate, for building trust pools.
func (*RotationManager) Current ¶
func (r *RotationManager) Current() *tls.Certificate
Current returns the active node certificate. It is never nil after NewRotationManager succeeds.
func (*RotationManager) Identity ¶
func (r *RotationManager) Identity() *NodeIdentity
Identity returns this node's SPIFFE identity.
func (*RotationManager) StartRotation ¶
func (r *RotationManager) StartRotation(ctx context.Context)
StartRotation runs a background goroutine that re-issues the node cert before it expires. It checks periodically (every min(30m, RotateAt/2)); when the current cert's remaining lifetime drops below RotateAt it issues a new cert, persists it, and atomically swaps it in. Issue failures are logged and retried on the next check — the loop never panics and stops only on ctx cancellation.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store persists TLS certificates on disk, encrypted with AES-256-GCM. Each domain's certificate (leaf, chain, and private key) is stored in its own file. The encryption key is derived from caller-supplied key material.
func NewStore ¶
NewStore opens (creating if needed) a certificate store at path, deriving the 32-byte AES key from key (SHA-256 of key if it is not already 32 bytes).
func (*Store) Load ¶
func (s *Store) Load(domain string) (*tls.Certificate, error)
Load decrypts and returns the certificate for domain. It returns os.ErrNotExist if no certificate is stored.
func (*Store) NeedsRenewal ¶
NeedsRenewal reports whether the stored cert for domain expires within `before`. A domain with no stored cert returns (false, nil): nothing to renew because nothing has been issued yet.