Skip to main content

Command Palette

Search for a command to run...

Improving Security in Agent-to-Agent Communication

Updated
•10 min read

Introduction

In my earlier blogpost, we built a Coordinator/Dispatcher Agent that routes tasks to specialized workers. We also read about how agent patterns mirror microservice patterns—API Gateways, Sagas, and Domain-Driven Design. But there's one critical piece we haven't touched: Security.

  • When Agent A asks Agent B for customer data, how does Agent B know that Agent A is authorized?

  • Can Agent C impersonate Agent A?

  • What if a compromised agent starts asking for sensitive information?

Common misunderstanding:

If we treat agent-to-agent (A2A) communication as just another API call, we miss the unique challenges of autonomous systems:

  • Agents can be compromised and used to launch attacks.

  • Agents can have dynamic identities (they spawn and die).

  • Agents may need fine-grained permissions (e.g., "read:customer" but not "write:customer").

  • We need audit trails that prove which agent did what.

In this post, we'll build a practical, production-ready security layer for A2A communication. We'll use JWT tokens with claims, an agent registry, and permission-based authorization. By the end, you'll have a reusable library that secures any agent‑to‑agent call in your system.


Part 1: Basics – Why A2A Security Is Different

The Microservice Analogy (and Why It Falls Short)

In microservices, we secure communication with:

  • Service-to-service authentication (mTLS, API keys)

  • Authorization (OAuth2 scopes, RBAC)

  • Audit logging

Agents introduce new complexities:

