Documentation
¶
Overview ¶
Package identity holds service principals and their credentials.
A principal is modelled separately from its credentials so it can later carry an OIDC subject binding instead of, or alongside, a secret — swapping how a token is minted without touching anything that consumes one (ADR-0004).
Index ¶
- Constants
- Variables
- func NewRecoveryShares(shareCount, threshold int) (shares []string, digest []byte, err error)
- func VerifyRecoveryShares(shares []string, threshold int, digest []byte) error
- func WithActor(ctx context.Context, principalID, name string) context.Context
- type Actor
- type AuditOperation
- type AuditOutcome
- type AuditScope
- type AuditTarget
- type AuthorizationVerdict
- type BasicAuthIssuer
- func (i *BasicAuthIssuer) Issue(principal *Principal, credential string) (string, string, error)
- func (i *BasicAuthIssuer) Reissue(principal *Principal, secretID string, authTime time.Time) (string, error)
- func (i *BasicAuthIssuer) TTL() time.Duration
- func (i *BasicAuthIssuer) Verify(signed string) (Verified, error)
- func (i *BasicAuthIssuer) VerifyExpired(signed string) (Verified, error)
- type Claims
- type Grant
- type IdentityKind
- type Principal
- func (p *Principal) Authenticate(plaintext string, now time.Time) (string, bool)
- func (p *Principal) Authorize(required Role, organizationID, projectID uuid.UUID) AuthorizationVerdict
- func (p *Principal) AuthorizeVisibility(required Role, organizationID uuid.UUID) AuthorizationVerdict
- func (p *Principal) HasActiveSecret(id string, now time.Time) bool
- func (p *Principal) IssueSecret(secretID string, expiresAt *time.Time, at time.Time) (string, error)
- func (p *Principal) RevokeSecret(secretID string, now time.Time) error
- func (p *Principal) Secrets() []Secret
- type Role
- type Scope
- type Secret
- type TokenIssuer
- type Verified
Constants ¶
const ( SystemScannerPrincipalID = "system:scanner" SystemBagDropPrincipalID = "system:bagdrop" SystemPluginPrunerPrincipalID = "system:plugin-pruner" SystemPluginBundlerPrincipalID = "system:plugin-bundler" )
const DummySecretHash = "$argon2id$v=19$m=19456,t=2,p=1$XlTPhjxTv+KIT+AriTHNpg$gPa3WcRFECTrrH3qzoICZ7jHlTnQju0Tt5CJicDsZcM"
DummySecretHash carries the current verification parameters for timing equalisation. Authentication padding and the repository's client-id miss path must share this source so a parameter change cannot make their costs diverge.
const MaxVerificationMemoryBytes = argonMemory * 1024 * maxActiveSecrets
MaxVerificationMemoryBytes is the most memory one authentication attempt can spend, so a caller admitting requests can size a budget against it.
Multiplied by maxActiveSecrets because Authenticate always performs exactly that many verifications, padding missing usable secrets with DummySecretHash.
const TokenAudience = "https://api.hashicorp.cloud"
Variables ¶
var ( ErrInvalid = errors.New("invalid") ErrConflict = errors.New("conflict") ErrNotFound = errors.New("not found") // ErrRootPermanence marks the refusals that keep a root principal holding // at least one usable, never-expiring secret — without one, expiry locks // the instance out on a timer (duf-2rw). Distinguished so the platform // plane can answer it with its own message rather than the cap's. ErrRootPermanence = errors.New("a root principal must hold a usable secret that never expires") // ErrIntegrity reports an identity row failing keyring MAC verification on // an encrypted deployment (ADR-0024): the row was not written by this // application. Audited distinctly; answered indistinguishably on the wire. ErrIntegrity = errors.New("identity integrity verification failed") )
ErrSharesRejected reports shares that decoded and recombined but did not reproduce the stored verifier — the caller does not hold custody.
Functions ¶
func NewRecoveryShares ¶
NewRecoveryShares mints the recovery secret and splits it into shareCount shares, any threshold of which prove custody to /sys/recovery.
The shares appear here and nowhere else. What the caller stores is the digest: the secret is 256-bit random material, not a password, so a plain digest is the correct verifier — memory-hard hashing defends low-entropy inputs (ADR-0024).
func VerifyRecoveryShares ¶
VerifyRecoveryShares recombines the presented shares and checks the result against the stored digest. Malformed or duplicated shares and too few of them wrap ErrInvalid; shares that recombine to the wrong secret return ErrSharesRejected.
Types ¶
type AuditOperation ¶
type AuditOperation string
AuditOperation names the security-relevant action being attempted.
const ( AuditOperationTokenIssue AuditOperation = "token.issue" AuditOperationProjectList AuditOperation = "project.list" AuditOperationProjectRead AuditOperation = "project.read" AuditOperationInstanceInitialize AuditOperation = "instance.initialize" AuditOperationInstanceRecover AuditOperation = "instance.recover" // The console's cookie session (duf-1cn): a credential materializing into // or leaving the browser's cookie jar is a lifecycle event like issuance. AuditOperationSessionCreate AuditOperation = "session.create" AuditOperationSessionRenew AuditOperation = "session.renew" AuditOperationSessionDelete AuditOperation = "session.delete" // The principal and credential lifecycle. ADR-0019 makes audit a // PRECONDITION of the role model rather than a follow-up: permitting a human // to hold root is justified by attribution, and that justification is only as // good as the trail. Granting and revoking authority are the highest-value // entries in it. AuditOperationPrincipalList AuditOperation = "principal.list" AuditOperationPrincipalRead AuditOperation = "principal.read" AuditOperationPrincipalCreate AuditOperation = "principal.create" AuditOperationPrincipalDelete AuditOperation = "principal.delete" AuditOperationSecretIssue AuditOperation = "secret.issue" AuditOperationSecretRevoke AuditOperation = "secret.revoke" AuditOperationScanExecute AuditOperation = "scan.execute" AuditOperationScanRetention AuditOperation = "scan.retention" // A scheduled prune of finished plugin jobs; the same operation names a // maintainer's Prune now through the API. AuditOperationPluginImportPrune AuditOperation = "plugin.import.prune" AuditOperationPluginRegistryBundleBuild AuditOperation = "plugin_registry.bundle.build" // An uploaded bundle read into the registry by the bundle importer. AuditOperationPluginRegistryBundleIngest AuditOperation = "plugin_registry.bundle.ingest" )
type AuditOutcome ¶
type AuditOutcome string
AuditOutcome distinguishes completed actions from refusals and system failures.
const ( AuditOutcomeSuccess AuditOutcome = "success" AuditOutcomeRefused AuditOutcome = "refused" AuditOutcomeFailure AuditOutcome = "failure" )
type AuditScope ¶
type AuditScope string
AuditScope identifies where an operation was attempted.
const ( AuditScopePlatform AuditScope = "platform" AuditScopeOrganization AuditScope = "organization" AuditScopeProject AuditScope = "project" )
type AuditTarget ¶
AuditTarget is one file the broker writes every audit event to.
Slot is deliberately absent. The database assigns 1..3 to make a fourth target unwriteable (the baseline's three-slot constraint), but that is storage enforcing a limit, not a property of the target — exposing it would invite a client to choose one, and the ordering it implies means nothing to a caller.
type AuthorizationVerdict ¶
type AuthorizationVerdict int
AuthorizationVerdict answers the whole authorization question while preserving why it was refused.
Authority and tenancy must never be checkable separately. Refusals still need to distinguish a tenancy denial, which conceals existence, from a role denial within a tenancy the caller may see. The verdict carries both truths without separating the checks.
const ( // AuthorizationAllowed means both tenancy and role checks passed. AuthorizationAllowed AuthorizationVerdict = iota // AuthorizationDeniedTenancy conceals a tenancy outside the principal's scope. AuthorizationDeniedTenancy // AuthorizationDeniedRole means the tenancy is visible but the role is insufficient. AuthorizationDeniedRole )
type BasicAuthIssuer ¶
type BasicAuthIssuer struct {
// contains filtered or unexported fields
}
BasicAuthIssuer issues tokens after authenticating a Principal secret.
func NewBasicAuthIssuer ¶
func NewKeyringAuthIssuer ¶
func NewKeyringAuthIssuer(issuer string, keys func() [][]byte, ttl time.Duration) (*BasicAuthIssuer, error)
NewKeyringAuthIssuer reads the current keyring on every issue and verify so rotation takes effect without rebuilding the HTTP planes.
func (*BasicAuthIssuer) TTL ¶
func (i *BasicAuthIssuer) TTL() time.Duration
TTL is the lifetime of tokens this issuer mints.
Exposed so the token endpoint can report expires_in from the same value that produced the token's exp claim, rather than holding a second copy that could drift.
func (*BasicAuthIssuer) VerifyExpired ¶
func (i *BasicAuthIssuer) VerifyExpired(signed string) (Verified, error)
VerifyExpired performs the same signature and claim checks as Verify while tolerating an expired exp claim. It exists only for the cookie-session renewal path; bearer middleware must continue to call Verify.
type Claims ¶
type Claims struct {
jwt.RegisteredClaims
AuthTime *jwt.NumericDate `json:"auth_time,omitempty"`
OrganizationID string `json:"organization_id,omitempty"`
ProjectID string `json:"project_id,omitempty"`
BucketID string `json:"bucket_id,omitempty"`
// SecretID is the credential this token was minted from. Not a secret
// itself — an opaque identifier — and it is what makes revocation take
// effect immediately rather than at expiry.
SecretID string `json:"sid,omitempty"`
// Scope is a deliberately reserved seam for authorization bindings
// (duf-vgn). It is emitted as an empty array until bindings exist, and
// token tests assert that emptiness.
Scope []string `json:"scope"`
// Grants is a deliberately reserved seam for authorization bindings
// (duf-vgn). It is emitted as an empty array until bindings exist, and
// token tests assert that emptiness.
Grants []Grant `json:"grants"`
}
Claims is the JWT contract shared by every issuance path.
type IdentityKind ¶
type IdentityKind string
IdentityKind says how the actor proved, or failed to prove, its identity.
const ( IdentityKindAnonymous IdentityKind = "anonymous" IdentityKindServicePrincipal IdentityKind = "service_principal" IdentityKindSystem IdentityKind = "system" IdentityKindUnknown IdentityKind = "unknown" )
type Principal ¶
type Principal struct {
ID string
Name string
ClientID string
Scope Scope
Role Role
CreatedAt time.Time
// contains filtered or unexported fields
}
Principal is a service identity: a client id, a scope, a role, and its secrets.
func NewPrincipal ¶
func NewPrincipal( id, name, clientID string, scope Scope, role Role, at time.Time, ) (*Principal, error)
NewPrincipal creates a principal holding NO secrets.
Creation and issuance are separate actions (duf-4ac): a principal that exists but cannot yet authenticate is an ordinary state, reached deliberately from the console and left behind whenever a non-root principal's last secret is revoked (ADR-0004, amended 2026-08-02). Call IssueSecret to give it a credential.
func RestorePrincipal ¶
func RestorePrincipal( id, name, clientID string, scope Scope, role Role, createdAt time.Time, secrets []Secret, ) (*Principal, error)
RestorePrincipal rebuilds a principal from storage.
func (*Principal) Authenticate ¶
Authenticate reports whether plaintext matches any active secret.
Every usable slot is checked even after a match, and missing slots are padded with a dummy verification, so the work does not reveal which one succeeded or whether the principal holds zero, one, or two usable secrets. Returns WHICH secret matched, so a token can name the credential that minted it and stop working when that credential is revoked (review finding 14).
Expired secrets are skipped entirely rather than checked-then-rejected, then their missing usable slots are padded. Presenting a correct-but-expired secret therefore takes the same verification path as garbage. Exactly maxActiveSecrets verifications also stays within MaxVerificationMemoryBytes, whose admission budget already assumes that work (duf-2rw).
func (*Principal) Authorize ¶
func (p *Principal) Authorize( required Role, organizationID, projectID uuid.UUID, ) AuthorizationVerdict
Authorize reports whether this principal may perform an action requiring a role on a project or organization tenancy.
func (*Principal) AuthorizeVisibility ¶
func (p *Principal) AuthorizeVisibility( required Role, organizationID uuid.UUID, ) AuthorizationVerdict
AuthorizeVisibility reports whether this principal may read an organization or a filtered collection within it. A project binding is visible within its containing organization without granting organization-level authority.
func (*Principal) HasActiveSecret ¶
HasActiveSecret reports whether a secret is still current on this principal.
ADR-0019 resolves authority per request so revocation is immediate; expiry gets the same immediacy — a token minted from a secret that has since expired stops working now, not at the token's own expiry.
func (*Principal) IssueSecret ¶
func (p *Principal) IssueSecret(secretID string, expiresAt *time.Time, at time.Time) (string, error)
IssueSecret mints an additional secret, returning the plaintext exactly once.
expiresAt is nil for a credential that never expires. The cap counts USABLE secrets: an expired one grants nothing, so requiring its revocation before a replacement can be issued would add a step at exactly the moment someone is fixing an outage (duf-2rw).
func (*Principal) RevokeSecret ¶
RevokeSecret removes a secret, refusing to remove a root's last one.
A principal with no secrets cannot authenticate and cannot mint a replacement FOR ITSELF — but it does not have to. A maintainer issues a replacement for any tenancy-scoped principal, so leaving one secretless is an ordinary, recoverable state.
Root is the exception, and the only one (ADR-0004, amended 2026-08-02). Nothing sits above it to re-issue on its behalf, so a root left secretless can only be recovered by direct database access — the same failure the first-run wizard warns about.
Expiry extends the same rule (duf-2rw): a root left holding only EXPIRING secrets is the same lockout on a timer, so a root must always keep at least one usable, never-expiring secret.
type Role ¶
type Role string
Role is what an identity may do within a scope it is entitled to (ADR-0019).
The tiers are strictly nested, which is what lets "no role more permissive than your own" be an ordering rather than a comparison of capability sets.
const ( // RoleReader sees buckets, versions, builds and unrestricted channels. RoleReader Role = "reader" // RoleBuilder additionally creates buckets, versions and builds, and consumes // restricted channels. This is what a Packer CLI principal holds — the // smallest role that completes a build. RoleBuilder Role = "builder" // RolePublisher additionally assigns channels. Separate from builder because // declaring a channel is asking to promote. RolePublisher Role = "publisher" // RoleMaintainer additionally manages restricted channels, principals and role // bindings within its scope. Named to avoid OpenShift's meaning of "operator". RoleMaintainer Role = "maintainer" // RoleRoot may do anything, including creating organisations and configuring // authentication and audit. Platform scope only. RoleRoot Role = "root" )
func (Role) AtLeast ¶
AtLeast reports whether this role includes the authority of another.
An unrecognised role on either side answers false: a value that is not a role cannot satisfy a requirement, and cannot be satisfied by one (ADR-0017).
func (Role) MayGrant ¶
MayGrant reports whether a holder of this role may grant another.
No identity may grant a role more permissive than its own — the ordinary escalation defence. Equality is permitted: a maintainer may appoint another maintainer.
This answers the LEVEL question only. The caller must separately establish that the grantor holds this role IN THE SCOPE being granted, because holding maintainer in one organisation confers nothing in another (ADR-0019).
func (Role) MayModifyHolderOf ¶
MayModifyHolderOf reports whether a holder of this role may modify or delete an identity holding another.
MayGrant alone stops upward granting but not downward attack: an identity able to delete a holder of a higher role has escalated by removal, since taking the higher tier out leaves it at the top. Same rationale, separate rule.
func (Role) PlatformOnly ¶
PlatformOnly reports whether a role may only be held at platform scope.
type Scope ¶
Scope is the tenancy a principal may act within.
Project is optional: with an organization set, the zero project means organization-scoped, seeing every project in that organization. Bucket is optional too; until bucket-aware enforcement lands, it records a narrower binding without changing the project-level authorization predicates. Both project and organization scope exist because the Packer CLI distinguishes them — a project-scoped principal gets 403 from ProjectService_List, which the CLI turns into a set-HCP_PROJECT_ID message, while an organization-scoped one seeing several projects makes it warn and select the oldest (ADR-0016).
The zero value is the MOST privileged one ¶
Scope{} is platform scope: above every tenancy, and the store answers it with every organisation and every principal on the instance. That is a deny-by- default inversion, so it carries a rule — never construct a Scope literally and hand it to anything that authorizes. Pass a scope that came from a restored Principal, whose construction enforced the binding (duf-ueq).
func (Scope) OrganizationScoped ¶
OrganizationScoped reports whether this scope spans every project in its organization.
func (Scope) Permits ¶
Permits reports whether the principal may act on the named tenancy.
This is the authorization primitive the compatibility plane needs: it derives its tenant from the request path, so the path must be checked against the caller rather than trusted (ADR-0017). Deny by default — an unset scope permits nothing.
func (Scope) PermitsOrganization ¶
PermitsOrganization reports whether this scope may act at ORGANISATION level — on the organisation itself, or on the set of its projects.
Distinct from Permits, which asks about a specific project and therefore requires one. An operation like "create a project in this organisation" has no project yet, so Permits would refuse it for want of an argument that cannot exist.
func (Scope) PlatformScoped ¶
PlatformScoped reports whether this scope sits above every tenancy.
Only `root` may be held here (ADR-0019). Permits deliberately still answers false for a platform scope: it asks whether a caller is entitled to a TENANCY, and a platform identity is not entitled to one — it outranks the question. Principal.May handles that, so the tenancy check keeps a single meaning.
func (Scope) WithinOrganization ¶
WithinOrganization reports whether this scope is bound to the organization, either directly or through one of its projects.
This is a membership and visibility question only. It does not grant authority to act at organization level; PermitsOrganization remains the predicate for that stricter question.
type Secret ¶
type Secret struct {
ID string
CreatedAt time.Time
LastUsedAt *time.Time
// ExpiresAt, when set, is the moment this credential stops granting
// anything. Expiry lives on the SECRET, not the principal — that is what
// makes overlap possible: the outgoing secret expires soon, the incoming
// one later (duf-2rw).
ExpiresAt *time.Time
// contains filtered or unexported fields
}
Secret is one credential belonging to a principal.
The plaintext exists only in the response that issues it. What is retained is an argon2id hash and metadata — nothing here can reconstruct the secret.
func RestoreSecret ¶
func RestoreSecret(id, encoded string, createdAt time.Time, lastUsedAt, expiresAt *time.Time) (Secret, error)
RestoreSecret rebuilds a Secret from storage.
type TokenIssuer ¶
type TokenIssuer interface {
Issue(principal *Principal, credential string) (token string, secretID string, err error)
Reissue(principal *Principal, secretID string, authTime time.Time) (string, error)
Verify(token string) (Verified, error)
VerifyExpired(token string) (Verified, error)
}
TokenIssuer authenticates a credential, issues a token, and verifies tokens carrying the resulting principal scope.
type Verified ¶
type Verified struct {
PrincipalID string
Scope Scope
// SecretID names the credential that minted this token, so a caller
// resolving the principal can refuse a token whose credential has since
// been revoked (review finding 14).
SecretID string
AuthTime time.Time
ExpiresAt time.Time
}
Verified is the authenticated identity a token carries.
The principal is returned alongside the scope because authorizing a request and recording who made it are different needs, and a not-found answer at a tenancy boundary must carry enough server-side detail to diagnose (ADR-0017). Scope alone cannot say which principal acted.