Documentation
¶
Overview ¶
Client cert ledger — persists metadata about issued client certs so we can revoke them by fingerprint. Plain JSON on disk; the volume of records is bounded by the number of Culvert nodes an operator enrolls (typically <100) so this is fine without a real database.
File layout (/data/clients.json):
{
"version": 1,
"clients": [
{ "fingerprint": "sha256:...", "name": "", "common_name": "Sluice Client",
"issued_at_unix": 1713110400, "not_after_unix": 1744646400,
"revoked_at_unix": 0, "revoke_reason": "" }
]
}
name is empty in v0.2 — we don't collect a label at Enroll time. Culvert stores its own label locally and uses fingerprint as the correlation key.
FingerprintTracker keeps the current server cert fingerprint plus an optional "rotated-out" fingerprint that clients should continue to accept during a migration grace window. This lets an operator swap the server cert without forcing every Culvert node to re-enroll.
The tracker is in-memory only. On daemon restart the rotated fingerprint (if any) is lost — operators should complete rotations before restarting, or bake a grace window long enough to outlast a restart. In practice the default 24h grace and typical restart times make this a non-issue.
Index ¶
- func BootstrapServerCerts(certFile, keyFile, caFile string, hosts []string) (caCertPEM, serverCertPEM []byte, err error)
- func CertCommonName(certPEM []byte) (string, error)
- func CertFingerprintSHA256(certPEM []byte) (string, error)
- func CertNotAfter(certPEM []byte) (time.Time, error)
- func GenerateCA() (certPEM, keyPEM []byte, err error)
- func GenerateClientCert(caCertPEM, caKeyPEM []byte) (certPEM, keyPEM []byte, err error)
- func GenerateClientCertForCN(caCertPEM, caKeyPEM []byte, commonName string) (certPEM, keyPEM []byte, err error)
- func GenerateServerCert(caCertPEM, caKeyPEM []byte, hosts []string) (certPEM, keyPEM []byte, err error)
- func LoadCAKey(caFile string) ([]byte, error)
- func LoadTLSConfig(certFile, keyFile, caFile string) (*tls.Config, error)
- func LoadTLSConfigOptionalClient(certFile, keyFile, caFile string) (*tls.Config, error)
- type ClientLedger
- func (l *ClientLedger) ActiveCount() int
- func (l *ClientLedger) Add(r ClientRecord) error
- func (l *ClientLedger) Get(fingerprint string) (ClientRecord, bool)
- func (l *ClientLedger) IsRevoked(fingerprint string) bool
- func (l *ClientLedger) List() []ClientRecord
- func (l *ClientLedger) Revoke(fingerprint, reason string) (bool, error)
- func (l *ClientLedger) RevokeAll(reason string) (int, error)
- type ClientRecord
- type EnrollmentManager
- func (m *EnrollmentManager) CACert() []byte
- func (m *EnrollmentManager) CAKey() []byte
- func (m *EnrollmentManager) Count() int
- func (m *EnrollmentManager) Enroll(token string) (caCert, clientCert, clientKey []byte, err error)
- func (m *EnrollmentManager) GenerateToken() (string, error)
- func (m *EnrollmentManager) Ledger() *ClientLedger
- func (m *EnrollmentManager) RenewClient(commonName string) (clientCert, clientKey []byte, notAfter time.Time, err error)
- func (m *EnrollmentManager) RevokeAll()
- func (m *EnrollmentManager) SetLedger(l *ClientLedger)
- func (m *EnrollmentManager) SetTTL(ttl time.Duration)
- func (m *EnrollmentManager) ValidToken(token string) bool
- type FingerprintTracker
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func BootstrapServerCerts ¶
func BootstrapServerCerts(certFile, keyFile, caFile string, hosts []string) (caCertPEM, serverCertPEM []byte, err error)
BootstrapServerCerts ensures a CA and server certificate exist at the given paths. If any file is missing, it generates a new CA (or reuses an existing one), issues a fresh server cert for the provided hosts, and writes all three files with mode 0600. Idempotent: if everything already exists, it returns (caCert, serverCert) unchanged.
Returns the PEM-encoded CA cert and the PEM-encoded server cert for any caller that needs to log fingerprints or derive the enrollment manager.
func CertCommonName ¶ added in v0.2.0
CertCommonName extracts the Subject Common Name from a PEM cert.
func CertFingerprintSHA256 ¶
CertFingerprintSHA256 returns the SHA-256 fingerprint of a PEM-encoded certificate as a lowercase hex string prefixed with "sha256:". This is what the operator pastes into Culvert's admin UI for TOFU verification.
func CertNotAfter ¶ added in v0.2.0
CertNotAfter decodes a PEM cert and returns its NotAfter timestamp. Exported because the CLI's `sluice cert expiry` needs to compute days-remaining without re-implementing x509 parsing.
func GenerateCA ¶
GenerateCA creates a new self-signed CA certificate and private key. Returns PEM-encoded cert and key.
func GenerateClientCert ¶
GenerateClientCert creates a client certificate signed by the given CA with a generic "Sluice Client" common name.
func GenerateClientCertForCN ¶ added in v0.2.0
func GenerateClientCertForCN(caCertPEM, caKeyPEM []byte, commonName string) (certPEM, keyPEM []byte, err error)
GenerateClientCertForCN creates a client certificate with a specific Common Name. Used by RenewClient so the renewed cert keeps the same identity as the presented cert.
func GenerateServerCert ¶
func GenerateServerCert(caCertPEM, caKeyPEM []byte, hosts []string) (certPEM, keyPEM []byte, err error)
GenerateServerCert creates a server certificate signed by the given CA. Returns PEM-encoded cert and key.
func LoadTLSConfig ¶
LoadTLSConfig creates a tls.Config for a gRPC server requiring mTLS. All clients MUST present a valid client cert (RequireAndVerifyClientCert).
func LoadTLSConfigOptionalClient ¶
LoadTLSConfigOptionalClient returns a tls.Config that verifies client certs when presented but does not require them. Used for gRPC servers that must accept both authenticated (Sanitize, Health) and unauthenticated (Enroll) RPCs on the same port — the per-RPC interceptor enforces auth.
Types ¶
type ClientLedger ¶ added in v0.2.0
type ClientLedger struct {
// contains filtered or unexported fields
}
ClientLedger is the persisted set of issued + revoked client certs. Access is serialized via a mutex; all mutating operations fsync to disk before returning so the in-memory set never diverges from /data/clients.json.
func NewClientLedger ¶ added in v0.2.0
func NewClientLedger(path string) (*ClientLedger, error)
NewClientLedger loads (or creates) the ledger at path. If the file doesn't exist, an empty ledger is returned and persisted on first write.
func (*ClientLedger) ActiveCount ¶ added in v0.2.0
func (l *ClientLedger) ActiveCount() int
ActiveCount returns the number of non-revoked, non-expired records.
func (*ClientLedger) Add ¶ added in v0.2.0
func (l *ClientLedger) Add(r ClientRecord) error
Add records a newly-issued cert. Called from Enroll + RenewCert paths. Fingerprint collisions overwrite (matches renewal semantics).
func (*ClientLedger) Get ¶ added in v0.2.0
func (l *ClientLedger) Get(fingerprint string) (ClientRecord, bool)
Get returns a copy of the record for fingerprint, or ok=false if unknown.
func (*ClientLedger) IsRevoked ¶ added in v0.2.0
func (l *ClientLedger) IsRevoked(fingerprint string) bool
IsRevoked is the hot-path check called from the mTLS interceptor on every RPC. Read-lock only; ledger writes are rare.
func (*ClientLedger) List ¶ added in v0.2.0
func (l *ClientLedger) List() []ClientRecord
List returns a snapshot of all records (copies, safe to mutate).
func (*ClientLedger) Revoke ¶ added in v0.2.0
func (l *ClientLedger) Revoke(fingerprint, reason string) (bool, error)
Revoke marks a fingerprint as revoked. Returns true if the fingerprint was previously active (i.e. a meaningful revocation), false if unknown or already-revoked (idempotent behaviour the proto documents).
type ClientRecord ¶ added in v0.2.0
type ClientRecord struct {
Fingerprint string `json:"fingerprint"` // "sha256:" + hex, unique key
Name string `json:"name,omitempty"` // reserved for future use
CommonName string `json:"common_name"` // x509 CN at issue time
IssuedAtUnix int64 `json:"issued_at_unix"` // cert NotBefore
NotAfterUnix int64 `json:"not_after_unix"` // cert NotAfter
RevokedAtUnix int64 `json:"revoked_at_unix"` // 0 if not revoked
RevokeReason string `json:"revoke_reason,omitempty"`
}
ClientRecord is one row in the ledger.
func (ClientRecord) Active ¶ added in v0.2.0
func (r ClientRecord) Active() bool
Active reports whether this cert is currently valid for auth: unrevoked and unexpired.
func (ClientRecord) IsExpired ¶ added in v0.2.0
func (r ClientRecord) IsExpired() bool
IsExpired reports whether the cert is past its NotAfter.
func (ClientRecord) IsRevoked ¶ added in v0.2.0
func (r ClientRecord) IsRevoked() bool
IsRevoked reports whether this record has been revoked.
type EnrollmentManager ¶
type EnrollmentManager struct {
// contains filtered or unexported fields
}
EnrollmentManager handles one-time enrollment tokens for Culvert integration. Tokens are stored as SHA-256 hashes at rest with a TTL (default 24h). Tokens are single-use: on successful Enroll, the entry is removed.
func NewEnrollmentManager ¶
func NewEnrollmentManager(caCert, caKey []byte, logger *slog.Logger) (*EnrollmentManager, error)
NewEnrollmentManager creates a manager. If caCert/caKey are nil, it generates a new CA.
func (*EnrollmentManager) CACert ¶
func (m *EnrollmentManager) CACert() []byte
CACert returns the PEM-encoded CA certificate used to sign enrolled clients.
func (*EnrollmentManager) CAKey ¶
func (m *EnrollmentManager) CAKey() []byte
CAKey returns the PEM-encoded CA private key. Handle with care.
func (*EnrollmentManager) Count ¶
func (m *EnrollmentManager) Count() int
Count returns the number of currently-valid tokens (for telemetry).
func (*EnrollmentManager) Enroll ¶
func (m *EnrollmentManager) Enroll(token string) (caCert, clientCert, clientKey []byte, err error)
Enroll consumes a token and returns CA cert + client cert + client key. Returns an error if the token is invalid, consumed, or expired. Successful enrollment removes the token entry (consume-and-delete).
func (*EnrollmentManager) GenerateToken ¶
func (m *EnrollmentManager) GenerateToken() (string, error)
GenerateToken creates a new one-time enrollment token. Returns the plaintext token to the caller (the only time it's visible) and stores only the hash.
func (*EnrollmentManager) Ledger ¶ added in v0.2.0
func (m *EnrollmentManager) Ledger() *ClientLedger
Ledger returns the wired ledger (may be nil).
func (*EnrollmentManager) RenewClient ¶ added in v0.2.0
func (m *EnrollmentManager) RenewClient(commonName string) (clientCert, clientKey []byte, notAfter time.Time, err error)
RenewClient mints a fresh client certificate for an already-enrolled caller. The caller is identified by its presented cert's Common Name (passed via commonName). Returns the new cert + key plus the cert's NotAfter so callers can report days_until_expiry without re-parsing.
This does NOT revoke the presented cert — operators who want to revoke the old cert after a successful renewal must call ledger.Revoke explicitly.
func (*EnrollmentManager) RevokeAll ¶
func (m *EnrollmentManager) RevokeAll()
RevokeAll clears every outstanding token (emergency rotation).
func (*EnrollmentManager) SetLedger ¶ added in v0.2.0
func (m *EnrollmentManager) SetLedger(l *ClientLedger)
SetLedger wires a persistent client cert ledger to the manager. Subsequent Enroll / RenewClient calls will record issued certs.
func (*EnrollmentManager) SetTTL ¶
func (m *EnrollmentManager) SetTTL(ttl time.Duration)
SetTTL overrides the token time-to-live. Must be > 0.
func (*EnrollmentManager) ValidToken ¶
func (m *EnrollmentManager) ValidToken(token string) bool
ValidToken reports whether a plaintext token is known and unconsumed (and unexpired).
type FingerprintTracker ¶ added in v0.2.0
type FingerprintTracker struct {
// contains filtered or unexported fields
}
FingerprintTracker is concurrency-safe.
func NewFingerprintTracker ¶ added in v0.2.0
func NewFingerprintTracker(current string) *FingerprintTracker
NewFingerprintTracker creates a tracker with a single current fingerprint.
func (*FingerprintTracker) Accepts ¶ added in v0.2.0
func (t *FingerprintTracker) Accepts(fp string) bool
Accepts reports whether fp (sha256:... hex) matches EITHER the current fingerprint OR the rotated-out previous fingerprint during its grace window. Used by any code path that needs to verify a pinned fingerprint (nothing on the server uses this today, but the helper is here for symmetry with what Culvert's client implements).
func (*FingerprintTracker) Current ¶ added in v0.2.0
func (t *FingerprintTracker) Current() string
Current returns the current server cert fingerprint.
func (*FingerprintTracker) Rotate ¶ added in v0.2.0
func (t *FingerprintTracker) Rotate(newCurrent string, grace time.Duration)
Rotate records a new current fingerprint and keeps the old one acceptable for `grace`. Passing grace <= 0 drops the previous fingerprint immediately (hard cutover — forces re-enrollment).
func (*FingerprintTracker) Snapshot ¶ added in v0.2.0
func (t *FingerprintTracker) Snapshot() (current, previous string, previousUntilUnix int64)
Snapshot returns the current fingerprint, the rotated-out previous fingerprint (or empty string), and the unix timestamp at which the previous fingerprint stops being acceptable (0 = no active rotation).
Callers handling Health responses should use Snapshot; callers handling TLS verification should use Accepts.