AspectMicroserviceAI Agent
IdentityFixed service accountEphemeral agent instances, may change per request
AuthorizationBased on caller roleBased on agent's intent and capabilities
Threat modelExternal attackersAlso compromised agents that were once trusted
AuditLog which service calledLog why the agent called (the user's original request)

Core Principles for A2A Security

  1. Authenticate every request – The receiving agent must cryptographically verify the caller's identity.

  2. Authorize based on permissions – Each agent should have a set of permissions (e.g., "read:orders", "write:customers"). The caller's token must contain the required permission for the endpoint.

  3. Use short-lived tokens – If a token is stolen, it expires quickly.

  4. Maintain an agent registry – A central place to register agents, their public keys (if using asymmetric signatures), and their permissions.

  5. Audit everything – Log every A2A call, including the caller, endpoint, and result.

My Approach: JWT + Agent Registry

We'll use JSON Web Tokens (JWT) because they are stateless, widely supported, and can carry custom claims (like permissions). Each agent will:

  • Obtain a token from a central Token Service (or generate its own using a shared secret/asymmetric key).

  • Include that token in the Authorization header of every HTTP request to another agent.

  • The receiving agent will validate the token (signature, expiry, audience) and check if the token's permissions include the required action.

We'll also build an Agent Registry that stores:

  • Agent ID

  • Permissions (as a list of strings)

  • Public key (if using RSA; otherwise, we use a shared symmetric key for simplicity)

For this tutorial, we'll use symmetric signing (HMAC-SHA256) for simplicity.
Note :In production, consider asymmetric keys (RSA/ECDSA) so agents don't need to share a secret.


Part 2: Architecture Overview

  1. Agent Registry stores agent metadata.

  2. Token Service issues JWTs for agents based on their identity.

  3. Agent A requests a token (or generates its own) and includes it in the request to Agent B.

  4. Agent B validates the token and checks permissions before executing the request.

  5. Audit logs are written at both ends.


Part 3: Practical Implementation – Step by Step

We'll assume you have a .NET 8 solution with two agent projects: AgentA and AgentB. They communicate via HTTP (ASP.NET Core minimal APIs or controllers). We'll also create a shared class library AgentSecurity for common code.

Step 1: Define Agent Identity and Registry

First, we need a way to represent an agent and its permissions. The registry can be as simple as an in‑memory dictionary for demo purposes, but in production you'd use a database.

AgentSecurity/AgentIdentity.cs

namespace AgentSecurity;

public class AgentIdentity
{
    public string AgentId { get; set; }
    public string[] Permissions { get; set; } = Array.Empty<string>();
    public string? PublicKey { get; set; } 
}

public interface IAgentRegistry
{
    Task<AgentIdentity?> GetAgentAsync(string agentId);
    Task<bool> HasPermissionAsync(string agentId, string requiredPermission);
}

public class InMemoryAgentRegistry : IAgentRegistry
{
    private readonly Dictionary<string, AgentIdentity> _agents = new();

    public InMemoryAgentRegistry()
    {
        // Pre‑register some agents for demo
        _agents["agent-customer-reader"] = new AgentIdentity
        {
            AgentId = "agent-customer-reader",
            Permissions = new[] { "read:customer" }
        };
        _agents["agent-order-writer"] = new AgentIdentity
        {
            AgentId = "agent-order-writer",
            Permissions = new[] { "write:order", "read:order" }
        };
    }

    public Task<AgentIdentity?> GetAgentAsync(string agentId)
        => Task.FromResult(_agents.TryGetValue(agentId, out var agent) ? agent : null);

    public Task<bool> HasPermissionAsync(string agentId, string requiredPermission)
    {
        if (_agents.TryGetValue(agentId, out var agent))
        {
            return Task.FromResult(agent.Permissions.Contains(requiredPermission));
        }
        return Task.FromResult(false);
    }
}

Step 2: Token Service – Issue and Validate JWTs

We'll use the System.IdentityModel.Tokens.Jwt package. Install it in the shared project.

AgentSecurity/TokenService.cs

using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace AgentSecurity;

public interface ITokenService
{
    string GenerateToken(string agentId, string[] permissions, TimeSpan expiry);
    Task<ClaimsPrincipal?> ValidateTokenAsync(string token);
}

public class JwtTokenService : ITokenService
{
    private readonly SymmetricSecurityKey _key;
    private readonly string _issuer;
    private readonly string _audience;
    private readonly IAgentRegistry _registry;

    public JwtTokenService(string secretKey, string issuer, string audience, IAgentRegistry registry)
    {
        _key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
        _issuer = issuer;
        _audience = audience;
        _registry = registry;
    }

    public string GenerateToken(string agentId, string[] permissions, TimeSpan expiry)
    {
        var claims = new List<Claim>
        {
            new Claim(JwtRegisteredClaimNames.Sub, agentId),
            new Claim("agent_id", agentId),
            new Claim("permissions", string.Join(",", permissions))
        };

        var creds = new SigningCredentials(_key, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            issuer: _issuer,
            audience: _audience,
            claims: claims,
            expires: DateTime.UtcNow.Add(expiry),
            signingCredentials: creds
        );

        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    public async Task<ClaimsPrincipal?> ValidateTokenAsync(string token)
    {
        var handler = new JwtSecurityTokenHandler();
        try
        {
            var principal = handler.ValidateToken(token, new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidIssuer = _issuer,
                ValidateAudience = true,
                ValidAudience = _audience,
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = _key,
                ValidateLifetime = true,
                ClockSkew = TimeSpan.Zero
            }, out _);

            // Optionally, check that the agent still exists and hasn't been revoked
            var agentId = principal.FindFirstValue("agent_id");
            if (string.IsNullOrEmpty(agentId) || await _registry.GetAgentAsync(agentId) == null)
            {
                return null;
            }

            return principal;
        }
        catch
        {
            return null;
        }
    }
}

Step 3: Authenticated HTTP Client for Caller Agent

Agent A needs an HTTP client that automatically attaches a token. We'll create a typed client.

AgentSecurity/AuthenticatedAgentClient.cs

using System.Net.Http.Headers;
using System.Text.Json;

namespace AgentSecurity;

public interface IAuthenticatedAgentClient
{
    Task<TResponse?> PostAsync<TRequest, TResponse>(string url, TRequest request, string requiredPermission);
}

public class AuthenticatedAgentClient : IAuthenticatedAgentClient
{
    private readonly HttpClient _httpClient;
    private readonly ITokenService _tokenService;
    private readonly string _callerAgentId;
    private readonly string[] _callerPermissions;

    public AuthenticatedAgentClient(
        HttpClient httpClient,
        ITokenService tokenService,
        string callerAgentId,
        string[] callerPermissions)
    {
        _httpClient = httpClient;
        _tokenService = tokenService;
        _callerAgentId = callerAgentId;
        _callerPermissions = callerPermissions;
    }

    public async Task<TResponse?> PostAsync<TRequest, TResponse>(
        string url,
        TRequest request,
        string requiredPermission)
    {
        // Ensure caller has the required permission (early check and early exit)
        if (!_callerPermissions.Contains(requiredPermission))
        {
            throw new UnauthorizedAccessException($"Agent {_callerAgentId} lacks permission {requiredPermission}");
        }

        // Generate a short‑lived token for this request
        var token = _tokenService.GenerateToken(_callerAgentId, _callerPermissions, TimeSpan.FromMinutes(5));

        var httpRequest = new HttpRequestMessage(HttpMethod.Post, url);
        httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        httpRequest.Content = JsonContent.Create(request);

        var response = await _httpClient.SendAsync(httpRequest);
        response.EnsureSuccessStatusCode();

        var json = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<TResponse>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
    }
}

Step 4: Server‑Side Middleware for Authentication/Authorization

On the receiving agent (Agent B), we need middleware that validates the token and checks permissions before the request reaches the endpoint.

Create an ASP.NET Core minimal API with authentication and authorization.

AgentB/Program.cs

using AgentSecurity;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

// Configuration
var secretKey = builder.Configuration["Jwt:SecretKey"] ?? "your-256-bit-secret-key-here";
var issuer = builder.Configuration["Jwt:Issuer"] ?? "agent-system";
var audience = builder.Configuration["Jwt:Audience"] ?? "agent-audience";

// Register services
builder.Services.AddSingleton<IAgentRegistry, InMemoryAgentRegistry>();
builder.Services.AddSingleton<ITokenService>(sp =>
    new JwtTokenService(secretKey, issuer, audience, sp.GetRequiredService<IAgentRegistry>()));

// Add authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = issuer,
            ValidateAudience = true,
            ValidAudience = audience,
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)),
            ValidateLifetime = true,
            ClockSkew = TimeSpan.Zero
        };
    });

