Documentation
¶
Overview ¶
Package auth provides CLI-side authentication infrastructure for communicating with the clawker control plane. The CLI is the trust orchestrator — it generates all key material and bind-mounts the public halves into the CP container.
Index ¶
- Constants
- func BuildAgentAssertion(audience string, signingKey *ecdsa.PrivateKey) (string, error)
- func BuildContainerSAN(containerID string) (*url.URL, error)
- func BuildSignedAssertion(claims AssertionClaims, signingKey *ecdsa.PrivateKey) (string, error)
- func CACert() (*x509.Certificate, error)
- func CanonicalAgentCN(project ProjectSlug, agent AgentName) string
- func ContainerIDFromCert(cert *x509.Certificate) (string, bool)
- func EnsureAuthMaterial() error
- func EnsureHydraSecret() (string, error)
- func LoadClientCert() (tls.Certificate, error)
- func LoadSigningKey() (*ecdsa.PrivateKey, error)
- func ReadJWK() (json.RawMessage, error)
- func RotateAuthMaterial(forceSigningKey bool) error
- func ValidateAssertionClaims(claims AssertionClaims) error
- type AgentCert
- type AgentName
- type AssertionClaims
- type AuthFileStatus
- type ProjectSlug
Constants ¶
const AgentAssertionTTL = 24 * time.Hour
AgentAssertionTTL bounds how long a CLI-signed agent assertion stays valid at Hydra. Sized for typical container session length: a single assertion can refresh access tokens for a full working day before clawkerd's parent container would be expected to be torn down.
const ContainerSANScheme = "urn:clawker:container:"
ContainerSANScheme is the URI SAN scheme used to bind a leaf cert to the docker container_id it was minted for. The Register handler reads this SAN at handler entry and rejects any cert presenting a container_id that doesn't match the cert's structural binding.
Example: urn:clawker:container:abc123def456...
Variables ¶
This section is empty.
Functions ¶
func BuildAgentAssertion ¶
func BuildAgentAssertion(audience string, signingKey *ecdsa.PrivateKey) (string, error)
BuildAgentAssertion signs an RFC 7523 client_assertion identifying the calling clawkerd as the clawker-agent OAuth2 client at Hydra. The assertion is consumed by clawkerd at boot to obtain the access token it needs for AgentService.Connect; it is NOT used for per-agent identity (that comes from the mTLS cert thumbprint at Connect).
Same private key as the CLI client (`clawker-cli`) — distinct client_id + scope keeps the AuthZ surface clean even though the signing key is shared. See `RegisterAgentClient` for the Hydra-side counterpart.
func BuildContainerSAN ¶
BuildContainerSAN composes the URI SAN for a given container_id. The returned *url.URL embeds in x509.Certificate.URIs.
Docker container IDs are 64-char hex strings (truncated forms in CLI output use prefixes of the same alphabet). We enforce hex-only here so a malformed ID (whitespace, slashes, control chars) cannot ride into the cert SAN — the Register handler reads this back via ContainerIDFromCert and uses it to look up a docker container, so an unvalidated value is a producer-side bug surface.
func BuildSignedAssertion ¶
func BuildSignedAssertion(claims AssertionClaims, signingKey *ecdsa.PrivateKey) (string, error)
BuildSignedAssertion creates a signed JWT assertion per RFC 7523 for use in private_key_jwt client authentication with Hydra. The assertion is signed with ES256 (ECDSA P-256). Returns the signed JWT string.
func CACert ¶
func CACert() (*x509.Certificate, error)
CACert reads the CLI CA certificate. The CLI uses this to verify server certs it signed.
func CanonicalAgentCN ¶
func CanonicalAgentCN(project ProjectSlug, agent AgentName) string
CanonicalAgentCN composes the canonical agent identity used as the cert's CN and as the agentregistry row's pre-computed canonical_cn column. Three-segment for a scoped project ("clawker.<project>.<agent>"), two-segment for the unscoped/ empty-project case ("clawker.<agent>") to match docker.ContainerName naming.
Takes typed AgentName + ProjectSlug values so the caller can't pass a canonical form, a dot-containing name, or arbitrary characters here — the constructors (NewAgentName / NewProjectSlug) enforce that contract once and the function trusts the values from there on.
Lives in this package because it is purely a function of consts.NamePrefix and the (project, agent) tuple — every layer that needs to compose or verify the canonical (cert minting, agent handler CN cross-check, registry lookup) reaches for this so the rule has a single home.
func ContainerIDFromCert ¶
func ContainerIDFromCert(cert *x509.Certificate) (string, bool)
ContainerIDFromCert extracts the container_id encoded as a URI SAN of the form urn:clawker:container:<id>. Returns ("", false) when no such SAN is present so callers can branch on a clean missing-binding signal rather than parsing strings.
func EnsureAuthMaterial ¶
func EnsureAuthMaterial() error
EnsureAuthMaterial checks for existing auth material and creates any that is missing. Idempotent — safe to call on every CLI invocation. Directories are created by the consts accessors.
The CLI is the root of trust. It generates:
- CA keypair — signs server and client certs, never enters containers
- ES256 signing keypair — for private_key_jwt auth with Hydra
- Server cert — signed by the CLI CA, bind-mounted into CP
- Client cert — signed by the CLI CA, used for mTLS to AdminService
func EnsureHydraSecret ¶
EnsureHydraSecret reads the persisted Hydra system secret from disk, or generates a new 32-byte random hex secret and writes it with 0600 permissions. The secret is generated once and reused across restarts.
Read errors are NOT collapsed into "regenerate" — a transient I/O fault that recovers between read and write would silently rotate the secret and invalidate every previously-issued Hydra token. Only os.ErrNotExist (first run) and an empty file (corruption fallback) trigger regeneration.
func LoadClientCert ¶
func LoadClientCert() (tls.Certificate, error)
LoadClientCert reads the CLI's mTLS client certificate and key as a tls.Certificate for use with grpc.WithTransportCredentials.
func LoadSigningKey ¶
func LoadSigningKey() (*ecdsa.PrivateKey, error)
LoadSigningKey reads the CLI's ES256 private key.
func ReadJWK ¶
func ReadJWK() (json.RawMessage, error)
ReadJWK reads the CLI's public signing key as raw JSON bytes.
func RotateAuthMaterial ¶
RotateAuthMaterial regenerates all auth material unconditionally. Unlike EnsureAuthMaterial which is idempotent (no-op if files exist), this deletes existing material and creates fresh keypairs.
The server cert is always regenerated because it depends on the CA. The signing key is regenerated only if forceSigningKey is true (it requires re-registering the CLI client with Hydra on next CP start).
func ValidateAssertionClaims ¶
func ValidateAssertionClaims(claims AssertionClaims) error
ValidateAssertionClaims checks that all required RFC 7523 claims are present. Returns an error describing the first missing or invalid claim.
Types ¶
type AgentCert ¶
AgentCert is the co-derived material produced by MintAgentCert: the PEM-encoded cert, its matching key, and the SHA-256 thumbprint over the cert DER. The three pieces are only meaningful as a unit — pairing a thumbprint with a different cert breaks the cert-swap defense at AgentService.Connect.
The String/GoString methods deliberately redact the contents so the struct (which carries the per-agent private key) can never leak via `%v`, `%+v`, `%#v`, or zerolog's interface logger. Callers that need the raw bytes must read the fields directly.
func MintAgentCert ¶
func MintAgentCert(caCertPath, caKeyPath string, project ProjectSlug, agent AgentName, containerID string) (*AgentCert, error)
MintAgentCert generates a per-agent mTLS leaf signed by the CLI CA at caCertPath/caKeyPath. The returned material is meant to be delivered to the agent container's writable layer via Docker's CopyToContainer API (see consts.BootstrapDir) and never persisted on the host.
CN is composed inside the function from (project, agent) via CanonicalAgentCN — callers MUST pass the user-typed short names and let the helper apply the consts.NamePrefix prefix and the 2-vs-3-segment rule. This keeps every cert minted by the CLI in a single canonical shape so the agent handler's CN cross-check has a single equality to enforce.
containerID is the docker container_id this cert is being minted for. MintAgentCert embeds it as a URI SAN (urn:clawker:container:<id>) so the CP-side Register handler can read the binding directly off the peer cert at handler entry. A leaked cert presented for a different container_id is rejected because the SAN won't match the docker container the peer IP resolves to.
The 24h lifetime is intentional — thumbprint pinning at registry lookup time makes longer-lived certs safe, but a tight ceiling caps the blast radius if a leaf leaks. CP captures the thumbprint at Register handler entry from the live mTLS peer (cert.Raw → SHA-256) and writes it into the agentregistry row alongside container_id; subsequent Sessions presenting a different cert for the same container_id are rejected as untrusted.
Returns *AgentCert (nil on error) so a caller that ignores the error cannot accidentally log the redacted zero-value as a successful cert.
project + agent are typed (auth.ProjectSlug, auth.AgentName) so the caller has gone through NewProjectSlug / NewAgentName and the canonical-form / dot-in-name / charset checks have already run. A raw-string caller now produces a compile error instead of a silently- malformed cert subject downstream.
type AgentName ¶
type AgentName struct {
// contains filtered or unexported fields
}
AgentName is the user-typed short agent name (e.g. "dev"). It is distinct from a `string` so callers cannot accidentally pass:
- the canonical form ("clawker.foo.bar") — the helpers compose that themselves;
- a name containing "." — segment counting breaks downstream wherever the canonical "clawker.<project>.<agent>" form is parsed or filtered;
- arbitrary characters that wouldn't survive Docker's container/ volume naming or the canonical-CN compose rules.
Construction goes through NewAgentName, which enforces the contract. In-package code that already trusts a value can convert via the String() accessor; out-of-package callers can't read the underlying string except through that accessor (no exported field).
func MustAgentName ¶
MustAgentName wraps a string that the caller has ALREADY validated (e.g. read back from a registry entry that was inserted via a typed path, or composed in tests). Panics if the input fails the AgentName contract — invariant violation that must surface loudly, not silently malformed identity downstream.
Production code should prefer NewAgentName + error handling at every wire/CLI boundary. MustAgentName exists for places where the validation already ran upstream and the boundary code holds a raw string (e.g. existing struct fields whose typing isn't yet migrated).
func NewAgentName ¶
NewAgentName parses + validates a user-typed agent short name and returns a typed value. Returns an error on empty input, names that look like the canonical form ("clawker.<...>"), names containing disallowed characters, or names exceeding the length cap. The error message names the offending input so a CLI surfaces the violation without the user guessing what was wrong.
type AssertionClaims ¶
type AssertionClaims struct {
// Issuer (iss) — must be the client_id.
Issuer string
// Subject (sub) — must be the client_id.
Subject string
// Audience (aud) — must be the Hydra token endpoint URL.
Audience string
// JWTID (jti) — cryptographically random unique ID.
JWTID string
// ExpiresIn is the duration until expiration (typically 30-60s).
ExpiresInSeconds int
}
AssertionClaims holds the claims for a client assertion JWT per RFC 7523.
type AuthFileStatus ¶
type AuthFileStatus struct {
Name string // human-readable name (e.g., "CA certificate")
Path string // filesystem path
Exists bool
Mode os.FileMode // only valid if Exists
ParseError error // non-nil if stat/read/parse failed (not os.ErrNotExist)
Expires time.Time // only valid for certificates
Expired bool // only valid for certificates
}
AuthFileStatus describes the state of a single auth material file.
func CheckAuthMaterial ¶
func CheckAuthMaterial() ([]AuthFileStatus, error)
CheckAuthMaterial inspects all auth material files and returns their status.
type ProjectSlug ¶
type ProjectSlug struct {
// contains filtered or unexported fields
}
ProjectSlug is the user-typed project slug (e.g. "myapp"). Like AgentName but allows the empty value (matches docker.ContainerName's 2-segment naming case where no project is configured).
func MustProjectSlug ¶
func MustProjectSlug(s string) ProjectSlug
MustProjectSlug is the unchecked-but-assertive companion to NewProjectSlug. See MustAgentName for the rationale and usage rule.
func NewProjectSlug ¶
func NewProjectSlug(s string) (ProjectSlug, error)
NewProjectSlug parses + validates a user-typed project slug. Empty input is allowed and returns the zero value (interpreted downstream as "unscoped project, 2-segment naming"). Non-empty inputs must satisfy the same charset/length contract as AgentName.
func (ProjectSlug) IsEmpty ¶
func (p ProjectSlug) IsEmpty() bool
IsEmpty reports whether the slug is the empty/unscoped value. Use this rather than `s.String() == ""` so a future refactor that changes the underlying representation doesn't silently break the empty check.
func (ProjectSlug) String ¶
func (p ProjectSlug) String() string
String returns the underlying slug; empty for the unscoped case.