# Server-to-server (S2S)

In addition to interactive authentication (Login form, OAuth2/OIDC, LDAP), ODD Platform supports **server-to-server (S2S) API-key authentication** (also called **machine-to-machine (M2M) tokens**) for programmatic clients that cannot go through an interactive login — CI/CD jobs, automation scripts, scheduled ingestion pipelines, and any other non-human callers of the Platform API.

## How it works

* A single long-lived token is configured on the platform via `auth.s2s.token`.
* Clients present the token in the `X-API-Key` HTTP header on every request.
* Requests carrying a valid token run with the built-in `ADMIN` user and ADMIN role, so they can call any endpoint that admins can call — including the ingestion API, management APIs, and entity mutations.
* S2S runs **alongside** the configured interactive auth mechanism, not instead of it. If a request has no `X-API-Key` header (or the value doesn't match), the filter falls through and the normal auth chain (Login form / OAuth2 / LDAP) handles the request. This means enabling S2S does not affect the user login flow.

{% hint style="info" %}
S2S is available when `auth.type` is `LOGIN_FORM`, `OAUTH2`, or `LDAP`. With `auth.type: DISABLED`, the platform is already open and S2S is not needed.
{% endhint %}

{% hint style="info" %}
**S2S (`X-API-Key`) is not the same as the per-collector ingestion token.** ODD Platform has two independent, separately-enabled API-auth mechanisms:

* **S2S** — this page. A single platform-wide key sent in the `X-API-Key` header, enabled by `auth.s2s.enabled`, that authenticates the caller as the built-in `ADMIN` for any endpoint.
* **Ingestion-token authentication** — a per-collector / per-datasource token sent in the `Authorization: Bearer <token>` header, enabled by `auth.ingestion.filter.enabled`, that protects the ingestion endpoints only.

They use different headers and different secrets. Sending `X-API-Key` does nothing if only the ingestion-token filter is enabled; sending `Authorization: Bearer` does nothing on the S2S path. See [Ingestion authentication](/configuration-and-deployment/enable-security.md#ingestion-authentication) for the per-collector token mechanism.
{% endhint %}

## Configuration

| Property           | Default   | Description                                                                                                                                                                                                                      |
| ------------------ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `auth.s2s.enabled` | `false`   | Turns the S2S filter on. When `true`, `auth.s2s.token` must be set — the platform refuses to start if the token is missing.                                                                                                      |
| `auth.s2s.token`   | *(unset)* | The shared API key. Any request presenting this value in the `X-API-Key` header is authenticated as the built-in `ADMIN` user. Treat this as a high-privilege secret and store it in a secrets manager, not in plaintext config. |

{% tabs %}
{% tab title="YAML" %}

```yaml
auth:
    type: OAUTH2          # or LOGIN_FORM / LDAP
    s2s:
        enabled: true
        token: {long_random_token}
```

{% endtab %}

{% tab title="Environment variables" %}

```
AUTH_TYPE=OAUTH2
AUTH_S2S_ENABLED=true
AUTH_S2S_TOKEN={long_random_token}
```

{% endtab %}
{% endtabs %}

## Using the token

Send the token in the `X-API-Key` header on every request:

```bash
curl -X POST https://{platform_host}/ingestion/entities \
     -H "X-API-Key: {long_random_token}" \
     -H "Content-Type: application/json" \
     -d @payload.json
```

The same header works for any other Platform API endpoint — for example, listing data sources:

```bash
curl https://{platform_host}/api/datasources \
     -H "X-API-Key: {long_random_token}"
```

## Security considerations

* The token is a **single static string** compared for equality — it is not rotated, expired, or scoped. Rotating it requires restarting the platform with a new `auth.s2s.token` value.
* Any client that holds the token gets full ADMIN access to the Platform API. Prefer to scope its distribution narrowly: one token per trusted caller tier, not one token shared across unrelated systems.
* Transport the token only over HTTPS. If the platform is exposed over plain HTTP, anyone on the network path can capture and replay the token.
* If you only need to authenticate the ingestion pipeline (collectors / push adapters), consider combining S2S with `auth.ingestion.filter.enabled: true` so the ingestion endpoints remain protected even when S2S is not enabled — see [Enable security](/configuration-and-deployment/enable-security.md) and the [deployment matrix](/configuration-and-deployment/enable-security.md#deployment-matrix-per-endpoint-per-auth-config) for which endpoints each flag covers.

## Operator caveats

{% hint style="warning" %}
**Do not name a real `LOGIN_FORM` or `LDAP` user `ADMIN` (case-sensitive, uppercase).** The S2S filter builds its synthetic principal as the literal uppercase string `ADMIN`. The platform's user-to-owner lookup compares this against the `USER_OWNER_MAPPING` table with case-sensitive equality. If an operator has provisioned a real user named `ADMIN` (uppercase) in `LOGIN_FORM` or `LDAP`, S2S-authenticated callers will resolve to that real user's owner binding — every S2S mutation will be attributed to the real user in the activity feed, and any owner the real user is bound to becomes the S2S caller's owner. Avoid the literal uppercase `ADMIN` (and similarly `admin` lowercase — see the [disabled-authentication](/configuration-and-deployment/enable-security/authentication/disabled-authentication.md#reserved-usernames) page) when provisioning users in any auth mode.
{% endhint %}

{% hint style="info" %}
**`auth.s2s.enabled: true` has no effect under `auth.type: DISABLED`.** The DISABLED-mode security configuration does not read `auth.s2s.enabled`, so the S2S filter is never wired into the chain in that mode. The property is accepted in YAML without warning, but `X-API-Key` requests under DISABLED behave identically to unauthenticated requests (which DISABLED already permits). If you are pre-configuring `auth.s2s.enabled` for a planned migration to `LOGIN_FORM`, `OAUTH2`, or `LDAP`, the property takes effect only after `auth.type` flips to one of those modes.
{% endhint %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.opendatadiscovery.org/configuration-and-deployment/enable-security/authentication/s2s.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
