Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Authentication

A deployment is unprotected by default: anything that can reach chap-core’s port can use its API. Turning authentication on is one flag at init time, or one command afterwards.

chap-devops init mychap --models chapkit_simple_multistep_model --api-token   # at creation
chap-devops auth enable                                # on a deployment that already exists

What the token protects

CHAP_API_TOKEN gates the whole API. Clients send it as an HTTP header:

curl -H "Authorization: Bearer $TOKEN" http://localhost:8700/v2/services

Three paths stay open, because they are not credentials-bearing calls: /health and /health/ready, which the container healthcheck calls with no headers at all, and /system/info, which is how a client discovers that a token is required in the first place. Everything else needs the token, /v2/services and /docs included.

The token is the one thing a client has to carry. For DHIS2 that is the chap route rather than the Modeling App: the app calls DHIS2, DHIS2 proxies the call to chap-core, and chap-devops dhis2 connect writes the token into the route’s auth as an Authorization: Bearer header, which DHIS2 stores and never lists back. Anything else calling the API needs it from chap-devops auth show --reveal.

How the models authenticate

SERVICEKIT_REGISTRATION_KEY is a second, independent secret. It is not an API token: chap-core accepts it only under /v2/services, which is where a chapkit service registers itself and sends its keepalive pings. A model presents it as X-Service-Key, because servicekit can send no other header.

chap-devops writes both secrets together, and this is why: once the API is protected, an unauthenticated registration is rejected like any other request, so a model service that sends nothing never appears in chap-devops status. The registration key is what it sends instead.

Nothing has to be copied for that to work. Every model overlay carries

    environment:
      SERVICEKIT_ORCHESTRATOR_URL: http://chap:8000/v2/services/$$register
      SERVICEKIT_REGISTRATION_KEY: ${SERVICEKIT_REGISTRATION_KEY:-}

and Compose substitutes the value from the .env beside the compose files, the same file chap-core reads its own copy from. The two ends cannot disagree. chap-devops sync writes that line when .chap-devops/project.yaml says the deployment has a key, and comments it out again when it has none.

chap-core needs the same value, and upstream’s compose.ghcr.yml passes only CHAP_API_TOKEN into the chap service - so the key never reached the container and the API answered every registration with

{"detail": "Missing or invalid API token"}

compose.chap-devops.yml is where chap-devops puts that right:

services:
  chap:
    environment:
      SERVICEKIT_REGISTRATION_KEY: ${SERVICEKIT_REGISTRATION_KEY:-}

Compose merges environment maps across -f files additively, so this adds the one variable and leaves upstream’s file untouched. It is rendered whether or not the deployment has a key: with none in .env the variable arrives empty, which chap-core reads as no key at all.

Where the secrets live

In .env, and nowhere else:

CHAP_API_TOKEN=d1f0...
SERVICEKIT_REGISTRATION_KEY=9a3c...

.chap-devops/project.yaml records two booleans and no values:

auth:
  api_token: true
  registration_key: true

That split is deliberate. .chap-devops/ is intent, it is small, and it is the kind of thing that gets committed, archived and pasted into a bug report; .env is the file Compose reads and the one to keep out of version control.

A generated secret is 64 lowercase hex characters, the same thing openssl rand -hex 32 produces. An explicit --api-token VALUE (or chap-devops auth enable --token VALUE) is used verbatim; below 32 characters chap-devops warns, and so does chap-core at startup, because its API has no rate limiting and a short token is guessable by anyone who can reach the port.

Verbatim includes $: a token with characters Compose would read as anything but themselves is written in single quotes (CHAP_API_TOKEN='$abc def'), which Compose reads literally. Written bare, $abc would be expanded, to an empty string when no such variable is set, and an empty CHAP_API_TOKEN is authentication off. A quote, a backslash or a control character cannot be written that way, so a token containing one is refused.

chap-devops reads .env by Compose’s rules as well: CHAP_API_PORT=8701 # mine is port 8701 to both, because a # after a space starts a comment. A # without a space before it (val#ue) is part of the value.

Turning it on and off

CommandWhat it does
chap-devops auth show [--reveal]Whether each secret is set, and the token only with --reveal; plus whether each OCS data source is set, never its value.
chap-devops auth enable [--token VALUE]Write both secrets, record them, re-render the overlays.
chap-devops auth disableComment both lines out, keeping their values, and re-render.
chap-devops auth rotateReplace both secrets with new ones.
chap-devops auth tokenPrint the token alone on stdout, for a script; exit 1 when authentication is off.

