Internal API Access

Overview

Certain platform administration operations (for example resetting a user’s OTP) are exposed by the API under the /api/internal prefix. These endpoints are privileged: they are intended to be called by trusted automation systems (operations scripts, orchestrators, internal tools), and not by browsers or end-user clients using the portal.

The need addressed by this feature is therefore: granting access to /api/internal without exposing it on the usual public surface (the portal or the standard API host), and protecting that access with strong authentication independent of normal user authentication.

To achieve this, /api/internal is published on a dedicated hostname (API_INTERNAL_URL), distinct from the portal and the standard API host, and can be protected by three complementary layers:

  • an IP allowlist (network filtering);

  • dedicated mTLS (mutual TLS authentication with its own PKI, separate from the standard internal PKI);

  • an HMAC key at the application layer, verified by the API itself.

None of these layers is individually mandatory, but they stack: for example, you can enable only IP filtering, or add mTLS for strong transport-layer authentication.

How It Works

When the feature is enabled, a dedicated Traefik route is created:

Host(`API_INTERNAL_URL`) && PathPrefix(`/api/internal`)

This route is attached:

  • to the portal service when the deployment type is infra_manager (the portal already relays /api/* to the API);

  • to the api service when roles are split (api_manager + portal_manager); the route is then opened only on the api_manager side.

The API_INTERNAL_URL hostname acts as an isolated entry point: it is the one that carries the dedicated mTLS authentication. This hostname-based isolation is required because the client PKI is selected during TLS negotiation (based on the hostname), before the /api/internal path is known. It is therefore not possible to assign a different PKI to a simple path on a hostname that is already in use.

Configuration Variables

API_INTERNAL_ENABLED

Enables the feature. Boolean value (true / false). Default: false.

API_INTERNAL_URL

Dedicated hostname on which /api/internal is published (for example apiinternal.example.com). Required as soon as the route must be opened. Must be different from the portal hostname (PORTAL_URL) and the standard reemo_api host.

API_INTERNAL_RESTRICT_IP

List of IP addresses or ranges allowed to reach /api/internal, in comma-separated CIDR format (for example "10.0.0.0/8,192.168.10.20/32"). A request coming from an unlisted IP receives a 403. Leave empty to disable IP filtering.

API_INTERNAL_SSL_CERT

Path to the server certificate that Traefik presents on the API_INTERNAL_URL hostname. Its SAN must include API_INTERNAL_URL.

API_INTERNAL_SSL_KEY

Path to the private key associated with the server certificate above.

API_INTERNAL_SSL_CA

Path to the CA used to validate the client certificate presented by the caller (mTLS). This variable is what actually enables dedicated mTLS: when set, only clients presenting a certificate signed by this CA are accepted.

Note

The three API_INTERNAL_SSL_* variables are optional. If they are not set, the route remains open but without a dedicated PKI. To obtain mTLS with a PKI separate from the standard internal PKI, you must set at least API_INTERNAL_SSL_CA (and provide a valid server certificate for the hostname — see Required Certificates).

API_INTERNAL_HMAC

Shared HMAC key, verified by the API at the application layer on /api/internal calls. Stored as a secret. This layer is independent of mTLS: a caller must present the correct HMAC in addition to satisfying the transport-layer controls.

Behaviour by Topology

Single-site deployment (infra_manager)

The /api/internal route is opened on the portal, on the API_INTERNAL_URL hostname. The portal then relays the request to the API.

In this mode, PORTAL_URL must be set to an exact hostname. If the portal is configured in catch-all mode (without PORTAL_URL), the dedicated route is not created, because the catch-all router would conflict with the mTLS authentication on the dedicated hostname.

Split deployment (api_manager + portal_manager)

The /api/internal route is opened only on api_manager, directly on the API service. The portal_manager does not open the route.

Note

On api_manager, the standard API host already enforces mTLS with the standard internal PKI. The dedicated API_INTERNAL_URL route adds, on its own hostname, mTLS with the dedicated PKI (API_INTERNAL_SSL_CA), allowing two distinct PKIs to coexist: one for regular API traffic, and one for the internal API.

Blocking Access via the Public Channel

To ensure that /api/internal is reachable only via the dedicated hostname, access to the /api/internal path is denied (403) on the public hostname (portal, or standard API host depending on the topology).

Warning

If a legitimate component calls /api/internal via the public host (rather than via API_INTERNAL_URL or directly on the container), this block will prevent it. Verify the path your callers take before enabling this behaviour.

Required Certificates

Two distinct elements are involved in mTLS:

The server certificate (API_INTERNAL_SSL_CERT / API_INTERNAL_SSL_KEY)

This is what Traefik presents to the client. Its SAN must include API_INTERNAL_URL, and it must be issued by an authority the caller trusts (the --cacert on the client side). If no certificate matching the hostname is provided, Traefik presents a self-signed certificate, and the client can only validate the connection by disabling verification (to be avoided).

The client validation CA (API_INTERNAL_SSL_CA)

This is the authority that must have signed the client certificate presented by the caller. A client certificate signed by a different authority (for example the standard internal PKI) will be rejected on this hostname.

Configuration Example

api_manager:
  vars:
    API_INTERNAL_ENABLED: true
    API_INTERNAL_URL: "apiinternal.example.com"
    API_INTERNAL_RESTRICT_IP: "10.0.0.0/8,192.168.10.20/32"
    API_INTERNAL_SSL_CERT: "/local/path/apiinternal.crt"
    API_INTERNAL_SSL_KEY: "/local/path/apiinternal.key"
    API_INTERNAL_SSL_CA: "/local/path/apiinternal_ca.crt"
    API_INTERNAL_HMAC: "<shared HMAC key>"

Verification

Once deployed, an authenticated call should reach the API without disabling TLS verification:

curl -v \
  --cacert /path/ca.pem \
  --cert /path/client.crt \
  --key /path/client.key \
  https://apiinternal.example.com/api/internal/users/reset-otp

Checkpoints during the handshake:

  • Request CERT on the server side: the server is requesting the client certificate (mTLS active);

  • the server certificate presented matches API_INTERNAL_URL and is validated by your --cacert (no self-signed certificate message);

  • the connection succeeds; an application-level code (for example 401 if the HMAC is missing or invalid) confirms the request reached the API.

Note

A 401 Unauthorized comes from the API (application HMAC check) and not from Traefik: it means the transport layer (routing, mTLS, IP filtering) succeeded and only the application authentication header is missing.

Key Points

  • API_INTERNAL_URL must be distinct from PORTAL_URL and API_DOCKER_NAME. Reusing an existing hostname causes a TLS option conflict on that hostname and the dedicated mTLS would not apply reliably.

  • The dedicated mTLS and the standard internal PKI are independent: the same client certificate is not valid on both. Distribute to internal API callers a certificate signed by API_INTERNAL_SSL_CA.

  • IP filtering, mTLS, and HMAC are cumulative. For an administration endpoint, it is recommended to combine at least dedicated mTLS and HMAC.

  • In infra_manager mode, PORTAL_URL must be an exact hostname for hostname-based isolation to work.