auth

package
v0.7.9-rc.2 Latest Latest
Warning

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

Go to latest
Published: May 11, 2026 License: MIT Imports: 25 Imported by: 0

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

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

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

func BuildContainerSAN(containerID string) (*url.URL, error)

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:

  1. CA keypair — signs server and client certs, never enters containers
  2. ES256 signing keypair — for private_key_jwt auth with Hydra
  3. Server cert — signed by the CLI CA, bind-mounted into CP
  4. Client cert — signed by the CLI CA, used for mTLS to AdminService

func EnsureHydraSecret

func EnsureHydraSecret() (string, error)

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

func RotateAuthMaterial(forceSigningKey bool) error

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

type AgentCert struct {
	CertPEM    []byte
	KeyPEM     []byte
	Thumbprint [sha256.Size]byte
}

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.

func (AgentCert) GoString

func (AgentCert) GoString() string

GoString redacts so fmt.Sprintf("%#v", cert) (and any logger that uses Go-syntax representation) also does not leak KeyPEM.

func (AgentCert) String

func (AgentCert) String() string

String redacts every field so AgentCert can never accidentally leak the per-agent private key via fmt.Sprintf("%v", cert) or zerolog.

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

func MustAgentName(s string) AgentName

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

func NewAgentName(s string) (AgentName, error)

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.

func (AgentName) IsZero

func (a AgentName) IsZero() bool

IsZero reports whether this is the zero value (uninitialized AgentName{}). The constructors always reject empty input, so a real AgentName is never zero — IsZero is for callers who hold a value of unknown provenance.

func (AgentName) String

func (a AgentName) String() string

String returns the underlying short name.

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.

Jump to

Keyboard shortcuts

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