Help us shape the future of Go! Take the 2026 Go Developer Survey

identity

package
v0.53.16 Latest Latest
Warning

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

Go to latest
Published: Oct 19, 2026 License: Apache-2.0 Imports: 42 Imported by: 0

Documentation

Overview

Package identity provisions PocketID owner and break-glass accounts.

PocketID v2 is passkey-only: there is no password field on user creation. The owner-activation flow is therefore three calls:

  1. POST /api/users -- create the user record (no password).
  2. PUT /api/users/:id/user-groups -- add to the "owners" group.
  3. POST /api/users/:id/one-time-access-token -- issue an enrollment token.

The token is rendered into a setup URL of the form

https://id.<domain>/lc/<token>?redirect=%2Fsettings%2Faccount

which the owner clicks once to register a WebAuthn credential. The token is single-use and consumed by PocketID on first redemption.

The local provisioner only supports Source=="local" (a daily-admin owner provisioned locally on the first node). Source=="cloud" is orchestrator managed by TechStack/kombify Cloud and must not call this local provisioner.

Index

Constants

View Source
const (
	// HomeEnrollmentPort is the catalog listener stackkit-server-home-enroll
	// (foundation/architecture_v2_catalog.cue): TLS 1.3 on the node site
	// address, exposure "lan".
	HomeEnrollmentPort = 9443
	// HomeEnrollmentServerName is the enrollment listener's certificate name.
	HomeEnrollmentServerName = localevidence.HomeEnrollmentServerName
	// HomePairingURIScheme prefixes the pairing payload shown as code and QR.
	HomePairingURIScheme = "stackkit-enroll"
)
View Source
const (
	HomeOverlayInterfaceName    = "wg-stackkit"
	HomeOverlayListenPort       = 51820
	HomeOverlayKeepaliveSeconds = 25
)

