Skip to main content

Overview

Agent tokens are JWT tokens specifically designed for Garnet runtime security agents. They provide authentication for agents to report events, send heartbeats, and create profiles. Agent tokens are limited in scope and can only access agent-specific operations.
Agent tokens are automatically generated when you create a new agent through the API.

How It Works

Agent authentication uses a custom header-based approach:
  1. Agent token is created when a new agent is registered
  2. Token is stored securely by the agent
  3. Agent includes token in X-Agent-Token header for each request
  4. API validates token and extracts agent ID for authorization

Configuration

Creating an Agent

Agent tokens are generated during agent creation:
import (
    "context"
    "github.com/garnet-org/api/client"
    "github.com/garnet-org/api/types"
)

func createAgent(userClient *client.Client) (string, error) {
    createReq := types.CreateAgent{
        Name:       "production-agent-01",
        OS:         "linux",
        Arch:       "amd64",
        Version:    "1.0.0",
        Kind:       types.AgentKindKubernetes,
        IP:         "10.0.1.42",
        Labels: map[string]string{
            "environment": "production",
            "region":      "us-west-2",
        },
    }
    
    var resp types.AgentCreated
    err := userClient.Post(context.Background(), &resp, "/api/v1/agents", createReq)
    if err != nil {
        return "", err
    }
    
    // resp.Token contains the agent token
    return resp.Token, nil
}
The agent token is only returned once during creation. Store it securely immediately.

Configuring Client with Agent Token

client/client.go
// WithAgentToken configures the client to use an agent token for authentication.
func (c *Client) WithAgentToken(token string) *Client {
	client := c.Clone()
	client.AuthToken = token
	client.TokenType = TokenTypeAgent
	return client
}
// Create client with agent token
agentToken := os.Getenv("GARNET_AGENT_TOKEN")
agentClient := client.New("", "").WithAgentToken(agentToken)

HTTP Header Format

Agent tokens are sent in the X-Agent-Token header:
X-Agent-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
The SDK automatically sets this header:
client/client.go
switch c.TokenType {
case TokenTypeAgent:
    req.Header.Set("X-Agent-Token", c.AuthToken)
}

Agent-Specific Operations

Agent tokens have restricted access to specific endpoints:

Send Heartbeat

Agents should regularly send heartbeat signals to update their last_seen timestamp:
func sendHeartbeat(agentClient *client.Client) error {
    var resp map[string]string
    err := agentClient.Post(
        context.Background(),
        &resp,
        "/api/v1/agent_heartbeat",
        nil,
    )
    return err
}

// Run heartbeat every 30 seconds
ticker := time.NewTicker(30 * time.Second)
for range ticker.C {
    if err := sendHeartbeat(agentClient); err != nil {
        log.Printf("Heartbeat failed: %v", err)
    }
}

Ingest Events

Agents report security events using the v2 ashkaal format:
import "github.com/garnet-org/api/types"

func reportEvent(agentClient *client.Client, event types.CreateOrUpdateEventV2) error {
    var resp types.EventV2CreatedOrUpdated
    err := agentClient.Put(
        context.Background(),
        &resp,
        "/api/v1/events_v2",
        event,
    )
    
    if err != nil {
        return fmt.Errorf("failed to ingest event: %w", err)
    }
    
    log.Printf("Event ingested: %s (status: %d)", resp.ID, resp.Status)
    return nil
}
The agent ID is automatically extracted from the token. You don’t need to include it in the request.

Create Profiles

Agents can create or update ashkaal profiles:
func createProfile(agentClient *client.Client, profile types.CreateProfile) error {
    var resp types.CreatedProfile
    err := agentClient.Post(
        context.Background(),
        &resp,
        "/api/v1/profiles",
        profile,
    )
    return err
}

Authorization Scope

Allowed Operations

Agent tokens can only access:
  • POST /api/v1/agent_heartbeat - Send heartbeat
  • PUT /api/v1/events_v2 - Ingest events
  • POST /api/v1/profiles - Create/update profiles

Restricted Operations

Agent tokens cannot access:
  • User information endpoints
  • Agent management (create, update, delete agents)
  • Issue management
  • Token management
  • Network policy endpoints
  • Project settings
Attempting to access unauthorized endpoints with an agent token returns 401 Unauthorized.

Security Best Practices

1

Secure Token Storage

Store agent tokens securely on the agent host:
// Store in encrypted configuration file
// Or use secret management systems (HashiCorp Vault, AWS Secrets Manager)
token := loadTokenFromSecureStorage()
2