chap-devops auth show reads both ends: .chap-devops/project.yaml for what the deployment intends and .env for what is actually set, and says so when the two disagree - a token recorded as in use but commented out in .env, say. The token itself is printed only when --reveal asks for it.

On a deployment with the ocs component it also lists the OCS data source credentials, which live in the same .env:

OCS data sources
  ECMWF_DATASTORES_URL  set
  ECMWF_DATASTORES_KEY  set
  EDH_API_KEY           unset
  CDSE_S3_ACCESS_KEY    unset
  CDSE_S3_SECRET_KEY    unset

Those five are read and never written: they are accounts with Copernicus and Earth Data Hub rather than this deployment’s own secrets, so enable, disable and rotate do not touch them, and their values stay hidden even under --reveal - there is nothing to paste into a client, only the question of whether a dataset will ingest. --json carries them under ocs_data_sources. See Components.

enable, disable and rotate only ever write those two .env lines. The database password, the image pins, the comments and the blank lines come out byte for byte as they went in, which is what makes them safe on a file an operator has edited.

They read and write the file by Compose’s rules, which is what makes the answer they report the answer Compose will act on: the last active assignment of a variable is the live one, export KEY= and quoted values are read as Compose reads them, and a write rewrites the variable’s first active assignment and removes every later duplicate of it. So a .env that somehow carries two CHAP_API_TOKEN= lines comes out of chap-devops auth rotate with one, holding the new token - rather than with the old, lower line still deciding what chap-core enforces. See the .env contract.

chap-devops auth enable
API authentication is on
run `chap-devops up` to restart chap-core and the models with authentication

With -v, the hints say where the secrets went and which files changed:

$ chap-devops -v auth enable
API authentication is on
hint: wrote the API token to `.env`; `chap-devops auth show --reveal` prints it
hint: wrote the registration key to `.env`; every model overlay now sends it
hint: wrote compose.auto-arima-chapkit.yml
hint: wrote compose.chapkit-simple-multistep-model.yml
run `chap-devops up` to restart chap-core and the models with authentication
hint: the DHIS2 `chap` route carries it once `chap-devops dhis2 connect` has run; any other client needs it from `chap-devops auth show --reveal`

chap-devops up is not optional. chap-core and the model containers read .env when Compose creates them, so nothing changes for a container that is already running until it is recreated.

chap-devops auth disable comments the lines out rather than deleting them:

# CHAP_API_TOKEN=d1f0...

so the token your clients are configured with is still recoverable. chap-devops auth enable afterwards picks those values back up rather than generating new ones, which makes an accidental disable a round trip.

Rotation

chap-devops auth rotate
chap-devops up

Rotating is two steps, and the second one matters: until chap-devops up recreates the containers, chap-core is still enforcing the old token, and after it every client that has not been updated gets a 401. Update them in the same sitting: chap-devops dhis2 connect rewrites the DHIS2 route with the new token, and anything else calling the API needs it by hand.

The registration key rotates with the token, and the models pick it up from the same chap-devops up, so there is nothing to do for them.

Reading it back

chap-devops status
chap-core   up   http://localhost:8700   v2.3.1   auth: on

For a script rather than a screen, chap-devops auth token prints the token and nothing else:

TOKEN=$(chap-devops auth token)
curl -H "Authorization: Bearer $TOKEN" http://localhost:8700/v2/services

On a deployment with authentication off it prints nothing on stdout, says so on stderr and exits 1, so a caller that captured an empty string stops rather than sending an empty header. chap-devops api needs none of this - it reads .env itself - and chap-devops auth show --reveal is the spelling for reading the value rather than capturing it. See Jobs and the API.

chap-devops status reads the token out of .env and sends it on every request, so it keeps working on a protected deployment, and the last cell of the chap-core line says which mode the deployment is in. A 401 is reported as a token problem rather than as “not chap-core”: see Troubleshooting.

--fresh-env regenerates it

init --fresh-env renders a whole new .env, which means a new database password and, if --api-token is passed with it, a new token and registration key. Without --api-token the new file has both secrets commented out, so the deployment comes back unprotected. init on a directory that already has a .env keeps that file, --force included, so --api-token there does nothing but warn and point at chap-devops auth enable. See the .env contract.