Bearer Token Authentication¶
In v0.3.8 the gRPC channel is secured by mTLS, not bearer tokens. This page is primarily about hypervisor API tokens.
As of v0.3.8 (unchanged since v0.3.7) the manager↔provider gRPC channel is authenticated by mutual TLS with a client-cert SAN allow-list (see mTLS), not by a bearer token. A bearer token is unnecessary when the manager is the only legitimate caller, so the in-tree manager does not attach an Authorization: Bearer <token> metadata header on outbound RPCs.
The provider SDK still exposes middleware.AuthConfig.BearerTokenAuth and an operator-supplied ValidateToken callback (sdk/provider/middleware/middleware.go) for external provider authors who want it in addition to (or instead of) mTLS. If you enable Bearer-token auth on a provider that the in-tree manager dials, the manager will get back codes.Unauthenticated and reconciles will fail — so treat gRPC-channel bearer auth as an external-provider feature, not an in-tree production path.
This page therefore covers two distinct scopes:
- Provider API tokens to hypervisors (Proxmox API tokens, future REST-based providers) — these are real, in-production, and the recommended posture for Proxmox.
- gRPC-channel bearer auth via the SDK — available for external provider authors; the in-tree manager authenticates with mTLS instead and will not send a bearer token.
Scope 1: Hypervisor API tokens¶
This is the production-relevant form of bearer-token-style authentication in v0.3.8. It applies to the Proxmox provider today and is the model for any future REST-API-based provider.
Proxmox API tokens¶
The Proxmox provider's pveapi client uses Proxmox VE's native API-token header on every request:
The token is built from two Secret-key values that the provider reads as files:
| Secret key | File path | Source |
|---|---|---|
token_id | /etc/virtrigaud/credentials/token_id | internal/providers/proxmox/server.go:69 |
token_secret | /etc/virtrigaud/credentials/token_secret | internal/providers/proxmox/server.go:70 |
Why API tokens beat password auth in production¶
- Scoped at creation time in the PVE UI — you bind the token to a role (typically
VirtRigaudRole) and a path (typically/). The token cannot do anything the underlying user can't do, but it also can't do anything the role doesn't grant. - Revocable independently of the user account. If the token leaks, you delete the token in PVE; the user account stays intact.
- No interactive-session state in the provider. Password auth issues a session ticket that has to be refreshed; token auth is stateless.
- PVE audit log records the token ID on every action, making the trail traceable to the VirtRigaud deployment that holds it (one token per deployment is the recommended pattern).
Creating a Proxmox API token (CLI)¶
# On a PVE node, as root:
pveum user add virtrigaud@pve --password "..." # underlying user, no login needed
pveum role add VirtRigaudRole -privs "VM.Allocate,VM.Audit,VM.Config.CPU,\
VM.Config.Cloudinit,VM.Config.Disk,VM.Config.HWType,VM.Config.Memory,\
VM.Config.Network,VM.Config.Options,VM.Console,VM.Monitor,VM.PowerMgmt,\
Datastore.AllocateSpace,Datastore.AllocateTemplate,Datastore.Audit,\
Sys.Audit,Sys.Modify"
pveum aclmod / -user virtrigaud@pve -role VirtRigaudRole
# Create the token. Save the printed secret — PVE will not show it again.
pveum user token add virtrigaud@pve vrtg-token --privsep 0
The output gives you:
token_id = "virtrigaud@pve!vrtg-token"token_secret = "<uuid>"
Materialise as a K8s Secret:
apiVersion: v1
kind: Secret
metadata:
name: proxmox-prod-credentials
namespace: virtrigaud-system
type: Opaque
stringData:
token_id: "virtrigaud@pve!vrtg-token"
token_secret: "<uuid>"
And reference from your Provider CR:
apiVersion: infra.virtrigaud.io/v1beta1
kind: Provider
metadata:
name: proxmox-prod
namespace: virtrigaud-system
spec:
type: proxmox
endpoint: https://pve-cluster.internal.example.com:8006
credentialSecretRef:
name: proxmox-prod-credentials
runtime:
image: ghcr.io/projectbeskar/virtrigaud/provider-proxmox:v0.3.8
Token rotation¶
Token rotation is a multi-step operation in v0.3.8:
- Create a new token in PVE.
- Update the K8s Secret (or your ExternalSecret) with the new
token_id/token_secret. - Restart the provider pod — the provider reads credentials only at startup (see External Secrets).
- Once the new token is confirmed working (the
ProviderCRHealthycondition stays True), delete the old token in PVE.
In-process token reload is on the roadmap for v0.4.0+.
vSphere SSO username/password¶
The vSphere provider does not use bearer-token-style auth. It authenticates via vCenter SSO (username/password). See the vSphere provider page for the service-account configuration pattern.
A future SAML/OIDC-based vCenter authentication mode could be modelled as a bearer token, but is not implemented in v0.3.8.
Libvirt¶
Libvirt authentication is SSH-based (key or password). Bearer tokens do not apply.
Scope 2: gRPC-channel bearer auth (SDK, external providers)¶
This scope is for operators writing their own external provider against sdk/provider. It is documented for completeness. In-tree providers authenticate the manager via mTLS (see mTLS); bearer-token auth on the gRPC channel is an optional extra that only external provider authors would wire in.
SDK API¶
sdk/provider/middleware/middleware.go:81-94:
type AuthConfig struct {
// RequireTLS requires TLS client certificates
RequireTLS bool
// AllowedSANs lists allowed Subject Alternative Names for mTLS
AllowedSANs []string
// BearerTokenAuth enables bearer token authentication
BearerTokenAuth bool
// ValidateToken function for bearer token validation
ValidateToken func(ctx context.Context, token string) error
}
When BearerTokenAuth: true, the SDK installs unary and stream interceptors that:
- Extract the
authorizationmetadata header from every incoming gRPC call. - Strip the
Bearerprefix. - Call your
ValidateTokencallback with the token string and request context. - Reject with
codes.UnauthenticatedifValidateTokenreturns an error or the header is missing/malformed.
External-provider example¶
In a hypothetical cmd/provider-mycustom/main.go:
config := server.DefaultConfig()
config.Middleware = &middleware.Config{
Logging: &middleware.LoggingConfig{Enabled: true, Logger: logger},
Recovery: &middleware.RecoveryConfig{Enabled: true, Logger: logger},
Auth: &middleware.AuthConfig{
BearerTokenAuth: true,
ValidateToken: validateMyToken,
},
}
// validateMyToken is operator-supplied. It MUST be cheap and side-effect-free
// on the happy path, since it runs on every RPC.
func validateMyToken(ctx context.Context, token string) error {
// Example: HMAC-signed token with a shared secret read from disk.
if !hmac.Equal(expected, sha256.Sum256([]byte(token))) {
return fmt.Errorf("invalid token")
}
return nil
}
Why the in-tree manager will not interoperate¶
The in-tree manager's gRPC client does not currently attach a bearer token to outbound calls. The relevant code path is internal/transport/grpc/client.go:94-165, where unary client interceptors are appended in this order:
providerRPCMetricsInterceptor(G4 / #90)- (optional)
providerCircuitBreakerInterceptor(G6 / #112)
There is no authorization-header-injecting interceptor. If you enable BearerTokenAuth: true on the server side, every RPC from the in-tree manager will fail with codes.Unauthenticated.
The in-tree channel is authenticated by mTLS, not bearer tokens
In v0.3.8 the manager authenticates to providers with a client certificate verified against a configured CA and a SAN allow-list (see mTLS). That is the in-tree authentication path; there is no plan to also send a bearer token from the in-tree manager. SDK bearer-token auth remains an optional control for external provider authors who want an additional or alternative gate on top of (or instead of) mTLS.
Why this is not "JWT with scopes" in v0.3.8¶
The SDK's ValidateToken is intentionally a flat func(ctx, token) error — it does not encode scopes, claims, expiration, audience, or any other JWT-like structure. Two reasons:
-
Scope mapping needs the gRPC method name to mean something policy-wise. That requires either:
- The provider knowing about scopes (which couples the SDK to a policy model), or
- A separate authorisation interceptor that lives downstream of
ValidateTokenand inspectsinfo.FullMethod.
Neither is implemented in v0.3.8.
-
In a one-manager-one-provider trust model, scoping doesn't add much. The manager is the only legitimate caller, so the question "what is this caller allowed to do?" is just "is this the manager?" — answered in v0.3.8 by mTLS client-cert identity and the SAN allow-list. The interesting authorisation surface is the K8s RBAC on the
ProviderCR — who is allowed to create/modify it — not the gRPC channel.
When and if multi-tenant providers become a thing, the design would extend ValidateToken to return a typed context value (claims), and a follow-on interceptor would gate methods on claim values. That is roadmap, not v0.3.8.
Auditing¶
For Scope 1 (hypervisor API tokens):
- Proxmox audit log records the token ID on every API call. Configure your PVE cluster to ship audit events to your SIEM.
- VirtRigaud-side logs record
token_id(the ID, not the secret) at DEBUG level when establishing the PVE client. The token secret is never logged.
For Scope 2 (gRPC-channel bearer auth, for external providers that enable it):
- The SDK's
Loggingmiddleware (enabled in all in-tree providers) logsmethod,code, anddurationper RPC. It does NOT log theauthorizationheader value.
See also¶
- Operations -> Security — overall credential flow.
- External Secrets — sourcing the
token_id/token_secretfrom your secret store. - Proxmox provider — Proxmox-specific token guidance.
- mTLS — the other half of the gRPC-channel security story.