Bearer Token Authentication¶
This guide covers how to configure bearer token authentication for VirtRigaud providers using JWT tokens and RBAC.
Overview¶
Bearer token authentication provides a stateless, scalable authentication mechanism using JSON Web Tokens (JWT). This approach is suitable for:
- Multi-tenant environments: Different tokens for different tenants
- API-based access: External systems accessing provider services
- Short-lived sessions: Tokens with configurable expiration
- Fine-grained permissions: Token-based RBAC
JWT Token Structure¶
Token Claims¶
{
"iss": "virtrigaud-manager",
"sub": "provider-client",
"aud": "virtrigaud-provider",
"exp": 1640995200,
"iat": 1640908800,
"nbf": 1640908800,
"scope": "vm:create vm:read vm:update vm:delete",
"tenant": "default",
"provider": "vsphere",
"jti": "unique-token-id"
}
Scopes Definition¶
| Scope | Description |
|---|---|
vm:create | Create virtual machines |
vm:read | Read virtual machine information |
vm:update | Update virtual machine configuration |
vm:delete | Delete virtual machines |
vm:power | Control virtual machine power state |
vm:snapshot | Create and manage snapshots |
vm:clone | Clone virtual machines |
admin | Full administrative access |
Token Generation¶
JWT Signing Key¶
# Generate RS256 private key
openssl genrsa -out jwt-private-key.pem 2048
# Extract public key
openssl rsa -in jwt-private-key.pem -pubout -out jwt-public-key.pem
# Store as Kubernetes secret
kubectl create secret generic jwt-keys \
--from-file=private-key=jwt-private-key.pem \
--from-file=public-key=jwt-public-key.pem \
--namespace=virtrigaud-system
Token Generation Service¶
package auth
import (
"crypto/rsa"
"time"
"github.com/golang-jwt/jwt/v4"
)
type TokenClaims struct {
Issuer string `json:"iss"`
Subject string `json:"sub"`
Audience string `json:"aud"`
ExpiresAt int64 `json:"exp"`
IssuedAt int64 `json:"iat"`
NotBefore int64 `json:"nbf"`
Scope string `json:"scope"`
Tenant string `json:"tenant"`
Provider string `json:"provider"`
ID string `json:"jti"`
jwt.RegisteredClaims
}
type TokenService struct {
privateKey *rsa.PrivateKey
publicKey *rsa.PublicKey
issuer string
}
func NewTokenService(privateKey *rsa.PrivateKey, publicKey *rsa.PublicKey, issuer string) *TokenService {
return &TokenService{
privateKey: privateKey,
publicKey: publicKey,
issuer: issuer,
}
}
func (ts *TokenService) GenerateToken(subject, tenant, provider string, scopes []string, duration time.Duration) (string, error) {
now := time.Now()
claims := &TokenClaims{
Issuer: ts.issuer,
Subject: subject,
Audience: "virtrigaud-provider",
ExpiresAt: now.Add(duration).Unix(),
IssuedAt: now.Unix(),
NotBefore: now.Unix(),
Scope: strings.Join(scopes, " "),
Tenant: tenant,
Provider: provider,
ID: generateJTI(),
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
return token.SignedString(ts.privateKey)
}
func (ts *TokenService) ValidateToken(tokenString string) (*TokenClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return ts.publicKey, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*TokenClaims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("invalid token")
}
func generateJTI() string {
return uuid.New().String()
}
Provider Authentication Interceptor¶
gRPC Interceptor¶
package middleware
import (
"context"
"strings"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
type AuthInterceptor struct {
tokenService *auth.TokenService
rbac *RBACManager
}
func NewAuthInterceptor(tokenService *auth.TokenService, rbac *RBACManager) *AuthInterceptor {
return &AuthInterceptor{
tokenService: tokenService,
rbac: rbac,
}
}
func (ai *AuthInterceptor) Unary() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// Skip authentication for health checks
if strings.HasSuffix(info.FullMethod, "/Health/Check") {
return handler(ctx, req)
}
token, err := ai.extractToken(ctx)
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "missing or invalid token: %v", err)
}
claims, err := ai.tokenService.ValidateToken(token)
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "invalid token: %v", err)
}
// Check authorization
if !ai.rbac.IsAuthorized(claims, info.FullMethod) {
return nil, status.Errorf(codes.PermissionDenied, "insufficient permissions")
}
// Add claims to context
ctx = context.WithValue(ctx, "claims", claims)
return handler(ctx, req)
}
}
func (ai *AuthInterceptor) extractToken(ctx context.Context) (string, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return "", fmt.Errorf("missing metadata")
}
authHeaders := md.Get("authorization")
if len(authHeaders) == 0 {
return "", fmt.Errorf("missing authorization header")
}
authHeader := authHeaders[0]
if !strings.HasPrefix(authHeader, "Bearer ") {
return "", fmt.Errorf("invalid authorization header format")
}
return strings.TrimPrefix(authHeader, "Bearer "), nil
}
RBAC Manager¶
package middleware
import (
"strings"
)
type Permission struct {
Resource string
Action string
}
type RBACManager struct {
permissions map[string][]Permission
}
func NewRBACManager() *RBACManager {
return &RBACManager{
permissions: map[string][]Permission{
// RPC method to required permissions mapping
"/provider.v1.ProviderService/CreateVM": {
{Resource: "vm", Action: "create"},
},
"/provider.v1.ProviderService/GetVM": {
{Resource: "vm", Action: "read"},
},
"/provider.v1.ProviderService/UpdateVM": {
{Resource: "vm", Action: "update"},
},
"/provider.v1.ProviderService/DeleteVM": {
{Resource: "vm", Action: "delete"},
},
"/provider.v1.ProviderService/PowerVM": {
{Resource: "vm", Action: "power"},
},
"/provider.v1.ProviderService/CreateSnapshot": {
{Resource: "vm", Action: "snapshot"},
},
"/provider.v1.ProviderService/CloneVM": {
{Resource: "vm", Action: "clone"},
},
},
}
}
func (rbac *RBACManager) IsAuthorized(claims *auth.TokenClaims, method string) bool {
requiredPerms, exists := rbac.permissions[method]
if !exists {
// Allow if no specific permissions required
return true
}
userScopes := strings.Split(claims.Scope, " ")
// Check if user has admin scope
for _, scope := range userScopes {
if scope == "admin" {
return true
}
}
// Check specific permissions
for _, requiredPerm := range requiredPerms {
requiredScope := requiredPerm.Resource + ":" + requiredPerm.Action
hasPermission := false
for _, userScope := range userScopes {
if userScope == requiredScope {
hasPermission = true
break
}
}
if !hasPermission {
return false
}
}
return true
}
Kubernetes RBAC Integration¶
ServiceAccount and ClusterRole¶
apiVersion: v1
kind: ServiceAccount
metadata:
name: virtrigaud-token-manager
namespace: virtrigaud-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: virtrigaud-token-manager
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: virtrigaud-token-manager
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: virtrigaud-token-manager
subjects:
- kind: ServiceAccount
name: virtrigaud-token-manager
namespace: virtrigaud-system
Token Management ConfigMap¶
apiVersion: v1
kind: ConfigMap
metadata:
name: token-config
namespace: virtrigaud-system
data:
config.yaml: |
tokenService:
issuer: "virtrigaud-manager"
defaultDuration: "1h"
maxDuration: "24h"
scopes:
- name: "vm:create"
description: "Create virtual machines"
- name: "vm:read"
description: "Read virtual machine information"
- name: "vm:update"
description: "Update virtual machine configuration"
- name: "vm:delete"
description: "Delete virtual machines"
- name: "vm:power"
description: "Control virtual machine power state"
- name: "vm:snapshot"
description: "Create and manage snapshots"
- name: "vm:clone"
description: "Clone virtual machines"
- name: "admin"
description: "Full administrative access"
tenants:
- name: "default"
description: "Default tenant"
allowedScopes: ["vm:create", "vm:read", "vm:update", "vm:delete", "vm:power"]
- name: "development"
description: "Development environment"
allowedScopes: ["vm:create", "vm:read", "vm:update", "vm:delete", "vm:power", "vm:snapshot", "vm:clone"]
- name: "production"
description: "Production environment"
allowedScopes: ["vm:read", "vm:power"]
Client Configuration¶
Manager Client Setup¶
package client
import (
"context"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
type AuthenticatedClient struct {
client providerv1.ProviderServiceClient
token string
}
func NewAuthenticatedClient(endpoint, token string) (*AuthenticatedClient, error) {
conn, err := grpc.Dial(endpoint, grpc.WithInsecure())
if err != nil {
return nil, err
}
return &AuthenticatedClient{
client: providerv1.NewProviderServiceClient(conn),
token: token,
}, nil
}
func (ac *AuthenticatedClient) CreateVM(ctx context.Context, req *providerv1.CreateVMRequest) (*providerv1.CreateVMResponse, error) {
ctx = ac.addAuthHeader(ctx)
return ac.client.CreateVM(ctx, req)
}
func (ac *AuthenticatedClient) addAuthHeader(ctx context.Context) context.Context {
md := metadata.Pairs("authorization", "Bearer "+ac.token)
return metadata.NewOutgoingContext(ctx, md)
}
Token Refresh¶
package auth
import (
"sync"
"time"
)
type TokenManager struct {
tokenService *TokenService
currentToken string
expiresAt time.Time
mutex sync.RWMutex
subject string
tenant string
provider string
scopes []string
}
func NewTokenManager(tokenService *TokenService, subject, tenant, provider string, scopes []string) *TokenManager {
return &TokenManager{
tokenService: tokenService,
subject: subject,
tenant: tenant,
provider: provider,
scopes: scopes,
}
}
func (tm *TokenManager) GetToken() (string, error) {
tm.mutex.RLock()
if tm.currentToken != "" && time.Now().Before(tm.expiresAt.Add(-5*time.Minute)) {
token := tm.currentToken
tm.mutex.RUnlock()
return token, nil
}
tm.mutex.RUnlock()
return tm.refreshToken()
}
func (tm *TokenManager) refreshToken() (string, error) {
tm.mutex.Lock()
defer tm.mutex.Unlock()
// Double-check after acquiring write lock
if tm.currentToken != "" && time.Now().Before(tm.expiresAt.Add(-5*time.Minute)) {
return tm.currentToken, nil
}
token, err := tm.tokenService.GenerateToken(tm.subject, tm.tenant, tm.provider, tm.scopes, time.Hour)
if err != nil {
return "", err
}
tm.currentToken = token
tm.expiresAt = time.Now().Add(time.Hour)
return token, nil
}
Helm Chart Integration¶
Provider Runtime with Bearer Token Auth¶
# values-bearer-auth.yaml
auth:
type: "bearer"
jwt:
publicKeySecret: "jwt-keys"
publicKeyKey: "public-key"
issuer: "virtrigaud-manager"
audience: "virtrigaud-provider"
# Environment variables for authentication
env:
- name: AUTH_TYPE
value: "bearer"
- name: JWT_PUBLIC_KEY_PATH
value: "/etc/jwt/public-key"
- name: JWT_ISSUER
value: "virtrigaud-manager"
- name: JWT_AUDIENCE
value: "virtrigaud-provider"
# Mount JWT public key
volumes:
- name: jwt-public-key
secret:
secretName: jwt-keys
volumeMounts:
- name: jwt-public-key
mountPath: /etc/jwt
readOnly: true
Monitoring and Logging¶
Authentication Metrics¶
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
authenticationAttempts = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "virtrigaud_authentication_attempts_total",
Help: "Total number of authentication attempts",
},
[]string{"method", "result", "tenant"},
)
authenticationDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "virtrigaud_authentication_duration_seconds",
Help: "Duration of authentication operations",
},
[]string{"method", "result"},
)
activeTokens = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "virtrigaud_active_tokens",
Help: "Number of active tokens by tenant",
},
[]string{"tenant", "provider"},
)
)
func RecordAuthAttempt(method, result, tenant string) {
authenticationAttempts.WithLabelValues(method, result, tenant).Inc()
}
func RecordAuthDuration(method, result string, duration time.Duration) {
authenticationDuration.WithLabelValues(method, result).Observe(duration.Seconds())
}
Audit Logging¶
package audit
import (
"context"
"encoding/json"
"time"
"go.uber.org/zap"
)
type AuditEvent struct {
Timestamp time.Time `json:"timestamp"`
EventType string `json:"event_type"`
Subject string `json:"subject"`
Tenant string `json:"tenant"`
Provider string `json:"provider"`
Resource string `json:"resource"`
Action string `json:"action"`
Result string `json:"result"`
Error string `json:"error,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type AuditLogger struct {
logger *zap.Logger
}
func NewAuditLogger(logger *zap.Logger) *AuditLogger {
return &AuditLogger{logger: logger}
}
func (al *AuditLogger) LogAuthEvent(ctx context.Context, eventType, subject, tenant, provider, result string, err error) {
event := AuditEvent{
Timestamp: time.Now(),
EventType: eventType,
Subject: subject,
Tenant: tenant,
Provider: provider,
Result: result,
}
if err != nil {
event.Error = err.Error()
}
eventJSON, _ := json.Marshal(event)
al.logger.Info("audit_event", zap.String("event", string(eventJSON)))
}
Security Best Practices¶
1. Token Validation¶
// Always validate all token claims
func validateTokenClaims(claims *TokenClaims) error {
now := time.Now()
// Check expiration
if claims.ExpiresAt < now.Unix() {
return fmt.Errorf("token expired")
}
// Check not before
if claims.NotBefore > now.Unix() {
return fmt.Errorf("token not yet valid")
}
// Check issuer
if claims.Issuer != expectedIssuer {
return fmt.Errorf("invalid issuer")
}
// Check audience
if claims.Audience != expectedAudience {
return fmt.Errorf("invalid audience")
}
return nil
}
2. Rate Limiting¶
// Implement rate limiting for token generation
type RateLimiter struct {
requests map[string][]time.Time
mutex sync.RWMutex
limit int
window time.Duration
}
func (rl *RateLimiter) Allow(key string) bool {
rl.mutex.Lock()
defer rl.mutex.Unlock()
now := time.Now()
requests := rl.requests[key]
// Remove old requests outside the window
var validRequests []time.Time
for _, req := range requests {
if now.Sub(req) < rl.window {
validRequests = append(validRequests, req)
}
}
// Check if we've exceeded the limit
if len(validRequests) >= rl.limit {
return false
}
// Add the current request
validRequests = append(validRequests, now)
rl.requests[key] = validRequests
return true
}
3. Token Blacklisting¶
// Implement token blacklisting for revoked tokens
type TokenBlacklist struct {
blacklistedTokens map[string]time.Time
mutex sync.RWMutex
}
func (tb *TokenBlacklist) IsBlacklisted(jti string) bool {
tb.mutex.RLock()
defer tb.mutex.RUnlock()
expiresAt, exists := tb.blacklistedTokens[jti]
if !exists {
return false
}
// Remove expired entries
if time.Now().After(expiresAt) {
delete(tb.blacklistedTokens, jti)
return false
}
return true
}
func (tb *TokenBlacklist) BlacklistToken(jti string, expiresAt time.Time) {
tb.mutex.Lock()
defer tb.mutex.Unlock()
tb.blacklistedTokens[jti] = expiresAt
}