Skip to content

Internal API Authentication

Mercury services communicate with each other behind the YARP gateway. Some endpoints are internal-only — they must never be callable from outside the cluster. The X-Internal-Key header and [RequiresInternalKey] attribute enforce this.

How it works

External client → Gateway (YARP) → strips X-Internal-Key → Backend API → 403
Internal service → direct HTTP (no gateway) → sends X-Internal-Key → Backend API → 200

The gateway removes the X-Internal-Key header from every inbound request before forwarding it to backend services. This means even if an external caller sends the header, it never reaches the API. Only services inside the cluster that call each other directly (bypassing the gateway) can include the header.

Configuration

Each service that needs to validate or send internal requests reads the key from appsettings.json:

{
  "InternalApi": {
    "Key": "dev-internal-key"
  }
}

Register it in your service's startup:

builder.Services.Configure<InternalApiOptions>(
    builder.Configuration.GetSection(InternalApiOptions.SectionName));

Warning

The dev-internal-key value is for development only. Production environments use a different key injected via environment variables or secrets management. Never commit production keys.

Protecting an endpoint

Add the [RequiresInternalKey] attribute to any controller or action that should only accept internal calls:

[ApiController]
[Route("api/v1/[controller]")]
[RequiresInternalKey]
public class SmsController : ControllerBase
{
    [HttpPost("send")]
    public async Task<IActionResult> Send([FromBody] SendSmsRequest request)
    {
        // Only reachable with a valid X-Internal-Key header
    }
}

The attribute checks the X-Internal-Key request header against the configured key. If the header is missing or the value does not match, the request gets a 403 Forbidden response.

Sending an internal request

When calling another service directly (not through the gateway), include the header:

using var request = new HttpRequestMessage(HttpMethod.Post, "http://api-identity/api/v1/sms/send");
request.Headers.Add(RequiresInternalKeyAttribute.HeaderName, internalApiOptions.Key);
request.Content = JsonContent.Create(body);

var response = await httpClient.SendAsync(request);

The constant RequiresInternalKeyAttribute.HeaderName resolves to X-Internal-Key. Use it instead of hardcoding the string.

Gateway header stripping

The YARP gateway is configured to strip X-Internal-Key from all routes. This is defined in yarp.json:

{
  "Transforms": [
    { "PathRemovePrefix": "/Identity" },
    { "ResponseHeader": "Source", "Append": "YARP" },
    { "RequestHeaderRemove": "X-Internal-Key" }
  ]
}

Every route (Medicine, Store, Identity) has this transform. If you add a new route to the gateway, include the RequestHeaderRemove transform.

Danger

Forgetting to add RequestHeaderRemove: X-Internal-Key to a new gateway route exposes internal endpoints to external callers.

Currently protected endpoints

Service Endpoint Purpose
Identity API POST /api/v1/sms/send Send SMS (called by Store API)

Source files

File Purpose
Mercury.Core/Identity/RequiresInternalKeyAttribute.cs Authorization filter that checks the header
Mercury.Core/Identity/InternalApiOptions.cs Options class bound to InternalApi config section
Mercury.Gateway/yarp.json Gateway route config with header stripping
Mercury.Store.Api/Tools/GatewayClient.cs Example of sending internal requests