15 KiB
RedFlag Token Authentication System Analysis
Based on comprehensive analysis of the RedFlag codebase, here's a detailed breakdown of the token authentication system:
Executive Summary
RedFlag uses a three-tier token system with different lifetimes and purposes:
- Registration Tokens - One-time use for initial agent enrollment (multi-seat capable)
- JWT Access Tokens - Short-lived (24h) stateless tokens for API authentication
- Refresh Tokens - Long-lived (90d) rotating tokens for automatic renewal
Important Clarification: The "rotating token system" is ACTIVE and working (not discontinued). It refers to the refresh token system that rotates every 24h during renewal.
1. Registration Tokens (One-Time Use Multi-Seat Tokens)
Purpose & Characteristics
- Initial agent registration/enrollment with the server
- Multi-seat support - Single token can register multiple agents
- One-time use per agent - Each agent uses it once during registration
- Configurable expiration - Admins set expiration (max 7 days)
Technical Implementation
Token Generation
// aggregator-server/internal/config/config.go:138-144
func GenerateSecureToken() string {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return ""
}
return hex.EncodeToString(bytes)
}
- Method: Cryptographically secure 32-byte random token → 64-character hex string
- Algorithm:
crypto/rand.Read()for entropy
Database Schema
-- 011_create_registration_tokens_table.up.sql
CREATE TABLE registration_tokens (
token VARCHAR(64) UNIQUE PRIMARY KEY,
max_seats INT DEFAULT 1,
seats_used INT DEFAULT 0,
expires_at TIMESTAMP NOT NULL,
status ENUM('active', 'used', 'expired', 'revoked') DEFAULT 'active',
metadata JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
Seat Tracking System
- Validation:
status = 'active' AND expires_at > NOW() AND seats_used < max_seats - Usage Tracking:
registration_token_usagetable maintains audit trail - Status Flow:
active→used(seats exhausted) orexpired(time expires)
Registration Flow
1. Admin creates registration token with seat limit
2. Token distributed to agents (via config, environment variable, etc.)
3. Agent uses token for initial registration at /api/v1/agents/register
4. Server validates token and decrements available seats
5. Server generates AgentID + JWT + Refresh token
6. Agent saves AgentID, discards registration token
2. JWT Access Tokens (Stateless Short-Lived Tokens)
Purpose & Characteristics
- API authentication for agent-server communication
- Web dashboard authentication for users
- Stateless validation - No database lookup required
- Short lifetime - 24 hours for security
Token Structure
Agent JWT Claims
// aggregator-server/internal/api/middleware/auth.go:13-17
type AgentClaims struct {
AgentID string `json:"agent_id"`
jwt.RegisteredClaims
}
User JWT Claims
// aggregator-server/internal/api/handlers/auth.go:41-47
type UserClaims struct {
UserID int `json:"user_id"`
Username string `json:"username"`
Role string `json:"role"`
jwt.RegisteredClaims
}
Security Properties
- Algorithm: HS256 using shared secret
- Secret Storage:
REDFLAG_JWT_SECRETenvironment variable - Validation: Bearer token in
Authorization: Bearer {token}header - Stateless: Server validates using secret, no database lookup needed
Key Security Consideration
// aggregator-server/cmd/server/main.go:130
if cfg.Admin.JWTSecret == "" {
cfg.Admin.JWTSecret = GenerateSecureToken()
log.Printf("Generated JWT secret: %s", cfg.Admin.JWTSecret) // Debug exposure!
}
- Development Risk: JWT secret logged in debug mode
- Production Requirement: Must set
REDFLAG_JWT_SECRETconsistently
3. Refresh Tokens (Rotating Long-Lived Tokens)
Purpose & Characteristics
- Automatic agent renewal without re-registration
- Long lifetime - 90 days with sliding window
- Rotating mechanism - New tokens issued on each renewal
- Secure storage - Only SHA-256 hashes stored in database
Database Schema
-- 008_create_refresh_tokens_table.up.sql
CREATE TABLE refresh_tokens (
agent_id UUID REFERENCES agents(id) PRIMARY KEY,
token_hash VARCHAR(64) UNIQUE NOT NULL,
expires_at TIMESTAMP NOT NULL,
last_used_at TIMESTAMP DEFAULT NOW(),
revoked BOOLEAN DEFAULT FALSE
);
Token Generation & Security
// aggregator-server/internal/database/queries/refresh_tokens.go
func GenerateRefreshToken() (string, error) {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}
func HashRefreshToken(token string) string {
hash := sha256.Sum256([]byte(token))
return hex.EncodeToString(hash[:])
}
Renewal Process (The "Rotating Token System")
1. Agent JWT expires (after 24h)
2. Agent sends refresh request to /api/v1/agents/renew
3. Server validates refresh token hash against database
4. Server generates NEW JWT access token (24h)
5. Server updates refresh_token.last_used_at
6. Server resets refresh_token.expires_at to NOW() + 90 days (sliding window)
7. Agent updates config with new JWT token
Key Features
- Sliding Window Expiration: 90-day window resets on each use
- Hash Storage: Only SHA-256 hashes stored, plaintext tokens never persisted
- Rotation: New JWT issued each time, refresh token extended
- Revocation Support: Manual revocation possible via database
4. Agent Configuration & Token Usage
Configuration Structure
// aggregator-agent/internal/config/config.go:48-90
type Config struct {
// ... other fields ...
RegistrationToken string `json:"registration_token,omitempty"` // One-time registration token
Token string `json:"token"` // JWT access token (24h)
RefreshToken string `json:"refresh_token"` // Refresh token (90d)
}
File Storage & Security
// config.go:274-280
func (c *Config) Save() error {
// ... validation logic ...
jsonData, err := json.MarshalIndent(c, "", " ")
if err != nil {
return err
}
return os.WriteFile(c.Path, jsonData, 0600) // Owner read/write only
}
- Storage: Plaintext JSON configuration file
- Permissions: 0600 (owner read/write only)
- Location: Typically
/etc/redflag/agent.jsonor user-specified path
Agent Registration Flow
// aggregator-agent/cmd/agent/main.go:450-476
func runRegistration(cfg *config.Config) (*config.Config, error) {
if cfg.RegistrationToken == "" {
return nil, fmt.Errorf("registration token required for initial setup")
}
// Create temporary client with registration token
client := api.NewClient("", cfg.ServerURL, cfg.SkipTLSVerify)
// Register with server
regReq := api.RegisterRequest{
RegistrationToken: cfg.RegistrationToken,
Hostname: cfg.Hostname,
Version: version.Version,
}
// Process registration response
// ...
cfg.Token = resp.Token // JWT access token
cfg.RefreshToken = resp.RefreshToken
cfg.AgentID = resp.AgentID
return cfg, nil
}
Token Renewal Logic
// aggregator-agent/cmd/agent/main.go:484-519
func renewTokenIfNeeded(cfg *config.Config) error {
if cfg.RefreshToken == "" {
return fmt.Errorf("no refresh token available")
}
// Create temporary client without auth for renewal
client := api.NewClient("", cfg.ServerURL, cfg.SkipTLSVerify)
renewReq := api.RenewRequest{
AgentID: cfg.AgentID,
RefreshToken: cfg.RefreshToken,
}
resp, err := client.RenewToken(renewReq)
if err != nil {
return err // Falls back to re-registration
}
// Update config with new JWT token
cfg.Token = resp.Token
return cfg.Save() // Persist updated config
}
5. Security Analysis & Configuration Encryption Implications
Current Security Posture
Strengths
- Strong Token Generation: Cryptographically secure random tokens
- Proper Token Separation: Different tokens for different purposes
- Hash Storage: Refresh tokens stored as hashes only
- JWT Stateless Validation: No database storage for access tokens
- File Permissions: Config files with 0600 permissions
Vulnerabilities
- Plaintext Storage: All tokens stored in clear text JSON
- JWT Secret Exposure: Debug logging in development
- Registration Token Exposure: Stored in plaintext until used
- Config File Access: Anyone with file access can steal tokens
Configuration Encryption Impact Analysis
Critical Challenge: Token Refresh Workflow
Current Flow:
1. Agent reads config (plaintext) → gets refresh_token
2. Agent calls /api/v1/agents/renew with refresh_token
3. Server returns new JWT access_token
4. Agent writes new access_token to config (plaintext)
Encrypted Config Flow:
1. Agent must decrypt config to get refresh_token
2. Agent calls /api/v1/agents/renew
3. Server returns new JWT access_token
4. Agent must encrypt and write updated config
Key Implementation Challenges
-
Key Management
- Where to store encryption keys?
- How to handle key rotation?
- Agent process must have access to keys
-
Atomic Operations
- Decrypt → Modify → Encrypt must be atomic
- Prevent partial writes during token updates
- Handle encryption/decryption failures gracefully
-
Debugging & Recovery
- Encrypted configs complicate debugging
- Lost encryption keys = lost agent registration
- Backup/restore complexity increases
-
Performance Overhead
- Decryption on every config read
- Encryption on every token renewal
- Memory footprint for decrypted config
Recommended Encryption Strategy
-
Selective Field Encryption
{ "agent_id": "123e4567-e89b-12d3-a456-426614174000", "token": "enc:v1:aes256gcm:encrypted_jwt_token_here", "refresh_token": "enc:v1:aes256gcm:encrypted_refresh_token_here", "server_url": "https://redflag.example.com" }- Encrypt only sensitive fields (tokens)
- Preserve JSON structure for compatibility
- Include version prefix for future migration
-
Key Storage Options
- Environment Variables:
REDFLAG_ENCRYPTION_KEY - Kernel Keyring: Store keys in OS keyring
- Dedicated KMS: AWS KMS, Azure Key Vault, etc.
- File-Based: Encrypted key file with strict permissions
- Environment Variables:
-
Graceful Degradation
func LoadConfig() (*Config, error) { // Try encrypted first if cfg, err := loadEncryptedConfig(); err == nil { return cfg, nil } // Fallback to plaintext for migration return loadPlaintextConfig() } -
Migration Path
- Detect plaintext configs and auto-encrypt on first load
- Provide migration utilities for existing deployments
- Support both encrypted and plaintext during transition
6. Token Lifecycle Summary
Registration Token Lifecycle:
┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌─────────────┐
│ Generated │───▶│ Distributed │───▶│ Used │───▶│ Expired/ │
│ (Admin UI) │ │ (To Agents) │ │ (Agent Reg) │ │ Revoked │
└─────────────┘ └──────────────┘ └─────────────┘ └─────────────┘
│
▼
┌──────────────────┐
│ Agent Registration│
│ (Creates: │
│ AgentID, JWT, │
│ RefreshToken) │
└──────────────────┘
JWT Access Token Lifecycle:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Generated │───▶│ Valid │───▶│ Expired │───▶│ Renewed │
│ (Reg/Renew) │ │ (24h) │ │ │ │ (via Refresh)│
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│
▼
┌──────────────┐
│ API Requests │
│ (Bearer Auth)│
└──────────────┘
Refresh Token Lifecycle (The "Rotating System"):
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Generated │───▶│ Valid │───▶│ Used for │───▶│ Rotated │
│(Registration)│ │ (90d) │ │ Renewal │ │ (90d Reset) │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
7. Security Recommendations
Immediate Improvements
- Remove JWT Secret Logging in production builds
- Implement Config File Encryption for sensitive fields
- Add Token Usage Monitoring and anomaly detection
- Secure Registration Token Distribution beyond config files
Configuration Encryption Implementation
- Use AES-256-GCM for field-level encryption
- Store encryption keys in kernel keyring or secure environment
- Implement atomic config updates to prevent corruption
- Provide migration utilities for existing deployments
- Add config backup/restore functionality
Long-term Security Enhancements
- Hardware Security Modules (HSMs) for key management
- Certificate-based authentication as alternative to tokens
- Zero-trust architecture for agent-server communication
- Regular security audits and penetration testing
8. Conclusion
The RedFlag token authentication system is well-designed with proper separation of concerns and appropriate token lifetimes. The main security consideration is the plaintext storage of tokens in agent configuration files.
Key Takeaways:
- The rotating token system is ACTIVE and refers to refresh token rotation
- Config encryption is feasible but requires careful key management
- Token refresh workflow must remain functional after encryption
- Gradual migration path is essential for existing deployments
The recommended approach is selective field encryption with strong key management practices, ensuring the token refresh workflow remains operational while significantly improving security at rest.