Directory v1.0: Dual-Mode Authentication for Secure Agent Discovery
The Agent Directory is more than just a service registry—it’s a trusted gateway for discovering and verifying AI agents. With the release of v1.0, we’re introducing a robust dual-mode authentication system that combines the convenience of GitHub OAuth for human operators with the security of SPIFFE workload identity for automated services.
This post explores the architecture, implementation, and practical usage of Directory’s new authentication system, designed for both interactive CLI users and automated workloads.
The Challenge: Two Types of Users
When building the Agent Directory, we encountered a fundamental question: Who should be allowed to access the system?
The answer wasn’t simple. We had two distinct user personas with very different needs:
- Human Operators (developers, administrators) need interactive authentication via CLI tools with familiar OAuth flows
- Automated Workloads (services, agents, applications) need machine-to-machine authentication with automatic certificate rotation in Kubernetes
Traditional authentication systems force you to choose one approach. We built a system that supports both.
The Solution: Dual-Mode Authentication
Directory v1.0 introduces a dual-mode authentication architecture that seamlessly handles both human and workload authentication:
flowchart LR
classDef workload fill:#0251af,stroke:#f3f6fd,stroke-width:2px,color:#f3f6fd;
classDef human fill:#03142b,stroke:#0251af,stroke-width:2px,color:#f3f6fd;
classDef gateway fill:#0251af,stroke:#f3f6fd,stroke-width:3px,color:#f3f6fd;
classDef api fill:#03142b,stroke:#0251af,stroke-width:3px,color:#f3f6fd;
subgraph Col1["🤖 Workload Authentication"]
direction TB
W[Agent/Service<br/>Kubernetes Pod]:::workload
SPIRE[SPIRE Agent<br/>Workload Identity]:::workload
W -->|Request<br/>Certificate| SPIRE
end
subgraph Col2["🏢 Directory Service"]
direction TB
API[Directory API<br/>SPIFFE-only Auth]:::api
end
subgraph Col3["👤 Human Authentication"]
direction TB
H[Developer<br/>dirctl CLI]:::human
GH[GitHub OAuth<br/>Device Flow]:::human
Envoy[Envoy Gateway<br/>ext_authz + RBAC]:::gateway
H -->|OAuth<br/>Token| GH
GH -->|Validated| Envoy
end
SPIRE ==>|mTLS +<br/>SPIFFE ID| API
Envoy ==>|Authorized via<br/>Envoy's SPIFFE ID| API
Key Insight: The Directory API itself has zero authentication code. It only validates SPIFFE IDs, trusting that callers have already been authenticated upstream.
Part 1: Authentication for Human Operators
The most common way to interact with Directory is through the dirctl CLI. This section covers everything you need to know about authenticating as a human user using GitHub OAuth.
Why GitHub OAuth?
Human operators need:
- ✅ Interactive authentication - Simple browser-based login flow
- ✅ No cluster access required - Works from any laptop or CI/CD runner
- ✅ Familiar experience - Uses your existing GitHub account
- ✅ Organization-based access - Leverage GitHub org memberships for authorization
This makes GitHub OAuth perfect for developers, administrators, and CI/CD pipelines.
How It Works: Architecture & Flow
For human operators using the dirctl CLI, we use GitHub OAuth 2.0 Device Flow. This provides a familiar authentication experience without requiring a local web server.
sequenceDiagram
participant User as Developer
participant CLI as dirctl CLI
participant GitHub as GitHub OAuth
participant Envoy as Envoy Gateway
participant API as Directory API
Note over User,GitHub: Authentication Phase
User->>CLI: dirctl auth login
CLI->>GitHub: Request Device Code
GitHub->>CLI: Device Code + URL
CLI->>User: Display: Visit URL<br/>Enter Code: ABC-123
User->>GitHub: Opens Browser<br/>Enters Device Code
GitHub->>User: Authorize dirctl?
User->>GitHub: Yes, Authorize
CLI->>GitHub: Poll for Token
GitHub->>CLI: Access Token
Note over User,API: API Call Phase
User->>CLI: dirctl routing list
CLI->>Envoy: gRPC Call + Bearer Token
Envoy->>Envoy: Validate Token<br/>Check RBAC Rules
Envoy->>API: Forward Request<br/>(as Envoy's SPIFFE ID)
API->>Envoy: Response
Envoy->>CLI: Response
CLI->>User: Display Results
How it works:
- Device Flow Initiation: CLI requests a device code from GitHub
- User Authorization: User visits GitHub and authorizes the application
- Token Issuance: CLI receives an OAuth access token
- Envoy Gateway: Token is sent to Envoy, which validates it and enforces RBAC
- SPIFFE Impersonation: Envoy calls the API using its own SPIFFE ID
Key Advantage: The Directory API never sees user tokens - it only sees Envoy’s trusted SPIFFE ID.
The Envoy Gateway: Policy Enforcement Point
The Envoy gateway is the linchpin of the human authentication flow. It handles:
- Token Validation - Verifies GitHub OAuth tokens
- Authorization - Enforces role-based access control (RBAC)
- Protocol Translation - Converts OAuth → SPIFFE
flowchart LR
classDef input fill:#0251af,stroke:#f3f6fd,stroke-width:2px,color:#f3f6fd;
classDef process fill:#03142b,stroke:#0251af,stroke-width:2px,color:#f3f6fd;
classDef output fill:#0251af,stroke:#f3f6fd,stroke-width:2px,color:#f3f6fd;
User[User Request<br/>Bearer: gho_xxx]:::input
subgraph Envoy["Envoy Gateway"]
ExtAuthz[ext_authz Filter<br/>Validate Token]:::process
RBAC[RBAC Filter<br/>Check Permissions]:::process
Router[Router<br/>Forward Request]:::process
end
API[Directory API<br/>Accepts: SPIFFE ID]:::output
User --> ExtAuthz
ExtAuthz -->|Valid User| RBAC
RBAC -->|Authorized| Router
Router -->|mTLS + Envoy SPIFFE ID| API
ExtAuthz -.->|Invalid Token| Reject[401 Unauthorized]
RBAC -.->|Forbidden| Reject2[403 Forbidden]
Envoy Configuration Highlights:
- Custom
ext_authzService: Validates GitHub tokens and extracts user identity - Casbin-Based RBAC: Policy-driven authorization with role assignments for users and organizations
- Fine-Grained Permissions: Control access per API method (e.g., allow read but deny write operations)
- SPIFFE Integration: Uses its own workload identity to call the Directory API
Hands-On: GitHub Authentication
Now that we understand the architecture, let’s walk through GitHub authentication with practical examples using the Agntcy-hosted testbed environment.
Prerequisites
- CLI Tool:
dirctlinstalled (Installation Guide) -
Authorization: Your GitHub username or organization must be in the production environment’s allowed list. In case you are not:
- Contact your Directory administrator if you don’t have access
- See the Authorization section for details on allow lists
Testbed Environment
Agntcy hosts a public testbed environment where you can experiment with Directory authentication without needing your own infrastructure. This environment is ideal for learning, testing, and prototyping.
Testbed Endpoints:
| Service | URL | Purpose |
|---|---|---|
| GitHub OAuth Gateway | https://prod.gateway.ads.outshift.io:443 |
Human authentication via GitHub OAuth (public access) |
Authentication Support:
- ✅ GitHub OAuth: Authenticate interactively using your GitHub account - Available for all testbed users
- ✅ Public Access: No VPN required - accessible from anywhere
- ⚠️ Authorization Required: Your GitHub username or organization must be in the allowed list
- ❌ SPIFFE mTLS: Not available for public testbed users (requires your own SPIRE infrastructure)
Use Cases:
- Testing: Validate your integration before deploying to your own environment
- Learning: Understand how Directory authentication works in practice
- Development: Build and test applications that use Directory APIs
- Demos: Showcase Directory capabilities to stakeholders
Step-by-Step Guide
Perfect for developers who want quick, interactive access without cluster credentials.
Step 1: Configure Environment
export DIRECTORY_CLIENT_SERVER_ADDRESS="prod.gateway.ads.outshift.io:443"
export DIRECTORY_CLIENT_AUTH_MODE="github"
Step 2: Authenticate with GitHub
dirctl auth login
You’ll see:
╔════════════════════════════════════════════════════════════╗
║ GitHub OAuth Authentication (Device Flow) ║
╚════════════════════════════════════════════════════════════╝
🔐 To authenticate, please follow these steps:
1. Visit: https://github.com/login/device
2. Enter code: AEC4-D40B
💡 You can complete this on any device (phone, laptop, etc.)
⏱️ Code expires in 15 minutes
Waiting for authorization...
Open your browser, visit the URL, and enter the code. After authorizing, you’ll see:
✓ Authorization complete!
Fetching user information...
✓ Authenticated as: yourusername (Your Name)
Fetching organization memberships...
✓ Organizations: your-org, another-org
✓ Token cached for future use
Cache location: ~/.config/dirctl/auth-token.json
╔════════════════════════════════════════════════════════════╗
║ Authentication Complete! ✓ ║
╚════════════════════════════════════════════════════════════╝
You can now use 'dirctl' commands with --auth-mode=github
Step 3: Use the CLI
# Get info about a record
dirctl info baeareiesad3lyuacjirp6gxudrzheltwbodtsg7ieqpox36w5j637rchwq
# Pull a specific record
dirctl pull baeareiesad3lyuacjirp6gxudrzheltwbodtsg7ieqpox36w5j637rchwq -o json
# Check authentication status
dirctl auth status
Token Caching: Your OAuth token is securely cached at ~/.config/dirctl/auth-token.json. Subsequent commands automatically use the cached token until it expires (default: 8 hours).
Step 4: Check Authorization Logs (Self-Hosted Only)
If you’re running your own Directory deployment and have cluster access, you can verify that requests are being authorized:
# Replace with your namespace and label selectors
kubectl logs -n <your-namespace> -l app=envoy-authz-authz --tail=50 | grep yourusername
Note: This step is not available for public testbed users as it requires Kubernetes cluster access.
Authorization: Role-Based Access Control (RBAC)
Now that you’ve successfully authenticated, the next critical question is: what can you actually do? Authentication proves who you are, but authorization determines what you can access.
The Envoy gateway enforces authorization using a role-based access control (RBAC) system powered by Casbin, configured in the Helm chart:
# Helm values for Envoy gateway
authServer:
authorization:
# Default role for any authenticated user (empty = deny by default)
defaultRole: "reader"
# Explicitly deny specific users (highest priority)
userDenyList:
- "github:malicious-user"
# Role definitions
roles:
# Admin role - full access to all API methods
admin:
allowedMethods:
- "*" # Wildcard = all Directory API methods
users:
- "github:alexdemo2026"
- "github:saradev2026"
orgs:
- "agntcy" # All members of "agntcy" org get admin access
# Reader role - read-only access
reader:
allowedMethods:
- "/agntcy.dir.store.v1.StoreService/Pull"
- "/agntcy.dir.store.v1.StoreService/Lookup"
- "/agntcy.dir.routing.v1.RoutingService/Search"
- "/agntcy.dir.routing.v1.RoutingService/List"
- "/agntcy.dir.search.v1.SearchService/SearchCIDs"
# ... more read-only methods
users:
- "github:mikeprod2026"
orgs:
- "contributors"
Authorization Logic (Precedence Order):
- Deny List (highest priority) - Blocks access even if user has a role
- User Role - Direct user-to-role assignment (e.g., specific users in
admin.users) - Organization Role - Org-to-role assignment (e.g., all members of
agntcyorg) - Default Role - Fallback for any authenticated user not explicitly assigned
- Deny by Default - If no role matches and no default role is set
Key Features:
- Policy-Driven: Authorization rules defined in YAML, no code changes needed
- Fine-Grained: Control access per API method (e.g., allow
Pullbut denyPush) - Flexible Wildcards: Use
"*"for full access or specify individual methods - Org-Based Assignment: Leverage GitHub organization membership for role assignment
Security & Best Practices
Understanding how to use authentication is important, but implementing it securely is critical. Let’s examine key security practices for managing GitHub tokens and integrating with CI/CD pipelines.
Token Security
GitHub OAuth Tokens:
- Tokens are stored with 0600 permissions (owner read/write only)
- Tokens expire after 8 hours (configurable)
- Token cache location:
~/.config/dirctl/auth-token.json - Never commit tokens to version control
- Use environment variables or secrets management for automation
CI/CD Integration
For CI/CD pipelines, you’ll need to provide authentication credentials programmatically. Personal Access Tokens (PATs) are the recommended approach for automated workflows.
Personal Access Token Setup
Create and configure a PAT for CI/CD authentication:
# GitHub Actions example using PAT
name: Deploy Agent
on: [push]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Authenticate with Directory
env:
DIRECTORY_CLIENT_SERVER_ADDRESS: "prod.gateway.ads.outshift.io:443"
DIRECTORY_CLIENT_AUTH_MODE: "github"
DIRECTORY_CLIENT_GITHUB_TOKEN: $
run: |
dirctl auth login
dirctl auth status
- name: Push agent card
env:
DIRECTORY_CLIENT_SERVER_ADDRESS: "prod.gateway.ads.outshift.io:443"
DIRECTORY_CLIENT_AUTH_MODE: "github"
run: |
dirctl push ./agent-card.json
Setup Steps:
- Create a GitHub Personal Access Token:
- Go to: https://github.com/settings/tokens/new
- Select scopes:
user:emailandread:org - Generate token and copy it immediately
- Store as repository secret:
- Go to your repository settings:
https://github.com/<org>/<repo>/settings/secrets/actions - Click “New repository secret”
- Name:
DIRECTORY_GITHUB_PAT - Value: Paste your token
- Go to your repository settings:
- Configure RBAC authorization:
authServer: authorization: roles: writer: # Or any role with required permissions allowedMethods: - "/agntcy.dir.store.v1.StoreService/Push" - "/agntcy.dir.store.v1.StoreService/Pull" users: - "github:your-username" # Add your GitHub username orgs: - "your-org" # Or authorize entire organization
Best Practices:
- ✅ Store PATs in repository secrets (never commit to code)
- ✅ Use dedicated service accounts for automation
- ✅ Rotate PATs regularly (e.g., quarterly)
- ✅ Scope tokens to minimum required permissions
- ✅ Ensure your user/org is assigned to an appropriate RBAC role
Security Notes:
- PATs should be treated as sensitive credentials and never committed to version control
- Use GitHub’s repository secrets or organization secrets for secure storage
- Consider using different PATs for different environments (dev, staging, prod)
- Audit PAT usage regularly through GitHub’s security logs
Part 2: Authentication for Automated Workloads
While GitHub OAuth is perfect for humans, automated services and agents running in Kubernetes need a different approach. This section covers SPIFFE-based workload identity for machine-to-machine authentication.
Why SPIFFE?
Automated workloads need:
- ✅ No human intervention - Fully automated certificate lifecycle
- ✅ Strong cryptographic identity - Workload-to-workload mTLS
- ✅ Automatic rotation - Certificates renew without restarts
- ✅ Kubernetes-native - Identity tied to pods, namespaces, and service accounts
This makes SPIFFE perfect for production services, agents, and microservices running in Kubernetes clusters.
How It Works: Architecture & Flow
SPIFFE (Secure Production Identity Framework For Everyone) provides workload identity through mTLS certificates. This is ideal for service-to-service communication within Kubernetes.
sequenceDiagram
participant Service as Agent/Service
participant Agent as SPIRE Agent
participant API as Directory API
Note over Service,Agent: Startup Phase
Service->>Agent: Request SPIFFE Certificate
Agent->>Agent: Verify Pod Identity<br/>(ServiceAccount, Labels, etc.)
Agent->>Service: Issue X.509-SVID Certificate<br/>(spiffe://prod.ads.outshift.io/my-agent)
Note over Service,API: API Call Phase
Service->>API: gRPC Call + mTLS Certificate
API->>API: Validate SPIFFE ID
alt Authorized Workload
API->>Service: 200 OK + Data
else Unauthorized
API->>Service: 403 Forbidden
end
How it works:
- Workload Registration: Services are registered in SPIRE with their intended SPIFFE ID
- Certificate Issuance: SPIRE Agent validates the workload and issues a short-lived X.509 certificate
- mTLS Communication: The service uses this certificate for mutual TLS with the Directory API
- Automatic Rotation: Certificates rotate automatically (typically every 1 hour)
Security Benefits:
- ✅ No secrets to manage - No passwords, API keys, or tokens
- ✅ Automatic expiration - Certificates rotate without human intervention
- ✅ Strong identity binding - Identity tied to Kubernetes resources
- ✅ Zero-trust security - Every call is cryptographically authenticated
Hands-On: SPIFFE Authentication
Note: SPIFFE authentication is not available for the public testbed environment. This option is for organizations running their own Directory deployment with SPIRE infrastructure.
Ideal for services running within your own Kubernetes cluster that need programmatic access. This method works best in environments where the SPIRE Agent is deployed as a DaemonSet, allowing workloads to automatically obtain their identity certificates via the Workload API.
Two Approaches
- Production Workloads (Recommended): Use SPIRE Agent + Workload API for fully automated certificate management
- Local Development/Testing: Manually export certificates from SPIRE Server for experimentation on your laptop
The manual export approach is only for demonstration and experimenting in your local environment. It allows you to test SPIFFE authentication without needing the full SPIRE infrastructure on your machine. Production workloads should always use the SPIRE Agent for automatic, zero-touch certificate management.
Manual Certificate Export (Local Testing Only)
When you want to test Directory authentication from your laptop without SPIRE Agent, you can manually mint a certificate from your own SPIRE server (requires cluster access and administrative permissions):
Step 1: Mint SPIFFE Certificate
# Replace with your own SPIRE server pod name and namespace
kubectl exec -n <your-spire-namespace> <your-spire-server-pod> \
-c spire-server -- \
/opt/spire/bin/spire-server x509 mint \
-dns <your-directory-api-address> \
-spiffeID spiffe://<your-trust-domain>/demo-client \
-ttl 1h \
-output json > spiffe-local.json
Example for your own deployment:
kubectl exec -n my-spire spire-server-0 \
-c spire-server -- \
/opt/spire/bin/spire-server x509 mint \
-dns directory.mycompany.com \
-spiffeID spiffe://mycompany.com/demo-client \
-ttl 1h \
-output json > spiffe-local.json
Step 2: Configure Environment
# Use your own Directory deployment endpoints
export DIRECTORY_CLIENT_SERVER_ADDRESS="<your-directory-api>:443"
export DIRECTORY_CLIENT_AUTH_MODE="token"
export DIRECTORY_CLIENT_SPIFFE_TOKEN="spiffe-local.json"
Step 3: Use the CLI
dirctl info <cid>
dirctl pull <cid> -o json
Production Deployment (Recommended)
The manual certificate export approach above is useful for testing and debugging, but production workloads should use the SPIRE Workload API to automatically obtain and rotate certificates. This requires the SPIRE Agent to be running on the same node as your workload (typically deployed as a Kubernetes DaemonSet).
Key Benefits of SPIRE Agent Integration:
- ✅ Zero manual steps - Certificates are obtained automatically via the Workload API
- ✅ Automatic rotation - Certificates renew before expiration without restarts
- ✅ No exported files - No need to manage
spiffe-local.jsonfiles - ✅ Production-ready - The recommended approach for all Kubernetes workloads
- ✅ Just works - Your service discovers the SPIRE Agent via the standard Unix domain socket
When to Use Manual Certificate Export:
- Testing/debugging from your laptop (outside the cluster)
- CI/CD runners without SPIRE Agent deployed
- One-time administrative tasks
- Development environments without full SPIRE infrastructure
For implementation details, see the Directory Go client documentation and SPIRE Workload API guide.
Authorization: Workload Identity Allow Lists
The Directory API maintains an allow list of trusted SPIFFE IDs:
// Directory API configuration (in your self-hosted deployment)
authorizedIDs := []spiffeid.ID{
spiffeid.RequireFromString("spiffe://mycompany.com/envoy"),
spiffeid.RequireFromString("spiffe://mycompany.com/agent-service"),
spiffeid.RequireFromString("spiffe://mycompany.com/indexer"),
}
Authorization Logic:
- Extract SPIFFE ID from client certificate
- Check against authorized ID list
- Deny by default if not found
Why This Matters:
- ✅ Least Privilege - Only explicitly authorized workloads can access the API
- ✅ Defense in Depth - Even if an attacker compromises a pod, they can’t access the Directory without the correct SPIFFE ID
- ✅ Audit Trail - Every request is logged with the authenticated SPIFFE ID
Security & Best Practices
SPIFFE Certificate Security:
- Certificates are short-lived, typically one hour
- Certificates auto-rotate before expiration
- Certificates are cryptographically bound to workload identity
- No manual rotation required
- No secrets stored - All key material is ephemeral
Production Best Practices:
- Deploy SPIRE Agent as a DaemonSet in all nodes
- Use pod selectors for fine-grained workload registration
- Monitor certificate expiry and rotation
- Implement proper SPIRE Server high availability
- Use SPIRE’s federation features for multi-cluster scenarios
Part 3: Choosing the Right Method
Comparison: When to Use Each Method
With both authentication methods available, you might wonder which one to choose. The answer depends on your use case, environment, and operational requirements. Here’s a comprehensive comparison to help you decide:
| Criteria | GitHub OAuth | SPIFFE (mTLS) |
|---|---|---|
| Use Case | Human operators | Automated workloads |
| Testbed Support | ✅ Available | ❌ Not available |
| Self-Hosted Support | ✅ Available | ✅ Available |
| Environment | CLI, laptops, CI/CD | Kubernetes clusters with SPIRE |
| Setup Complexity | Low (just GitHub account) | Medium (requires SPIRE infrastructure) |
| Security Model | User identity | Workload identity |
| Token Lifetime | Medium (8 hours) | Short (1 hour) |
| Rotation | Manual (re-login) | Automatic (with SPIRE Agent) |
| Production Workflow | Login once per 8 hours | Fully automated via Workload API |
| Local Testing | Same as production | Manual cert export (demo/experimentation) |
| Cluster Access | Not required | Required for setup |
| Best For | Development, testing, demos | Production services in self-hosted |
Rule of Thumb:
- For Agntcy Testbed: Use GitHub OAuth (only authentication method available for public testbed)
- For Human Users (any environment): Use GitHub OAuth for CLI interactions, development, and CI/CD
- For Self-Hosted Production Workloads: Use SPIFFE with SPIRE Agent for Kubernetes services (fully automated, zero manual steps)
- For Local Development/Testing: Use SPIFFE manual cert export for experimentation with your own infrastructure (demo purposes only)
What’s Next?
Directory v1.0 provides a solid foundation for secure agent discovery with fine-grained RBAC already implemented. The authentication story continues to evolve with these planned enhancements:
- OIDC Provider Support: Integration with enterprise identity providers (Okta, Azure AD, Zitadel)
- Enhanced RBAC: Per-resource permissions (e.g., read/write access to specific CIDs or namespaces)
- Audit Logging: Comprehensive access logs for compliance and security monitoring
- Multi-Tenancy: Namespace-based isolation for multi-organization deployments
- Dynamic Policy Updates: Hot-reload RBAC policies without service restarts
Conclusion
Directory v1.0’s dual-mode authentication architecture demonstrates that security doesn’t have to compromise usability. By combining GitHub OAuth, SPIFFE workload identity, and Casbin-based RBAC, we’ve created a system that’s:
- ✅ Developer-friendly - Familiar OAuth flows, cached tokens, simple CLI
- ✅ Secure by default - Zero-trust, mutual TLS, automatic rotation, fine-grained permissions
- ✅ Production-ready - Battle-tested in real-world environments with policy-driven authorization
- ✅ Flexible - Supports both human operators and automated workloads with customizable roles
Whether you’re building AI agents, deploying microservices, or managing a service mesh, the Agent Directory provides the trusted foundation for secure service discovery with enterprise-grade access control.