Watch
1
0
Fork
You've already forked RedFlag
0
RedFlag/RAF/security/02-authentication-stack.md
Fimeg d9ba008f67 raf: sync docs to code + relative cross-reference links
Handler/migration counts, calculateBackoff -> calculateDelay, machine-id
binding (cross-platform machineid + fallbacks, not hostname), last-reviewed
dates, and made [[cross-references]] relative so they resolve.
2026-06-15 09:39:34 -04:00

205 lines
No EOL
6.2 KiB
Markdown

# Authentication Stack
**Four-layer authentication: registration tokens → JWT → refresh tokens → machine binding.**
---
## Layer 1: Registration Tokens
**Purpose:** One-time enrollment tokens
**Format:** Random 64-character hex string
**Lifecycle:**
1. Server generates token with `max_seats` count
2. Admin distributes token to operators
3. Agent uses token to register
4. Server marks token as used and increments `seats_used`
5. Token is revoked after use
**Endpoint:** `POST /api/v1/agents/register`
**Request:**
```json
{
"hostname": "server-01",
"os_type": "linux",
"os_version": "6.19",
"machine_id": "sha256-fingerprint...",
"public_key": "ed25519-public-key...",
"available_scanners": ["apt", "dnf", "docker"]
}
```
**Response:**
```json
{
"agent_id": "uuid-4",
"jwt_token": "...",
"refresh_token": "...",
"server_public_key": "..."
}
```
**Cross-references:**
- [flows/01-registration](../flows/01-registration.md) (registration flow)
- [security/03-refresh-tokens](03-refresh-tokens.md) (refresh tokens)
---
## Layer 2: JWT Access Tokens
**Purpose:** Short-lived access tokens for API calls
**Issuer:** `"redflag-agent"` for agents, `"redflag-web"` for web
**Duration:** 24 hours
**Algorithm:** HS256
**Claims:**
```json
{
"sub": "agent-uuid",
"iss": "redflag-agent",
"exp": 1234567890,
"iat": 1234567890
}
```
**Validation:**
```go
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing authorization header"})
c.Abort()
return
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
if tokenString == authHeader {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid authorization format"})
c.Abort()
return
}
token, err := jwt.ParseWithClaims(tokenString, &AgentClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(JWTSecret), nil
})
if err != nil || !token.Valid {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
c.Abort()
return
}
if claims, ok := token.Claims.(*AgentClaims); ok {
// Validate issuer to prevent cross-type token confusion
if claims.Issuer != "" && claims.Issuer != JWTIssuerAgent {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token type"})
c.Abort()
return
}
c.Set("agent_id", claims.AgentID)
c.Next()
} else {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token claims"})
c.Abort()
}
}
}
```
**Cross-references:**
- [security/01-trust-boundaries](01-trust-boundaries.md) (trust boundary matrix)
- [flows/02-command-execution](../flows/02-command-execution.md) (polling loop)
---
## Layer 3: Refresh Tokens
**Purpose:** Long-lived authentication for polling, with rotation and reuse detection
**Format:** 64-character hex string (32 bytes crypto/rand)
**Storage:** SHA-256 hash in database; rotation lineage via `family_id`, `consumed_at`, `superseded_by` (migration 045)
**Duration:** 90 days (bumped on each renewal, not on every check-in)
**Lifecycle:**
1. Generated at registration with a fresh `family_id` (root of the rotation chain)
2. Agent calls `POST /renew` only when its JWT expires (~24h) — not on every poll
3. Each renewal mints a **successor** token in the same family and marks the parent `consumed`
4. Server returns the new refresh token alongside the new JWT; agent persists it to `config.json`
5. Accept-previous-once grace: an agent that crashed before persisting the new token can retry with the consumed old one — the server sees the successor is still unconsumed and re-issues
6. Reuse detection: a consumed token presented after its successor is also consumed → entire family revoked, security event logged, both parties locked out
**Machine binding:** Renewal requires `X-Machine-ID` to match the registered host (same as command endpoints). A stolen `config.json` cannot mint access tokens from an unregistered machine.
**Instance lock:** A `flock` (Unix) or named kernel mutex (Windows) prevents two agent processes from sharing the same `config.json` on the same host, serializing renewal at the process level.
**Endpoint:** `POST /api/v1/agents/renew`
**Request:**
```json
{
"agent_id": "uuid",
"refresh_token": "64-char-hex",
"agent_version": "0.2.0.7"
}
```
Headers: `X-Machine-ID` (required), `Content-Type: application/json`
**Response:**
```json
{
"token": "new-jwt...",
"refresh_token": "new-64-char-hex..."
}
```
The agent must persist `refresh_token` to disk; if it crashes before doing so, accept-previous-once grace recovers on the next attempt.
**Cross-references:**
- [flows/01-registration](../flows/01-registration.md) (registration flow)
- [flows/02-command-execution](../flows/02-command-execution.md) (renewal in polling loop)
---
## Layer 4: Machine Binding
**Purpose:** Bind JWT to specific hardware
**Method:** SHA-256 hash of machine-id + hostname (no boot-id)
**Validation:** Middleware checks `X-Machine-ID` header matches DB
**Failure modes:**
- Machine ID mismatch → 403 Forbidden
- Agent row deleted → 401 Unauthorized
- Update in progress → Validates nonce
**Middleware:** `server/internal/api/middleware/machine_binding.go`
**Cross-references:**
- [security/01-trust-boundaries](01-trust-boundaries.md) (trust boundary matrix)
- [flows/01-registration](../flows/01-registration.md) (machine ID generation)
---
## Footer: Assumptions & Connections
**Assumption:** Trust On First Use (TOFU) model — agent caches server public key at registration and uses it for all future verification.
**Connection:** [security/01-trust-boundaries](01-trust-boundaries.md) (trust boundary matrix)
**Connection:** [security/04-machine-binding](04-machine-binding.md) (hardware-bound auth)
**Connection:** [verification/01-signing-pipeline](../verification/01-signing-pipeline.md) (Ed25519 signing)
**Connection:** [verification/02-agent-verification](../verification/02-agent-verification.md) (command verification)
---
*Last reviewed: 2026-06-14*