vtls

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 11, 2026 License: Apache-2.0 Imports: 29 Imported by: 0

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

View Source
const (
	LetsEncryptProductionURL = "https://acme-v02.api.letsencrypt.org/directory"
	LetsEncryptStagingURL    = "https://acme-staging-v02.api.letsencrypt.org/directory"
)

ACME directory endpoints.

View Source
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

func ValidateSPIFFEID(spiffeURI, expectedTrustDomain string) error

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

func NewLocalCA(store *Store) (*LocalCA, error)

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.

func (*LocalCA) Issue

func (ca *LocalCA) Issue(domain string) (*tls.Certificate, error)

Issue returns a leaf certificate for domain signed by the CA. If a valid, non-expiring cert is already stored for the domain it is returned from cache; otherwise a new 90-day cert is issued and persisted.

func (*LocalCA) TLSConfig

func (ca *LocalCA) TLSConfig() *tls.Config

TLSConfig returns a *tls.Config whose GetCertificate issues (or returns a cached) leaf certificate for the requested ServerName via the CA.

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

func (m *Manager) ChallengeHandler() http.Handler

ChallengeHandler returns the ACME HTTP-01 challenge handler, or nil for the internal (LocalCA) provider which needs no challenge.

func (*Manager) LocalCA

func (m *Manager) LocalCA() *LocalCA

LocalCA returns the underlying LocalCA, or nil if the provider is not "internal". Useful for trust-anchor installation and tests.

func (*Manager) StartBackground

func (m *Manager) StartBackground(ctx context.Context)

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

func (m *Manager) TLSConfig() *tls.Config

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

func NewStore(path string, key []byte) (*Store, error)

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) Delete

func (s *Store) Delete(domain string) error

Delete removes the stored certificate for domain. It is idempotent.

func (*Store) List

func (s *Store) List() ([]string, error)

List returns the domains that have stored certificates.

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

func (s *Store) NeedsRenewal(domain string, before time.Duration) (bool, error)

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.

func (*Store) Save

func (s *Store) Save(domain string, cert *tls.Certificate) error

Save encrypts and writes cert for domain.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL