Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ Prefer plain ASCII characters in Markdown unless a typographic character is genu

- Tag minor-version availability inline: `<VersionBadge version="vX.Y.0" />` for new surface, `<VersionBadge type="changed" version="vX.Y.0" />` for behavior changes to existing surface.
- Derive the version from the core release the change ships in, stripping prerelease suffixes (`5.1.0-beta.1` → `v5.1.0`).
- **Determine that release from the core repo's git tags, not from the feature branch's `package.json`.** A branch reading `5.2.0-beta.3` says which release was open when the branch started, not which one the change lands in — if a release is cut before the feature merges, the badge is silently wrong. Check `git tag --sort=-creatordate` for the newest release, and `git tag --contains <merge-commit>` for whether the change is in one; a merged-but-untagged feature ships in the _next_ version, which may be a minor bump. Re-check on every refresh pass of a long-lived docs PR, because a release cut between passes invalidates a badge that was correct when written.
- **Fetch tags and confirm the tag object before trusting the answer.** `git tag --contains` is only as good as the local tag it compares against: a stale or divergent tag of the same name gives a confidently wrong answer in either direction. Run `git fetch --tags`, then check `git rev-parse <tag>` against `git ls-remote --tags origin <tag>`. When a badge is disputed, the decisive check is not `--contains` at all but whether the feature's files exist at the tag — `git ls-tree -r --name-only <tag> | grep <path>`, or `git show <tag>:<file>` — since that cannot be confounded by ancestry or by which tag your clone happens to hold.
- Each minor release gets a file under `release-notes/<major-codename>/` (e.g. `release-notes/v5-lincoln/5.1.md`); the sidebar picks it up automatically.
- Absolute links from `release-notes/` (or `learn/`) into current reference docs use the versioned path `/reference/v5/...` — the reference plugin maps the current version to the `v5` URL path.
- When documenting a change from a core/pro PR, cross-link the feature PR and the docs PR in both descriptions.
Expand Down
46 changes: 44 additions & 2 deletions reference/cli/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ For remote Operations API commands, the CLI uses the first complete authenticati
4. Legacy `CLI_TARGET_USERNAME` and `CLI_TARGET_PASSWORD` environment variables
5. `HARPER_CLI_OPERATION_TOKEN` and `HARPER_CLI_REFRESH_TOKEN` environment variables, or their legacy `CLI_TARGET_` equivalents — see [Token credentials for CI/CD](#token-credentials-for-cicd)
6. A token saved by `harper login`
7. `username=` and `password=` operation parameters (legacy fallback)
7. A [workload identity token](#workload-identity-oidc) exchanged with the runtime's OIDC provider (v5.3.0)
Comment thread
dawsontoth marked this conversation as resolved.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we also update the recommendation table immediately below this list? It still tells every CI/CD pipeline to use HARPER_CLI_REFRESH_TOKEN, which conflicts with this new no-stored-credential happy path and steers supported runners toward a secret they need to rotate. Consider recommending workload identity when available, with a refresh token as the fallback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — fixed in a7c885ac. The row now reads "Workload identity where the runtime supports it; otherwise HARPER_CLI_REFRESH_TOKEN".

Worth saying how it got that way, since it is a process failure rather than a judgment one. I had made exactly this edit on the earlier branch. When the OIDC content was extracted onto this one I re-applied the precedence entry and the Workload identity section but not the table row, so the page ended up recommending a rotatable secret three paragraphs above telling readers they need no stored credential. Re-applying a section by hand loses the edits that were scattered outside it.

🤖 Addressed by Claude Code

8. `username=` and `password=` operation parameters (legacy fallback)

:::tip
**Configure one credential style per context, not two.** Precedence exists to resolve a conflict, but it resolves it silently, and the ways this page describes for authentication to go wrong all need two styles live at once: a payload `username=`/`password=` pair takes over when a token stops resolving, a blank token variable hands the run to whatever saved login the machine has. Pick one and leave the others unset:
Expand All @@ -62,7 +63,7 @@ Credentials are resolved as a pair and are never combined across sources. An inc

Entry 5 is not a two-step fallback the way 3 and 4 are: whichever token namespace is merely **set** claims the choice, so a blank `HARPER_CLI_REFRESH_TOKEN` shadows a complete `CLI_TARGET_REFRESH_TOKEN` instead of deferring to it — see below.

Entries 5 and 6 authenticate with a bearer token and apply to **remote targets only**. A local operation goes over the domain socket, which the server already trusts — as, by default, it trusts any loopback address, since `authentication.authorizeLocal` defaults to `true`. Token environment variables are deliberately ignored on the socket path — a token minted for one instance would otherwise be attached to every local `harper` command in that shell and rejected. Note what that trust means on a self-hosted runner: an unset or blank `target` does not fail. The CLI falls back to the target saved by a previous `harper login` on that machine, and only if there is none does it go local — where it runs as superuser on the socket's ambient trust, with no credential checked at all. So a job that loses its `HARPER_CLI_TARGET` either deploys to whatever remote that runner last logged into, or to the runner's own node. Neither is an error, and the first is the wider blast radius.
Entries 5 through 7 authenticate with a bearer token and apply to **remote targets only**. A local operation goes over the domain socket, which the server already trusts — as, by default, it trusts any loopback address, since `authentication.authorizeLocal` defaults to `true`. Token environment variables are deliberately ignored on the socket path — a token minted for one instance would otherwise be attached to every local `harper` command in that shell and rejected. Note what that trust means on a self-hosted runner: an unset or blank `target` does not fail. The CLI falls back to the target saved by a previous `harper login` on that machine, and only if there is none does it go local — where it runs as superuser on the socket's ambient trust, with no credential checked at all. So a job that loses its `HARPER_CLI_TARGET` either deploys to whatever remote that runner last logged into, or to the runner's own node. Neither is an error, and the first is the wider blast radius.

Before v5.2.0, `username=` and `password=` operation parameters took precedence over environment variables and saved login tokens. This could authenticate an operation as the wrong user when those fields were part of the operation payload, such as the user being created by `add_user`.

Expand Down Expand Up @@ -255,6 +256,47 @@ Create a **dedicated CI user** and run `harper login --for-ci` as that user. Tha
There is no operation that revokes a refresh token directly. To invalidate one, either run `harper login --for-ci` again as that user — minting overwrites the stored hash, so the previous token stops working — or deactivate the user with `alter_user`. `harper logout` is not a revocation: it deletes your local copy and leaves the server-side hash valid.
:::

##### Workload identity (OIDC)

<VersionBadge version="v5.3.0" />
Comment thread
dawsontoth marked this conversation as resolved.

On a runner that can prove its own identity, the CLI needs **no stored credential at all**. It asks the runtime for an identity token addressed to your instance and trades it for a one-hour operation token. Nothing durable is stored in your CI provider, and there is no 30-day token to rotate.

Configure the instance to trust the workflow once with [`add_oidc_trust`](../operations-api/operations.md#add_oidc_trust), then grant the token permission in the workflow.

The block below is **GitHub Actions** syntax — `permissions: id-token: write` is how Actions specifically opts a job into requesting an identity token. Other CI systems expose the same idea differently, and Harper currently detects GitHub Actions only; an unrecognized runtime is not an error, the CLI simply falls through to its other credential sources.

```yaml
# .github/workflows/deploy.yml
permissions:
id-token: write
contents: read
environment: production
steps:
- run: harper deploy by_ref=true restart=true replicated=true
env:
HARPER_CLI_TARGET: ${{ vars.HARPER_CLI_TARGET }} # a var, not a secret
```
Comment thread
dawsontoth marked this conversation as resolved.

`HARPER_CLI_TARGET` is the only variable the step needs, and it is not sensitive — hence `vars` rather than `secrets`.

**With a trust policy in place you need nothing else** — no `HARPER_CLI_REFRESH_TOKEN`, no password, no credentials on the command. That is the point of it: the workflow holds no Harper secret at all.

If your pipeline already sets a token or password, remove it. Leaving it in place is not harmful but it does keep winning — a configured credential outranks the exchange, deliberately, so that enabling this does not silently re-point an existing pipeline at a different user. See [Authentication Precedence](#authentication-precedence) if you need the full order for a mixed setup.

The exchange is attempted only when the runtime actually offers an identity. GitHub Actions sets `ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN` together on a job that declares `id-token: write`; without both, the CLI falls through to its other credential sources rather than reporting a failure. An unrecognized runtime falls through the same way.

On success the CLI prints, to stderr, which policy authenticated it and as whom:

```
Requesting a GitHub Actions identity token for https://my-instance.harperdb.io:9925/...
Authenticated as 'ci-deploy' via OIDC trust policy 'my-app-prod'.
```

If Harper rejects the token, the CLI reports it and carries on rather than aborting. On a workflow configured as above there is nothing left to try, so the operation itself fails with a 401 — but do not treat a rejected exchange as a guaranteed hard stop: if anything else on the runner can authenticate, the command proceeds under that identity instead. [Authentication Precedence](#authentication-precedence) covers those cases.

The server deliberately does not report which check failed — see [`exchange_oidc_token`](../operations-api/operations.md#exchange_oidc_token) — so diagnose with `list_oidc_trust` and the instance's `oidc-trust` log.

#### Method 3: Dedicated Authentication Parameters

<VersionBadge version="v5.2.0" />
Expand Down
26 changes: 13 additions & 13 deletions reference/cli/operations-api-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,24 +54,24 @@ This is just a brief overview of all operations available as CLI commands. Revie
| `sql` | Execute SQL queries | [Data](../operations-api/operations.md#nosql-operations) | v4.3.0 |
| `csv_file_load` | Load data from CSV file | [Data](../operations-api/operations.md#nosql-operations) | v4.3.0 |
| `csv_url_load` | Load data from CSV URL | [Data](../operations-api/operations.md#nosql-operations) | v4.3.0 |
| `list_users` | List all users | [Security](../operations-api/operations.md#certificate-management) | v4.3.0 |
| `add_user` | Create a new user | [Security](../operations-api/operations.md#certificate-management) | v4.3.0 |
| `alter_user` | Modify user properties | [Security](../operations-api/operations.md#certificate-management) | v4.3.0 |
| `drop_user` | Delete a user | [Security](../operations-api/operations.md#certificate-management) | v4.3.0 |
| `list_roles` | List all roles | [Security](../operations-api/operations.md#certificate-management) | v4.3.0 |
| `drop_role` | Delete a role | [Security](../operations-api/operations.md#certificate-management) | v4.3.0 |
| `list_users` | List all users | [Users & Roles](../operations-api/operations.md#users--roles) | v4.3.0 |
| `add_user` | Create a new user | [Users & Roles](../operations-api/operations.md#users--roles) | v4.3.0 |
| `alter_user` | Modify user properties | [Users & Roles](../operations-api/operations.md#users--roles) | v4.3.0 |
| `drop_user` | Delete a user | [Users & Roles](../operations-api/operations.md#users--roles) | v4.3.0 |
| `list_roles` | List all roles | [Users & Roles](../operations-api/operations.md#users--roles) | v4.3.0 |
| `drop_role` | Delete a role | [Users & Roles](../operations-api/operations.md#users--roles) | v4.3.0 |
| `create_csr` | Create certificate signing request | [Security](../operations-api/operations.md#certificate-management) | v4.4.0 |
| `sign_certificate` | Sign a certificate | [Security](../operations-api/operations.md#certificate-management) | v4.4.0 |
| `list_certificates` | List SSL/TLS certificates | [Security](../operations-api/operations.md#certificate-management) | v4.4.0 |
| `add_certificate` | Add SSL/TLS certificate | [Security](../operations-api/operations.md#certificate-management) | v4.4.0 |
| `remove_certificate` | Remove SSL/TLS certificate | [Security](../operations-api/operations.md#certificate-management) | v4.4.0 |
| `add_ssh_key` | Add SSH key | [Security](../operations-api/operations.md#certificate-management) | v4.4.0 |
| `get_ssh_key` | Get SSH key | [Security](../operations-api/operations.md#certificate-management) | v4.7.2 |
| `update_ssh_key` | Update SSH key | [Security](../operations-api/operations.md#certificate-management) | v4.4.0 |
| `delete_ssh_key` | Delete SSH key | [Security](../operations-api/operations.md#certificate-management) | v4.4.0 |
| `list_ssh_keys` | List all SSH keys | [Security](../operations-api/operations.md#certificate-management) | v4.4.0 |
| `set_ssh_known_hosts` | Set SSH known hosts | [Security](../operations-api/operations.md#certificate-management) | v4.4.0 |
| `get_ssh_known_hosts` | Get SSH known hosts | [Security](../operations-api/operations.md#certificate-management) | v4.4.0 |
| `add_ssh_key` | Add SSH key | [Components](../operations-api/operations.md#components) | v4.4.0 |
| `get_ssh_key` | Get SSH key | [Components](../operations-api/operations.md#components) | v4.7.2 |
| `update_ssh_key` | Update SSH key | [Components](../operations-api/operations.md#components) | v4.4.0 |
| `delete_ssh_key` | Delete SSH key | [Components](../operations-api/operations.md#components) | v4.4.0 |
| `list_ssh_keys` | List all SSH keys | [Components](../operations-api/operations.md#components) | v4.4.0 |
| `set_ssh_known_hosts` | Set SSH known hosts | [Components](../operations-api/operations.md#components) | v4.4.0 |
| `get_ssh_known_hosts` | Get SSH known hosts | [Components](../operations-api/operations.md#components) | v4.4.0 |
| `cluster_get_routes` | Get cluster routing information | [Clustering](../operations-api/operations.md#replication--clustering) | v4.3.0 |
| `cluster_network` | Get cluster network status | [Clustering](../operations-api/operations.md#replication--clustering) | v4.3.0 |
| `cluster_status` | Get cluster status | [Clustering](../operations-api/operations.md#replication--clustering) | v4.3.0 |
Expand Down
Loading
Loading