The kit-owned WireGuard overlay realizes the remote-private exposure in Standard Mode (#HomeWireGuardOverlayContractV1 in foundation/architecture_v2.cue). Its peers are exactly the enrolled household devices that registered an overlay public key and are neither revoked nor expired. The node private key is generated here into local custody and never leaves it: the rendered interface configuration loads it from the custody file at interface start.

View Source
const (
	// HomePairingLifetime is the #DeviceEnrollmentPolicy pairing code lifetime.
	HomePairingLifetime = 300 * time.Second
	// HomeOwnerStepUpFreshness is how recent the PocketID owner authentication
	// must be to issue a pairing code.
	HomeOwnerStepUpFreshness = 60 * time.Second
)
View Source
const (
	HomePublicTrustSchema    = "stackkit.home-human-device-trust/v1"
	HomeDeviceCredentialType = "stackkit-device-credential+jwt"
)
View Source
const (
	HomeVerifierPath   = "/api/v1/identity/home/verify"
	HomeEnrollmentPath = "/api/v1/identity/home/enroll"
)
View Source
const StaticAPIKeyFilename = "pocketid-static-api-key"

StaticAPIKeyFilename is the basename of the on-disk file that holds the PocketID STATIC_API_KEY for a homelab. It lives under <homelab>/.stackkit/.

Variables

View Source
var (
	ErrHomeDeviceRevoked  = errors.New("identity: this device key was revoked and cannot enroll again; generate a new key")
	ErrHomeDeviceConflict = errors.New("identity: another active device already uses this name")
	ErrHomeDeviceNotFound = errors.New("identity: no enrolled device has this id")
)
View Source
var (
	ErrHomeOverlayDeviceInactive = errors.New("identity: only an enrolled, unrevoked, unexpired device can join the overlay")
	ErrHomeOverlayKeyInvalid     = errors.New("identity: overlay public key must be a base64 32-byte WireGuard key")
	ErrHomeOverlayKeyInUse       = errors.New("identity: another device already registered this overlay key")
	ErrHomeOverlayPoolExhausted  = errors.New("identity: the overlay address pool has no free address")
)
View Source
var (
	ErrHomeAccessDenied = errors.New("identity: current independent Home human and device proof required")
)
View Source
var (
	ErrHomePairingDenied = errors.New("identity: Home pairing requires a fresh Owner step-up, a LAN client and an unused pairing code")
)

Functions

func BindHomePublicTrust

func BindHomePublicTrust(root string, trust HomePublicTrust) error

BindHomePublicTrust is local Owner publication of current public trust. It does not enroll a device or mint a credential.

func HomeEnrollmentCertificateSource added in v0.53.14

func HomeEnrollmentCertificateSource(root string) func(*tls.ClientHelloInfo) (*tls.Certificate, error)

HomeEnrollmentCertificateSource issues the listener certificate through step-ca on first use and renews it an hour before expiry. Without step-ca or owner custody the TLS handshake fails, so the listener stays closed.

func HomeEnrollmentHandler

func HomeEnrollmentHandler(root string) http.Handler

HomeEnrollmentHandler is the enrollment route for any listener other than NewHomeEnrollmentServer. It refuses every request: an enrollment must arrive on the dedicated LAN listener.

func HomeVerifierHandler

func HomeVerifierHandler(root string) http.Handler

HomeVerifierHandler is mounted on the existing server. It does not create a listener or treat API keys, workload TLS, or forwarded headers as identity.

func NewHomeEnrollmentServer added in v0.53.14

func NewHomeEnrollmentServer(root, listen string, issue HomeDeviceIssuer, certificate func(*tls.ClientHelloInfo) (*tls.Certificate, error)) (*http.Server, error)

NewHomeEnrollmentServer is the dedicated LAN enrollment listener. It serves only POST HomeEnrollmentPath over TLS 1.3 with a step-ca issued server certificate. Admission uses the transport peer address of the accepted connection; forwarded headers are never read. The same handler mounted on any other listener refuses every request.

func ObservedLANAddress added in v0.53.13

func ObservedLANAddress(address string) bool

ObservedLANAddress classifies a transport peer address the server observed (a connection RemoteAddr or the SSH client address of a node-local CLI). It admits loopback, private (RFC 1918 / ULA) and link-local peers only; it is never read from a request header or body.

func ReadStaticAPIKey

func ReadStaticAPIKey(baseDir string) (string, error)

ReadStaticAPIKey returns the existing STATIC_API_KEY for a homelab without generating one when the file is missing. This supports reading immutable v0.6 artifacts during migration; current native generation uses owner custody.

func RenderHomeOverlayConfig added in v0.53.13

func RenderHomeOverlayConfig(root string, now time.Time) ([]byte, error)

RenderHomeOverlayConfig renders the node's wg-quick interface configuration. It carries public keys and addresses only: the private key is loaded from local custody by PostUp. Peers of revoked or expired devices are omitted, so the next render and interface reload removes their access.

Types

type BackupEncryptionKeyCredential

type BackupEncryptionKeyCredential struct {
	// Engine is the snapshot engine name. Always "kopia" today; here so a
	// future format change does not require a bundle break.
	Engine string

	// Passphrase is the cleartext value passed to Kopia at
	// repository-create time. Goes into the recovery bundle; never
	// logged. Same handling discipline as TinyAuthStaticCredential.PasswordPlain.
	Passphrase string

	// RepositoryHint is a human-readable pointer to where the data
	// lives ("b2://kombify-vault/host-a", "sftp://u@host:/repo", …).
	// Optional. Helps a recovery operator who has only the bundle find
	// the offsite repo.
	RepositoryHint string
}

BackupEncryptionKeyCredential carries the Kopia repository passphrase used to encrypt snapshots. It is the third break-glass layer (after PocketID-admin and TinyAuth-static) and lives in the recovery bundle so a host loss does not equal a data loss.

Unlike the other layers, this credential is OPTIONAL: a node that has not enabled the rollout does not have one, and the bundle's BackupEncryptionKey field is omitted entirely.

type BackupEncryptionKeyGenerator

type BackupEncryptionKeyGenerator struct {
	// Engine is recorded into the credential. Defaults to "kopia".
	Engine string

	// RepositoryHint is recorded verbatim. Optional.
	RepositoryHint string
}

BackupEncryptionKeyGenerator creates a fresh repository passphrase. The output is suitable for direct use as KOPIA_PASSWORD.

func (*BackupEncryptionKeyGenerator) Generate

Generate produces 32 random bytes encoded as base64 (~43 characters). Same shape as TinyAuthStaticGenerator's plaintext: high entropy, safe for the YAML bundle, copy-pasteable in a recovery scenario.

type BackupEncryptionKeyPayload

type BackupEncryptionKeyPayload struct {
	// Engine is the snapshot engine that owns the passphrase. "kopia" today;
	// the field is here so a future engine swap (extremely unlikely given
	// ADR-0016) does not require a bundle-format break.
	Engine string `yaml:"engine"`

	// Passphrase is the cleartext value the operator (or stackkit apply)
	// generated. Treat as secret. Same handling rules as the TinyAuth
	// plaintext password on the same bundle.
	Passphrase string `yaml:"passphrase"`

	// RepositoryHint is a human-readable pointer to where the data lives
	// (e.g. "b2://kombify-vault/host-a"). Optional. The recovery
	// operator typically already knows this from out-of-band sources;
	// the hint is here so a recovery scenario starting from "I only have
	// the bundle" is still tractable.
	RepositoryHint string `yaml:"repositoryHint,omitempty"`
}

BackupEncryptionKeyPayload escrows the Kopia repository passphrase used to encrypt snapshots. It is the only path back to the data if the host disk is lost: an operator decrypts the bundle and re-attaches the offsite repo with this passphrase.

The plaintext passphrase lives only inside the encrypted .age bundle. Mode 0600 on the .txt convenience copy plus the mandatory recovery passphrase on the .age file are the same protections used for the other layers.

type BreakGlassCredential

type BreakGlassCredential struct {
	// Username is the synthetic local handle, of the form
	// "bg-<nodename>@local". It is unique per node so multiple homelabs
	// don't collide on a shared PocketID.
	Username string

	// SetupToken is the raw one-time-access token issued by PocketID. It
	// is single-use and consumed when the recoverer registers a passkey.
	SetupToken string

	// SetupURL is the full URL the recoverer clicks to complete WebAuthn
	// enrollment, of the form "https://id.<domain>/lc/<t>?redirect=%2Fsettings%2Faccount".
	SetupURL string

	// Group is the PocketID group the account was added to (typically
	// "owners").
	Group string

	// UserID is the PocketID-assigned UUID for this account, used by later
	// rotation/revoke operations.
	UserID string
}

BreakGlassCredential is the materialized result of a break-glass admin provisioning. Username, SetupToken, SetupURL and UserID are intended to be embedded into the recovery bundle (Task 9).

type BreakGlassGenerator

type BreakGlassGenerator struct {
	// Client is the PocketID admin-API client (real or fake). Reuses the
	// same PocketIDClient interface defined in owner.go.
	Client PocketIDClient

	// NodeName identifies the homelab firstnode this break-glass account
	// belongs to. Required.
	NodeName string

	// PocketIDURL is the public origin of the PocketID instance, used to
	// build SetupURL. Example: "https://id.example.com" (no trailing slash).
	PocketIDURL string

	// OwnersGroup is the name of the group the break-glass admin is added
	// to. Defaults to "owners" when empty.
	OwnersGroup string

	// TokenTTL controls how long the one-time-access token remains valid.
	// Defaults to 365 days when zero.
	TokenTTL time.Duration
}

BreakGlassGenerator creates a per-node break-glass admin in PocketID and returns a credential bundle suitable for sealing into the recovery envelope.

func (*BreakGlassGenerator) Generate

Generate provisions a break-glass admin account on PocketID for the configured node and returns its BreakGlassCredential. The account is created with admin privileges and added to the owners group; a long-TTL one-time-access token is issued so the recoverer can enroll a WebAuthn credential when (and only when) they actually open the recovery bundle.

type BreakGlassSection

type BreakGlassSection struct {
	PocketIDAdmin       PocketIDAdminPayload        `yaml:"pocketidAdmin"`
	TinyAuthStatic      TinyAuthStaticPayload       `yaml:"tinyauthStatic"`
	BackupEncryptionKey *BackupEncryptionKeyPayload `yaml:"backupEncryptionKey,omitempty"`
}

BreakGlassSection holds the recovery layers.

PocketIDAdmin and TinyAuthStatic are mandatory — every node has them. BackupEncryptionKey is optional: it is only populated when a rollout has an encrypted snapshot repository and the operator has chosen to escrow the Kopia encryption passphrase here. Without that escrow, "lost host" equals "lost backups", which defeats the snapshot policy.

type BundleBuilder

type BundleBuilder struct {
	// NodeName identifies the homelab node and drives the bundle's
	// filename ("break-glass-<nodename>.age"). Required.
	NodeName string

	// Hostname is the node's network hostname (FQDN or short name); a
	// purely-documentation field but useful during recovery for matching
	// the bundle to physical hardware.
	Hostname string

	// ClusterRole is one of "main", "worker", or "storage". Required.
	ClusterRole string

	// PocketIDURL is the public origin of the PocketID instance, mirrored
	// into the bundle so the recovery operator knows which install this
	// belongs to.
	PocketIDURL string

	// PocketIDAdmin is the Layer-1 break-glass credential from Task 7.
	// Required.
	PocketIDAdmin *BreakGlassCredential

	// TinyAuthStatic is the Layer-2 break-glass credential from Task 8.
	// Required.
	TinyAuthStatic *TinyAuthStaticCredential

	// BackupEncryptionKey escrows the Kopia repository passphrase used by
	// the snapshot repository. Optional — only populated when snapshot
	// encryption is enabled. nil leaves the BackupEncryptionKey field out of the
	// emitted YAML entirely (yaml:"...,omitempty").
	BackupEncryptionKey *BackupEncryptionKeyCredential

	// BundleDir is the directory the .age and .txt files are written to.
	// Defaults to "/var/lib/stackkit/recovery" when empty.
	BundleDir string

	// Now is injectable for deterministic tests. Defaults to time.Now.
	Now func() time.Time
}

BundleBuilder aggregates the inputs needed to build a recovery bundle. Construct one, populate the credentials produced by the Task-7 and Task-8 generators, and call BuildAndSave.

func (*BundleBuilder) BuildAndSave

func (b *BundleBuilder) BuildAndSave(passphrase string) (*BundlePaths, error)

BuildAndSave validates the builder, marshals the bundle YAML, encrypts it with the supplied passphrase, and writes both the encrypted (.age, 0644) and plaintext (.txt, 0600) files. It returns the on-disk paths.

The plaintext file is convenience-only — the encrypted bundle is the disaster-recovery artifact that must be backed up out-of-band.

type BundleNode

type BundleNode struct {
	Name        string `yaml:"name"`
	Hostname    string `yaml:"hostname"`
	ClusterRole string `yaml:"clusterRole"` // "main"|"worker"|"storage"
	PocketIDURL string `yaml:"pocketidUrl"`
}

BundleNode identifies the homelab node this bundle belongs to. Including hostname and cluster role makes the bundle self-describing during a recovery — the operator can match the artifact to physical hardware without consulting external state.

type BundlePaths

type BundlePaths struct {
	// EncryptedPath is the .age file. This is the disaster-recovery
	// artifact that must be backed up out-of-band; it is safe to copy
	// to cloud storage, USB sticks, paper QR codes, etc.
	EncryptedPath string

	// PlaintextPath is the .txt file (mode 0600). Convenience-only — a
	// root-on-the-node operator can read it without redecrypting. Lives
	// next to the encrypted file in the bundle directory.
	PlaintextPath string
}

BundlePaths returns the on-disk locations of the written bundle files.

type BundlePayload

type BundlePayload struct {
	Version             int               `yaml:"version"`
	GeneratedAt         string            `yaml:"generatedAt"`
	Node                BundleNode        `yaml:"node"`
	BreakGlass          BreakGlassSection `yaml:"breakGlass"`
	RestoreInstructions string            `yaml:"restoreInstructions"`
}

BundlePayload is the structure that is serialized to YAML and then encrypted into the .age file. Field names match foundation/break-glass.cue #BundlePayload (camelCase).

type HomeAccessProof

type HomeAccessProof struct {
	HumanSession     string `json:"humanSession"`
	DeviceCredential string `json:"deviceCredential"`
	DPoP             string `json:"dpop"`
}

HomeAccessProof is a credential and must never be logged. HumanSession is the original PocketID ID token. DeviceCredential is signed by the Home device-authority issuer, never the workload mTLS issuer.

type HomeAuthentication

type HomeAuthentication struct {
	Subject   string    `json:"subject"`
	Device    string    `json:"device"`
	ExpiresAt time.Time `json:"expiresAt"`
}

HomeAuthentication is request-scoped admission, not privileged approval.

func AuthenticateHomeRequest

func AuthenticateHomeRequest(root, method, target string, proof HomeAccessProof) (HomeAuthentication, error)

AuthenticateHomeRequest reloads current signed Home trust for every request and durably consumes the DPoP proof. Workload mTLS, API keys and forwarded identity headers cannot satisfy this boundary.

type HomeConfirmation

type HomeConfirmation struct {
	JWKThumbprint string `json:"jkt"`
}

type HomeDevice added in v0.53.13

type HomeDevice struct {
	ID           string     `json:"id"`
	Name         string     `json:"name"`
	OwnerSubject string     `json:"ownerSubject"`
	SerialNumber string     `json:"serialNumber"`
	EnrolledAt   time.Time  `json:"enrolledAt"`
	NotAfter     time.Time  `json:"notAfter"`
	Revoked      bool       `json:"revoked"`
	RevokedAt    *time.Time `json:"revokedAt,omitempty"`
}

HomeDevice is the public registry record of one enrolled device key. It holds no private key, pairing code or certificate body.

func EnrollHomeDeviceWithPairing added in v0.53.13

func EnrollHomeDeviceWithPairing(ctx context.Context, root string, request HomeEnrollmentRequest, issue HomeDeviceIssuer, now time.Time) (HomeDevice, []byte, error)

EnrollHomeDeviceWithPairing completes the shared enrollment ceremony used by the State Console code and the CLI token fallback: it redeems the single-use pairing (Owner step-up, LAN, proof of possession), refuses a revoked key or a name held by another active device, has the device provisioner issue a certificate bounded by credentialTTLSeconds, and records the device.

func ListHomeDevices added in v0.53.13

func ListHomeDevices(root string) ([]HomeDevice, error)

ListHomeDevices returns enrolled and revoked devices, newest first.

func RevokeHomeDevice added in v0.53.13

func RevokeHomeDevice(root, id string, now time.Time) (HomeDevice, error)

RevokeHomeDevice permanently revokes a device key. Revocation only removes access, so it is a local Owner operation without a fresh step-up; the key can never enroll again.

type HomeDeviceIssuer added in v0.53.13

type HomeDeviceIssuer func(ctx context.Context, csrPEM []byte, ttlSeconds int) ([]byte, error)

HomeDeviceIssuer turns an admitted device CSR into a certificate chain through the step-ca device provisioner. Production uses localevidence.IssueHomeDeviceCertificate.

type HomeEnrollmentBootstrap added in v0.53.14

type HomeEnrollmentBootstrap struct {
	EnrollmentURL string `json:"enrollmentUrl"`
	TLSServerName string `json:"tlsServerName"`
	CARootSHA256  string `json:"caRootSha256"`
}

HomeEnrollmentBootstrap is the trust bootstrap a device needs before it sends its CSR: where the LAN enrollment listener is, which server name its certificate carries, and the step-ca root fingerprint to pin. It contains no secret; the pairing code is the only credential in the payload.

func LoadHomeEnrollmentBootstrap added in v0.53.14

func LoadHomeEnrollmentBootstrap(root string) (HomeEnrollmentBootstrap, error)

LoadHomeEnrollmentBootstrap derives the bootstrap from owner custody: the node site address the LAN resolver custody already binds, and the established step-ca root. It fails closed when either is missing or the address is not a LAN address.

func (HomeEnrollmentBootstrap) PairingURI added in v0.53.14

func (b HomeEnrollmentBootstrap) PairingURI(pairing HomePairing) string

PairingURI is the payload the step-up page renders as text and QR code.

type HomeEnrollmentGrant added in v0.53.10

type HomeEnrollmentGrant struct {
	OwnerSubject         string
	DeviceKeyThumbprint  string
	DeviceCommonName     string
	CSRDER               []byte
	PairingExpiresAt     time.Time
	CredentialTTLSeconds int
}

HomeEnrollmentGrant is the pairing outcome handed to the device provisioner. The Owner subject comes from the step-up that issued the code, and the key is the device-generated key proven by the CSR signature.

func RedeemHomePairing added in v0.53.10

func RedeemHomePairing(root string, request HomeEnrollmentRequest, now time.Time) (HomeEnrollmentGrant, error)

RedeemHomePairing consumes a pairing code exactly once and proves the device holds the private key of the CSR it submits. The code is consumed before the CSR is examined, so a wrong or replayed submission cannot probe the code. It issues no credential: the provisioner (step-ca) acts only on the grant.

type HomeEnrollmentRequest added in v0.53.10

type HomeEnrollmentRequest struct {
	Code   string
	CSRPEM []byte
	LAN    bool
}

HomeEnrollmentRequest is what a device presents. LAN is the transport admission the server observed for the connection; it is never read from a request header or body.

type HomeOverlayNode added in v0.53.13

type HomeOverlayNode struct {
	InterfaceName       string `json:"interfaceName"`
	PublicKey           string `json:"publicKey"`
	Address             string `json:"address"`
	ListenPort          int    `json:"listenPort"`
	KeepaliveSeconds    int    `json:"persistentKeepaliveSeconds"`
	PrivateKeyInCustody bool   `json:"privateKeyInLocalCustody"`
}

HomeOverlayNode is what a device needs to configure its side of the tunnel. The endpoint stays owner-declared and is not part of this record.

func EnsureHomeOverlayNodeKey added in v0.53.13

func EnsureHomeOverlayNodeKey(root string) (HomeOverlayNode, error)

EnsureHomeOverlayNodeKey generates the node's WireGuard private key into local custody on first use and returns the node's public overlay identity. The private key is never returned.

type HomeOverlayPeer added in v0.53.13

type HomeOverlayPeer struct {
	DeviceID  string `json:"deviceId"`
	PublicKey string `json:"publicKey"`
	Address   string `json:"address"`
}

HomeOverlayPeer is the public peer record of one enrolled device. It holds only the device's WireGuard public key and its overlay address.

func BindHomeOverlayPeer added in v0.53.13

func BindHomeOverlayPeer(root, deviceID, publicKey string, now time.Time) (HomeOverlayPeer, error)

BindHomeOverlayPeer registers the device-generated WireGuard public key of an active enrolled device and assigns it one overlay address. Rebinding the same device replaces its key and keeps its address.

type HomePairing added in v0.53.10

type HomePairing struct {
	Code      string    `json:"code"`
	ExpiresAt time.Time `json:"expiresAt"`
}

HomePairing is shown once to the Owner (State Console QR/code or CLI token). The code is never stored; only its hash is.

func IssueHomePairing added in v0.53.10

func IssueHomePairing(root, humanSession string, now time.Time) (HomePairing, error)

IssueHomePairing issues a single-use, node-bound pairing code. It requires a PocketID human session that is the pinned Owner and was authenticated within HomeOwnerStepUpFreshness, against the current signed Home trust.

func IssueHomePairingWithOwnerStepUp added in v0.53.14

func IssueHomePairingWithOwnerStepUp(root, idToken, nonce string, now time.Time) (HomePairing, error)

IssueHomePairingWithOwnerStepUp issues the pairing code from the local Owner step-up page (device-enroll mode). The ID token comes from the existing confidential step-up client, so its audience is localowner.StepUpClientID rather than the Home access client; everything else is the Home trust check of verifyHomeHumanToken (issuer, pinned Owner subject, signing keys, revocation, time claims), plus the flow nonce and a passkey (amr "phr") authentication, as for an action approval.

type HomePocketIDTrust

type HomePocketIDTrust struct {
	Issuer   string             `json:"issuer"`
	ClientID string             `json:"clientId"`
	Subject  string             `json:"subject"`
	Keys     jose.JSONWebKeySet `json:"keys"`
}

HomePocketIDTrust is the live PocketID OIDC authority. The CUE human issuer URN cannot alias this HTTPS issuer or its OIDC client audience.

type HomePublicTrust

type HomePublicTrust struct {
	Schema                        string                                  `json:"schema"`
	HomeSiteRef                   string                                  `json:"homeSiteRef"`
	PublicOrigin                  string                                  `json:"publicOrigin"`
	DeviceAuthorityRef            string                                  `json:"deviceAuthorityRef"`
	DeviceIssuer                  string                                  `json:"deviceIssuer"`
	DeviceAudiences               []string                                `json:"deviceAudiences"`
	DeviceKeySetRef               string                                  `json:"deviceKeySetRef"`
	DeviceCredentialTTLSeconds    int                                     `json:"deviceCredentialTTLSeconds"`
	DeviceSessionTTLSeconds       int                                     `json:"deviceSessionTTLSeconds"`
	HumanSessionTTLSeconds        int                                     `json:"humanSessionTTLSeconds"`
	RevocationMaxStalenessSeconds int                                     `json:"revocationMaxStalenessSeconds"`
	PartitionDenied               bool                                    `json:"partitionDenied"`
	PocketID                      HomePocketIDTrust                       `json:"pocketId"`
	DeviceKeys                    jose.JSONWebKeySet                      `json:"deviceKeys"`
	RevokedCredentialIDs          []string                                `json:"revokedCredentialIds"`
	RevokedSubjects               []string                                `json:"revokedSubjects"`
	IssuedAt                      time.Time                               `json:"issuedAt"`
	ExpiresAt                     time.Time                               `json:"expiresAt"`
	Signature                     localevidence.OwnerPolicyStateSignature `json:"signature"`
}

HomePublicTrust is current Home public verifier material. Device issuer fields rebound HomeDeviceAuthority; partition denial rebounds Modern IdentityTrust. Private keys, pairing state and enrollment tokens are absent.

type OwnerProvisioner

type OwnerProvisioner struct {
	// Client is the PocketID admin-API client (real or fake).
	Client PocketIDClient

	// PocketIDURL is the public origin of the PocketID instance, used to
	// build SetupURL. Example: "https://id.example.com" (no trailing slash).
	PocketIDURL string

	// OwnersGroup is the name of the group new owners are added to.
	// Defaults to "owners" when empty.
	OwnersGroup string
}

OwnerProvisioner creates the owner record in PocketID and adds them to the owners group, then issues a one-time-access token for WebAuthn enrollment.

func (*OwnerProvisioner) Provision

func (p *OwnerProvisioner) Provision(ctx context.Context, spec OwnerSpec) (*ProvisionResult, error)

Provision creates the owner user in PocketID, adds them to the owners group, and returns a ProvisionResult containing the user's UUID and a setup URL the owner clicks once to enroll a WebAuthn credential.

Source=="local" is the only locally provisioned path. Source=="cloud" returns an orchestrator-managed error. Any other Source value is rejected as invalid.

type OwnerSpec

type OwnerSpec struct {
	// Source controls the provisioning path. This local provisioner supports
	// "local" only; "cloud" is handled by TechStack/kombify Cloud.
	Source string

	// Email is the owner's address (also used as the WebAuthn account label).
	Email string

	// Username is the PocketID login handle.
	Username string

	// DisplayName is what PocketID renders in the UI; defaults to Username
	// when empty. PocketID v2 stores this in the FirstName column on the
	// underlying user record.
	DisplayName string

	// ForeignSubjectID is the external IdP subject for Source=="cloud".
	// Ignored when Source=="local".
	ForeignSubjectID string
}

OwnerSpec describes the daily-admin owner of a homelab instance.

type PocketIDAdminPayload

type PocketIDAdminPayload struct {
	Username   string `yaml:"username"`
	SetupToken string `yaml:"setupToken"`
	SetupURL   string `yaml:"setupUrl"`
	Group      string `yaml:"group"`
	UserID     string `yaml:"userId"`
}

PocketIDAdminPayload is the Layer-1 recovery credential. The setup token + URL is the v2 passkey-only equivalent of the classical sealed password: redeeming it lets the recoverer enroll a WebAuthn credential and become a fully-privileged PocketID admin.

type PocketIDClient

type PocketIDClient interface {
	CreateUser(ctx context.Context, req pocketid.CreateUserRequest) (*pocketid.User, error)
	AddUserToGroup(ctx context.Context, userID, groupID string) error
	GetGroupIDByName(ctx context.Context, groupName string) (string, error)
	CreateOneTimeAccessToken(ctx context.Context, userID string, ttl time.Duration) (string, error)
}

PocketIDClient is the subset of pocketid.Client used by provisioners. Defined as an interface so tests can supply fakes without standing up an HTTP server.

type ProvisionResult

type ProvisionResult struct {
	// UserID is the PocketID-assigned UUID for the new owner record.
	UserID string

	// SetupURL is the one-time link the owner clicks to enroll a WebAuthn
	// credential. It embeds a single-use token with a 24-hour TTL. Local
	// source only.
	SetupURL string
}

ProvisionResult is what Provision returns on success.

type TinyAuthStaticCredential

type TinyAuthStaticCredential struct {
	// Username is the synthetic local handle of the form
	// "bg-<nodename>-static". Distinct from the PocketID break-glass
	// username so logs/audit can tell the two layers apart.
	Username string

	// PasswordPlain is the human-typeable password (base64 of 32 random
	// bytes). Goes into the recovery bundle; never logged.
	PasswordPlain string

	// PasswordBcrypt is the bcrypt hash of PasswordPlain. Goes into the
	// TinyAuth container env via ToEnvValue.
	PasswordBcrypt string
}

TinyAuthStaticCredential is the materialized result of a TinyAuth static user provisioning. PasswordPlain is shown to the owner exactly once and embedded in the sealed recovery bundle; PasswordBcrypt is what gets rendered into the TinyAuth container `USERS` env var.

func (*TinyAuthStaticCredential) ToEnvValue

func (c *TinyAuthStaticCredential) ToEnvValue() string

ToEnvValue returns the "username:bcrypt-hash" entry suitable for direct inclusion in TinyAuth's USERS env var. Multiple users are joined with commas; this method returns a single entry.

type TinyAuthStaticGenerator

type TinyAuthStaticGenerator struct {
	// NodeName identifies the homelab firstnode this credential belongs
	// to. Required.
	NodeName string

	// BcryptCost is the bcrypt cost factor. Defaults to 12 when zero.
	// Tests may pass a lower value to keep the suite fast.
	BcryptCost int
}

TinyAuthStaticGenerator creates a per-node TinyAuth static credential.

func (*TinyAuthStaticGenerator) Generate

Generate produces a fresh TinyAuth static credential. The plaintext password is 32 random bytes encoded as base64 (~43 characters); the bcrypt hash is computed at the configured cost.

type TinyAuthStaticPayload

type TinyAuthStaticPayload struct {
	Username       string `yaml:"username"`
	PasswordPlain  string `yaml:"passwordPlain"`
	PasswordBcrypt string `yaml:"passwordBcrypt"`
}

TinyAuthStaticPayload is the Layer-2 recovery credential. PasswordPlain is here so the operator has something to type even if PocketID itself is corrupt; PasswordBcrypt mirrors what was rendered into the TinyAuth container env so the bundle is a complete record of state.

Jump to

Keyboard shortcuts

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