How does a caller present ZAP's control-API key, and where does that key come from?
answer
- two places, one of them a header
- nobody has to invent the value
- a fresh config directory means a fresh one
- -config api.key pins it
basics
~20 sEither as an apikey request parameter or as an x-zap-api-key header, whose case does not matter. If no key is configured, ZAP generates one, saves it under api.key, and reuses it; pin it with -config api.key=value.
solid answer
~40 sThe key travels one of two ways: as a request parameter named `apikey`, or as an HTTP header the program spells `x-zap-api-key`. Header names are matched case-insensitively, so the mixed-case `X-ZAP-API-Key` the generated clients send is the same header — neither spelling is the "real" one. Prefer the header in a pipeline, because a query string leaks into shell history, process lists and logs. The key itself is generated on first use with a cryptographic random generator and persisted under `api.key`, so a runner with a fresh configuration directory invents a fresh key; pin it with the core `-config api.key=…` option instead of reading it back. A second credential also exists — a path-bound nonce — and if one is present the key is not checked at all.
code
bash · 10 lines# pin the key rather than reading back a generated one
zap.sh -daemon -config api.key="$ZAP_API_KEY"
# as a header (preferred: keeps it out of URLs and logs)
curl --proxy "$ZAP_PROXY" -H "x-zap-api-key: $ZAP_API_KEY" \
"http://zap/JSON/core/view/version/"
# as a request parameter (ends up in shell history)
curl --proxy "$ZAP_PROXY" \
"http://zap/JSON/core/view/version/?apikey=$ZAP_API_KEY"go deeper
Recall the two carriers — an apikey request parameter, or an x-zap-api-key header — and that the program invents a key for itself if none is set. Prefer the header so the secret stays out of URLs.
Explain where the value comes from and why a fresh configuration directory produces a fresh one, and how pinning it with -config api.key removes that variability. Be able to say that header-name case is irrelevant and why both spellings exist.
Bring the operational angle: a key in a query string is a key in shell history, in a process list and in whatever the runner records. Know that a nonce short-circuits the key check, which explains a refusal that the key alone cannot.
Decide how credentials for the tool are issued and injected across every runner — pinned from the secret store versus self-generated per container — and make that decision once rather than per pipeline.
## Two places the key can ride A call to ZAP's control API carries its key in one of exactly two places, and the program looks in both: - **as a request parameter named `apikey`** — in the query string on a `GET`, or in the body on a form-encoded `POST`; - **as an HTTP request header** — the constant in the program is spelled `x-zap-api-key`. The header is the better habit for a pipeline, because a query string is the part of a URL that ends up in shell history, in a process list, in a proxy's own traffic log and in whatever the CI system records about the command it ran. The header keeps the secret out of the URL. **The case you type in the header name does not matter.** The program stores and looks up header names after normalising them, so `x-zap-api-key` and `X-ZAP-API-Key` are the same header. This is worth saying plainly because the two spellings genuinely do live in different places — the program's own constant is lower case, and every generated client sends the mixed-case form — and it is easy to conclude from that split that one of them is the "real" one. Neither is; HTTP header names are not case-sensitive and the lookup honours that. ## Where the key comes from You do not have to invent one. The first time the program needs a key and none is configured, it generates a random value with a cryptographic generator, writes it into its configuration under `api.key`, and saves the file. From then on that value is the key for that configuration directory. That has one consequence people meet the hard way: **a container or a runner that starts with a fresh configuration directory generates a fresh key**, so a key you copied out of a previous run will be rejected on the next one. If a pipeline needs to know the key before the program starts, pin it instead of reading it back — the core `-config` option sets any configuration key on the command line, so `-config api.key=…` fixes it for that run. ## The second credential nobody names The key is not the only thing that authenticates a call. The program also accepts a **nonce**, sent either as the `apinonce` parameter or as the `x-zap-api-nonce` header, and it is a different shape of credential: | | the key | the nonce | |---|---|---| | scope | every call | **bound to one API path**; presenting it for a different path fails | | where it comes from | configuration, or generated once and persisted | minted by the program for a specific call it is about to hand out | | reuse | reusable indefinitely | either **one-time** (consumed on first use) or long-lived | | lifetime | until the configuration changes | a one-time nonce expires after a configurable window (`api.noncettlsecs`); a long-lived one does not expire | The checking order matters: **if a nonce is present, the key is never looked at.** The program resolves the nonce, checks that it has not been consumed, that it has not expired, and that the path being called is the path it was minted for — and if any of those fail, the call is refused. There is no fallback to the key. The nonce exists so that the program can hand out a single-use permission to make one specific call without handing over the key that would permit every call. Its natural home is a link or a form the program itself generated. For a pipeline step it is rarely the right tool — a step makes many different calls and would need a nonce for each — but knowing it exists explains why a call can be refused even though the key in your environment is correct: something put a nonce on the request and that is the credential that got checked. ## The POST trap If you move a call from `GET` to `POST` to keep parameters out of the URL, the body must be **form-encoded or multipart**. Those are the two content types the program will parse parameters out of; anything else is refused with `content_type_not_supported` before the call is even looked up. In particular you cannot send a JSON body to this API. The `JSON` in the path is the *response* format, not the request format — a genuinely easy thing to assume backwards when every other API a team works with takes JSON in. ## Putting it together for a run For an unattended run the shape that causes the least trouble is short: - **pin** the key with `-config api.key=…`, from a secret the CI system injects, so the value is known before the program starts rather than read back afterwards; - **send** it as `x-zap-api-key` on every call, in whatever case your client library prefers; - **keep** it out of the query string, so it does not survive in shell history or a build log; - **expect** a nonce only where the program itself generated the link you are following. Then a failing call is a failing call, rather than a question about which key that runner happened to generate.
- The generated clients send `X-ZAP-API-Key` but the program's constant is lower case. Which spelling is required?Neither — they are the same header. Header names are normalised before they are stored and looked up, so case is irrelevant on the wire. The split is only a difference between where each spelling is written down: the program's own constant is lower case, the clients' literal is mixed case.
- What is the `apinonce` credential, and how does it interact with the key?A nonce minted by the program for one specific API path, either one-time or long-lived. It arrives as the `apinonce` parameter or an `x-zap-api-nonce` header. If a nonce is present the key is never checked — the call succeeds or fails on the nonce alone, including a check that the path matches the one it was minted for.
- Why does a POST with a JSON body fail against this API?Parameters are only parsed out of a form-encoded or multipart body; any other content type is refused with `content_type_not_supported`. The `JSON` in the path selects the *response* format, not the request format.
saying these in an interview costs you the question
- Claims only the mixed-case header spelling is accepted
- Says you must invent and configure a key before starting
- Assumes a key copied from a previous container still works
- Sends a JSON request body to an action endpoint
- Believes a nonce falls back to the key when it fails