Limit Token Exposure

  • Never log agent tokens
  • Don’t include tokens in error messages
  • Avoid passing tokens as command-line arguments
// ❌ BAD: Token in logs
log.Printf("Using token: %s", token)

// ✅ GOOD: No token exposure
log.Printf("Agent authenticated successfully")
3

Rotate Tokens Regularly

Implement token rotation by:
  1. Creating a new agent
  2. Migrating to new agent token
  3. Deleting old agent
4

Monitor Agent Activity

Track agent heartbeats and event submissions to detect compromised tokens:
if time.Since(lastHeartbeat) > 5*time.Minute {
    log.Warn("Agent heartbeat missing - possible token issue")
}
5

Use TLS

Always communicate over HTTPS to prevent token interception:
client := client.New("https://api.garnet.ai", "")
agentClient := client.WithAgentToken(token)

Error Handling

Authentication Errors

err := agentClient.Post(ctx, &resp, "/api/v1/agent_heartbeat", nil)
if err != nil {
    if strings.Contains(err.Error(), "401") {
        // Token is invalid or expired
        log.Fatal("Agent authentication failed: invalid token")
    }
    if strings.Contains(err.Error(), "404") {
        // Agent not found (possibly deleted)
        log.Fatal("Agent not found - may need re-registration")
    }
}

Common Issues

Error CodeCauseSolution
401Invalid or expired tokenVerify token is correct and agent exists
401Wrong endpointAgent tokens can only access agent-specific endpoints
404Agent not foundAgent may have been deleted; create new agent
400Invalid event formatValidate event payload matches ashkaal format

Complete Agent Example

package main

import (
    "context"
    "log"
    "os"
    "time"
    
    "github.com/garnet-org/api/client"
    "github.com/garnet-org/api/types"
)

type Agent struct {
    client *client.Client
}

func NewAgent(token string) *Agent {
    c := client.New("https://api.garnet.ai", "")
    return &Agent{
        client: c.WithAgentToken(token),
    }
}

func (a *Agent) Start(ctx context.Context) error {
    // Send initial heartbeat
    if err := a.sendHeartbeat(ctx); err != nil {
        return fmt.Errorf("initial heartbeat failed: %w", err)
    }
    
    // Start heartbeat ticker
    ticker := time.NewTicker(30 * time.Second)
    defer ticker.Stop()
    
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-ticker.C:
            if err := a.sendHeartbeat(ctx); err != nil {
                log.Printf("Heartbeat failed: %v", err)
            }
        }
    }
}

func (a *Agent) sendHeartbeat(ctx context.Context) error {
    var resp map[string]string
    return a.client.Post(ctx, &resp, "/api/v1/agent_heartbeat", nil)
}

func (a *Agent) ReportEvent(ctx context.Context, event types.CreateOrUpdateEventV2) error {
    var resp types.EventV2CreatedOrUpdated
    return a.client.Put(ctx, &resp, "/api/v1/events_v2", event)
}

func main() {
    token := os.Getenv("GARNET_AGENT_TOKEN")
    if token == "" {
        log.Fatal("GARNET_AGENT_TOKEN not set")
    }
    
    agent := NewAgent(token)
    
    ctx := context.Background()
    if err := agent.Start(ctx); err != nil {
        log.Fatalf("Agent failed: %v", err)
    }
}

Testing Agent Authentication

Test agent authentication in development:
func TestAgentAuth(t *testing.T) {
    // Create test agent with user token
    userClient := client.New("https://staging-api.garnet.ai", userToken)
    
    createReq := types.CreateAgent{
        Name:    "test-agent",
        OS:      "linux",
        Arch:    "amd64",
        Version: "1.0.0",
        Kind:    types.AgentKindKubernetes,
    }
    
    var agentResp types.AgentCreated
    err := userClient.Post(context.Background(), &agentResp, "/api/v1/agents", createReq)
    require.NoError(t, err)
    
    // Test agent token
    agentClient := userClient.WithAgentToken(agentResp.Token)
    
    var heartbeat map[string]string
    err = agentClient.Post(context.Background(), &heartbeat, "/api/v1/agent_heartbeat", nil)
    require.NoError(t, err)
    require.Equal(t, "ok", heartbeat["status"])
}

Next Steps

Project Tokens

Learn about programmatic access tokens

User Tokens

Understand user authentication

Build docs developers (and LLMs) love