builder.Services.AddAuthorization();

// Add permission-based authorization handler (optional but convenient)
builder.Services.AddSingleton<IAuthorizationHandler, PermissionHandler>();

builder.Services.AddControllers(); // or minimal APIs, we'll use controllers for clarity

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

Create a custom authorization handler that checks for a required permission claim.

AgentSecurity/PermissionHandler.cs

using Microsoft.AspNetCore.Authorization;

public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
    {
        var permissionsClaim = context.User.FindFirst("permissions")?.Value;
        if (permissionsClaim != null)
        {
            var permissions = permissionsClaim.Split(',', StringSplitOptions.RemoveEmptyEntries);
            if (permissions.Contains(requirement.Permission))
            {
                context.Succeed(requirement);
            }
        }
        return Task.CompletedTask;
    }
}

public class PermissionRequirement : IAuthorizationRequirement
{
    public string Permission { get; }
    public PermissionRequirement(string permission) => Permission = permission;
}

// Extension method to easily add [HasPermission("read:customer")]
public static class AuthorizationPolicyBuilderExtensions
{
    public static AuthorizationPolicyBuilder RequirePermission(this AuthorizationPolicyBuilder builder, string permission)
    {
        builder.Requirements.Add(new PermissionRequirement(permission));
        return builder;
    }
}

Now create a controller in AgentB that requires a specific permission.

AgentB/Controllers/CustomerController.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/customer")]
[Authorize]
public class CustomerController : ControllerBase
{
    [HttpGet("{id}")]
    [Authorize(Policy = "ReadCustomer")]
    public IActionResult GetCustomer(int id)
    {
        // This would normally fetch from a database, for demo purpuse it is predefined here
        var customer = new { Id = id, Name = "John Doe", Email = "john@example.com" };
        return Ok(customer);
    }

    [HttpPost]
    [Authorize(Policy = "WriteCustomer")]
    public IActionResult CreateCustomer([FromBody] CreateCustomerRequest request)
    {
        // create customer
        return Ok(new { Id = 123 });
    }
}

