Files
orca/internal/identity/authtoken.go
T
Jon Chery 5232fcb808 fix(P04): wire ACL enforcement + WebAuthn reg auth + audit actor (REQ-153)
R-023: Zero-trust enforcement operationally wired.

ACL enforcement (C-45 staged rollout):
- acl.Check wired into all 5 daemon handlers (dispatch/jobs/nodes/tasks)
- health endpoints exempt (liveness probes not gated)
- ACL log-only mode default (config acl.enforce=false); enforce after
  bootstrap ACL verified
- sshpush auth: ORCA_OIDC_TOKEN validated against JWKS before apply
- txn apply: Authorize hook validates OIDC token before running pull
- acl.json mode 0600 (was 0644)
- flock on acl.json for concurrent grant/revoke
- bootstrap ACL: init grants cluster-admin to orca-admins group + SVID

Audit actor identity:
- currentActor reads OIDC sub from credentials.json (was hardcoded "cli")
- threaded through all audit.Record calls via context

WebAuthn registration auth:
- BeginRegistration/FinishRegistration require authenticated session
- fail-closed 401 when no authFunc configured

New files: internal/daemon/acl.go, internal/cli/authactor.go,
internal/engine/actor.go, internal/identity/authtoken.go,
internal/sshpush/auth.go, internal/txn/auth_test.go

---ci---
project: orca
phase: 4
milestone: v0.13
status: complete
requirements:
  covered: [153]
---/ci---
2026-08-07 20:33:39 +00:00

61 lines
2.3 KiB
Go

// Package identity — authtoken.go provides the ORCA_OIDC_TOKEN
// validation helper used by the SSH-push applier and the txn apply
// path (P04, v0.13; C-44). Both paths validate the env-var token
// against the issuer's JWKS before applying any state change.
//
// The token is read from $ORCA_OIDC_TOKEN. The issuer + client ID
// come from the OIDC config (oidc.issuer, oidc.client_id). If the
// token is missing or invalid, the apply is refused. The verified
// claims (sub + groups) are returned so the caller can thread them
// into the audit actor field (T5) and the ACL check (T3/T4).
package identity
import (
"context"
"fmt"
"os"
)
// EnvOIDCToken is the environment variable holding the OIDC ID token
// for the SSH-push / txn apply paths (R-021: the IdP issues the
// token; Orca never issues its own).
const EnvOIDCToken = "ORCA_OIDC_TOKEN"
// VerifyOperatorToken reads $ORCA_OIDC_TOKEN and verifies it against
// the issuer's JWKS. Returns the verified claims (sub, groups) on
// success. Returns an error if the token is missing, expired, or
// fails signature verification.
//
// The issuer + clientID come from the OIDC config block. When issuer
// is empty, the function returns an error — the apply path requires
// an OIDC issuer to be configured.
func VerifyOperatorToken(ctx context.Context, issuer, clientID string) (*IDTokenClaims, error) {
raw := os.Getenv(EnvOIDCToken)
if raw == "" {
return nil, fmt.Errorf("identity: %s env var is not set (operator OIDC token required for apply)", EnvOIDCToken)
}
if issuer == "" {
return nil, fmt.Errorf("identity: oidc.issuer is not configured (required to verify %s)", EnvOIDCToken)
}
if clientID == "" {
clientID = "orca-cli"
}
claims, err := VerifyIDTokenStatic(ctx, issuer, clientID, raw)
if err != nil {
return nil, fmt.Errorf("identity: verify %s: %w", EnvOIDCToken, err)
}
return claims, nil
}
// OperatorActor renders the verified operator identity for the audit
// `actor` field. The convention is "oidc:<sub>" so audit entries can
// be filtered by human operator. Falls back to "oidc:unknown" when
// claims are nil (e.g. when the caller could not verify the token but
// still wants to record an audit entry).
func OperatorActor(claims *IDTokenClaims) string {
if claims == nil || claims.Subject == "" {
return "oidc:unknown"
}
return "oidc:" + claims.Subject
}