Register the policies in Program.cs:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ReadCustomer", policy =>
        policy.Requirements.Add(new PermissionRequirement("read:customer")));
    options.AddPolicy("WriteCustomer", policy =>
        policy.Requirements.Add(new PermissionRequirement("write:customer")));
});

Step 5: Calling from Agent A

Now, in Agent A, we'll use the AuthenticatedAgentClient to call Agent B.

AgentA/Program.cs (excerpt)

using AgentSecurity;

// Setup
var registry = new InMemoryAgentRegistry();
var tokenService = new JwtTokenService("your-256-bit-secret-key-here", "agent-system", "agent-audience", registry);

// Assume Agent A's identity is "agent-customer-reader" (permissions: read:customer)
var client = new AuthenticatedAgentClient(
    new HttpClient { BaseAddress = new Uri("http://localhost:5001/") }, // Agent B's URL
    tokenService,
    callerAgentId: "agent-customer-reader",
    callerPermissions: new[] { "read:customer" }
);

// Call Agent B to get customer 42
try
{
    var customer = await client.PostAsync<object, CustomerResponse>(
        "api/customer/42",   // Note: using GET would be better, but we use POST for demo
        null,                // no request body for GET
        requiredPermission: "read:customer"
    );
    Console.WriteLine($"Got customer: {customer.Name}");
}
catch (UnauthorizedAccessException ex)
{
    Console.WriteLine($"Permission denied: {ex.Message}");
}
catch (HttpRequestException ex)
{
    Console.WriteLine($"Request failed: {ex.Message}");
}

For a GET request, we'd need a different method. You can extend the client accordingly.

Step 6: Add Audit Logging

We should log every A2A call. Add an IAuditLogger interface and a simple console implementation.

AgentSecurity/IAuditLogger.cs

public interface IAuditLogger
{
    void LogCall(string callerAgentId, string targetUrl, string requiredPermission, bool success, string? error = null);
}

public class ConsoleAuditLogger : IAuditLogger
{
    public void LogCall(string callerAgentId, string targetUrl, string requiredPermission, bool success, string? error = null)
    {
        Console.WriteLine($"[AUDIT] {DateTime.UtcNow:O} | Caller: {callerAgentId} | URL: {targetUrl} | Required: {requiredPermission} | Success: {success} | Error: {error}");
    }
}

Inject this into AuthenticatedAgentClient and log after each call. Also log on the server side (in middleware or action filter) to capture the request before validation.

Step 7: Testing the Flow

  1. Run Agent B (port 5001).

  2. Run Agent A (console app) and try to call api/customer/42. It should succeed because Agent A has "read:customer".

  3. Modify Agent A's permissions to remove "read:customer" and run again. The client will throw UnauthorizedAccessException before even sending the request (due to the early check). If you bypass the early check, the server will return 403 Forbidden because the token won't contain the required permission.

  4. Try calling a write endpoint (e.g., POST to api/customer). The client will fail the early permission check because Agent A lacks "write:customer".

Step 8: Advanced: Mutual TLS (mTLS) for Transport Security

While JWT handles authentication and authorization, you still need transport security (HTTPS). For higher security, consider mutual TLS, where both sides present certificates. This ensures that even if a token is stolen, the attacker cannot connect without the correct client certificate. Many cloud environments (like Azure App Service with TLS mutual authentication) support this.

You can combine mTLS + JWT for defense in depth: the TLS connection verifies the agent's machine identity, while the JWT carries the agent's logical identity and permissions.


Part 4: Conclusion & Next Steps

We've built a complete security layer for agent-to-agent communication:

  • Authentication with JWT tokens signed by a central service.

  • Authorization with permission claims checked both client‑side and server‑side.

  • Audit logging to track who called what.

  • Integration with ASP.NET Core's authentication and authorization pipeline.

This pattern mirrors the API Gateway / Coordinator pattern we explored earlier—now with security baked in.

Key Takeaways

  • Never trust agent identity from the network layer alone – always include authentication in the application layer.

  • Use short‑lived tokens to limit the impact of token theft.

  • Define fine‑grained permissions per agent, aligned with the capabilities they expose.

  • Audit everything – you'll need it for compliance and debugging.

  • Consider mTLS for high‑security environments.