From 40f0e69c34b7d34d5af0b02f8438f0cfc6ec0bd1 Mon Sep 17 00:00:00 2001 From: Dawson Toth Date: Wed, 29 Jul 2026 14:37:49 -0400 Subject: [PATCH] docs(deploy): by-reference deploys, sealed credentials, CI token auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds on the two-phase deploy docs (#599) to cover the rest of the deploy-by-reference workflow. reference/components/applications.md - "Deploying by Reference" — `harper deploy by_ref=true`, `ref=`, `credential=`. Explains why a reference pins to a resolved SHA rather than the tag or branch name typed: peers resolve the package independently, so a tag that moves mid-deploy could otherwise leave nodes running different code. Notes that the cluster clones the pushed remote, and that by-reference means the cluster builds from source — so an app whose build can't run on the node should stay on payload deploys. - "Provisioning a Deploy Credential" — `harper deploy setup=true`, the client-side sealing flow, and that reverting to the previous version needs no credential at all. reference/cli/authentication.md - Documents HARPER_CLI_REFRESH_TOKEN / HARPER_CLI_OPERATION_TOKEN and adds "Token credentials for CI/CD", so the CI guidance is no longer "put an admin password in your pipeline". Covers precedence, in-memory-only refresh, and token lifetimes (1d / 30d defaults). - Warns that a user holds only ONE valid refresh token at a time: issuing a new one invalidates the last, so a routine local `harper login` silently breaks a pipeline using the same account. Hence the recommendation to give CI its own user. (Verified against security/tokenAuthentication.ts, which stores a single hashed refresh_token per user record.) reference/cli/commands.md - `harper login --for-ci`, including the stdout/stderr split that makes `| gh secret set --env-file -` work without displaying the token. reference/security/secrets.md - Points the existing private-source deploy credentials section at the CLI flow that automates it. Documents HarperFast/harper#1850, #1851, and #1876. Verified every anchor link in the changed files resolves (16 checked). Co-Authored-By: Claude Opus 4.8 --- reference/cli/authentication.md | 49 +++++++++++++++++++ reference/cli/commands.md | 15 ++++++ reference/components/applications.md | 73 ++++++++++++++++++++++++++++ reference/security/secrets.md | 2 + 4 files changed, 139 insertions(+) diff --git a/reference/cli/authentication.md b/reference/cli/authentication.md index c2d9487ac..f24f54bc4 100644 --- a/reference/cli/authentication.md +++ b/reference/cli/authentication.md @@ -71,6 +71,10 @@ The CLI supports loading environment variables from your shell environment (or o - `HARPER_CLI_TARGET` (or `CLI_TARGET`) - Sets the default `target` for CLI commands. - `HARPER_CLI_USERNAME` (or `CLI_TARGET_USERNAME`) - Harper admin username for the target. - `HARPER_CLI_PASSWORD` (or `CLI_TARGET_PASSWORD`) - Harper admin password for the target. +- `HARPER_CLI_REFRESH_TOKEN` (or `CLI_TARGET_REFRESH_TOKEN`) - Long-lived token the CLI exchanges for an operation token on each run. Available since v5.2.0. +- `HARPER_CLI_OPERATION_TOKEN` (or `CLI_TARGET_OPERATION_TOKEN`) - Short-lived operation token, if you would rather supply one directly. Available since v5.2.0. + +**Prefer tokens over a password in CI.** A refresh token is scoped to authentication, can be revoked without changing the account password, and — unlike `HARPER_CLI_PASSWORD` — cannot be reused to log in interactively. See [Token credentials for CI/CD](#token-credentials-for-cicd) below. **Example `.env` file**: @@ -129,6 +133,51 @@ harper deploy target=https://prod-server.com:9925 replicated=true harper restart target=https://prod-server.com:9925 replicated=true ``` +##### Token credentials for CI/CD + +Available since: v5.2.0 + +Rather than storing an admin password in your CI provider, log in once locally and hand CI a **refresh token**. The CLI mints a fresh, short-lived operation token from it on every run, so the only durable secret CI holds is a revocable token. + +`harper login --for-ci` writes the two variables to **stdout** in `.env` format — and nothing else, so it pipes cleanly. Everything a human reads (the banner, prompts, status) goes to stderr: + +```bash +# Set both GitHub Actions secrets in one command — the token is never displayed +harper login --for-ci | gh secret set --env-file - + +# Or copy them to the clipboard to paste in by hand +harper login --for-ci | pbcopy +``` + +The block it emits: + +```bash +HARPER_CLI_TARGET=https://example.com:9925/ +HARPER_CLI_REFRESH_TOKEN=eyJhbGciOi... +``` + +Because stdout carries only these lines, the token never appears on screen or in your shell history — which is not true of copying it out of terminal output by hand. + +Expose the two values to the deploy step and no other credentials are needed: + +```yaml +- name: Deploy + run: harper deploy project=my-app restart=true replicated=true + env: + HARPER_CLI_TARGET: ${{ secrets.HARPER_CLI_TARGET }} + HARPER_CLI_REFRESH_TOKEN: ${{ secrets.HARPER_CLI_REFRESH_TOKEN }} +``` + +**Precedence**: an explicitly supplied username (arguments or `HARPER_CLI_USERNAME`/`HARPER_CLI_PASSWORD`) wins; otherwise env-var tokens are used; otherwise the token saved by `harper login`. Env-var tokens deliberately outrank the saved credentials file so a runner that has both behaves predictably. A token refreshed from an env var is held in memory for that invocation only — nothing is written to `~/.harperdb/credentials.json`. + +**Lifetimes**: operation tokens expire after `authentication.operationTokenTimeout` (default `1d`) and refresh tokens after `authentication.refreshTokenTimeout` (default `30d`). CI needs re-provisioning when the refresh token expires. + +:::warning +**Each user has only one valid refresh token at a time.** Logging in again as that user issues a new one and invalidates the previous one, so a routine local `harper login` will break a pipeline holding an older token for the same account. + +Create a **dedicated CI user** and run `harper login --for-ci` as that user. This also scopes the pipeline's permissions and lets you revoke its access — by logging in again as that user, or by changing its password — without disturbing anyone else. +::: + #### Method 3: Command Parameters Provide credentials directly as command parameters: diff --git a/reference/cli/commands.md b/reference/cli/commands.md index 24cfbc57b..d216a125a 100644 --- a/reference/cli/commands.md +++ b/reference/cli/commands.md @@ -155,6 +155,7 @@ harper login **Optional Parameters**: - `` - The URL of the Harper instance to log in to. +- `--for-ci` - Print CI/CD credentials to stdout after logging in. Available since v5.2.0. **Prompts**: @@ -164,6 +165,20 @@ You'll be asked to type in the following information: - `` - Harper admin username. - `` - Harper admin password. +**`--for-ci`**: + +Prints `HARPER_CLI_TARGET` and `HARPER_CLI_REFRESH_TOKEN` to **stdout** in `.env` format — and nothing else, so the output pipes directly into a secret store without the token being displayed. Everything else (banner, prompts, status) goes to stderr: + +```bash +# Set both GitHub Actions secrets in one command +harper login --for-ci | gh secret set --env-file - + +# Or copy them to paste in by hand +harper login --for-ci | pbcopy +``` + +See [Token credentials for CI/CD](authentication.md#token-credentials-for-cicd) for how the CLI consumes these variables, and for why a pipeline should use a dedicated user. + ### `harper logout` Available since: v5.0.0 diff --git a/reference/components/applications.md b/reference/components/applications.md index dfc70cd33..95882a494 100644 --- a/reference/components/applications.md +++ b/reference/components/applications.md @@ -130,6 +130,79 @@ Harper generates a `package.json` from component configurations and uses a form For SSH-based private repos, use the [Add SSH Key](#add_ssh_key) operation to register keys first. +### Deploying by Reference + +Available since: v5.2.0 + +Omitting `package` uploads a snapshot of your working directory. The result is an anonymous artifact: nothing records _which_ commit it came from, so reproducing it later — or stepping back to a previous release — means finding those exact files again. + +Deploying by **reference** sends a pinned git reference instead, and the cluster fetches that exact commit. Redeploying the same reference is an exact redeploy, and rolling back is deploying an older one. + +`harper deploy by_ref=true` builds that reference from the local git repository, so you don't assemble the URL yourself: + +```sh +harper deploy by_ref=true restart=true replicated=true +``` + +This resolves the repository's `origin` remote and the current commit, then deploys `package=git+https://github.com//.git#`. + +**Parameters**: + +- `by_ref` - Build the package reference from the local repository. +- `ref` _(optional)_ - Deploy a specific commit, tag, or branch instead of `HEAD`. Implies `by_ref`. +- `credential` _(optional)_ - Git host whose stored credential authenticates the clone, e.g. `github.com`. Omit for public repositories. + +```sh +# Deploy a specific tag +harper deploy ref=v1.2.0 restart=true replicated=true + +# Roll back by deploying an older commit +harper deploy ref=9f8c2a1 restart=true replicated=true +``` + +**A reference is pinned to a SHA, not to the name you typed.** Tags and branches are resolved locally and the full commit SHA is what ships. This matters on a cluster: peers resolve the package independently, so a tag that moves mid-deploy — or a branch that advances — could otherwise leave nodes running different code. + +**Commit and push first.** The cluster clones from the remote, so it only sees commits that have been pushed. `by_ref` warns when the working tree is dirty, since uncommitted changes won't be part of the deploy. + +#### Private repositories + +Pass `credential=` for a private repository. The CLI attaches a `credentials` reference naming a secret that the cluster resolves in memory at clone time, so no token travels in the operation body or lands on disk: + +```sh +harper deploy by_ref=true credential=github.com restart=true replicated=true +``` + +Provision that credential once with [`harper deploy setup=true`](#provisioning-a-deploy-credential). See [Private-source deploy credentials](../security/secrets.md#private-source-deploy-credentials) for how the secret is named and resolved, and [`add_ssh_key`](#add_ssh_key) for the SSH-key alternative. + +:::note +Deploying by reference means the **cluster** installs and builds the component from source. If your application needs a build step that can't run on the node, keep shipping the built output as a payload deploy instead. +::: + +### Provisioning a Deploy Credential + +Available since: v5.2.0 + +`harper deploy setup=true` provisions the credential a private deploy needs. It's interactive, and runs once per component and source: + +```sh +harper deploy setup=true +``` + +It asks which private source needs a credential (a GitHub repository or an npm registry) and sources a token — from your `gh` CLI session, or one you paste — then: + +1. Fetches the cluster's public key with `get_secrets_public_key`. +2. **Encrypts the token locally** into an `enc:v1:` envelope. +3. Stores only the ciphertext via `set_secret`, granted to the component. +4. Prints the `credentials` reference for the deploy to use. + +The plaintext never leaves your machine: the operations API, its logs, and replication only ever carry the envelope, and the cluster decrypts it in memory at deploy time. This requires a cluster with secrets custody (Harper Pro / Fabric) — see [Client-side encryption](../security/secrets.md#client-side-encryption-encrypt-before-it-leaves-the-client). + +Because the stored token is durable, later deploys — including re-fetching an older reference — reuse it without re-entering anything. + +:::note +Rolling back to the **immediately previous** version needs no credential at all: [`revert_component`](../operations-api/operations.md#revert_component) swaps in the retained previous build without re-fetching from the source. +::: + ## Dependency Management Harper uses `npm` and `package.json` for dependency management. diff --git a/reference/security/secrets.md b/reference/security/secrets.md index 2e2a0a842..9b0ac045d 100644 --- a/reference/security/secrets.md +++ b/reference/security/secrets.md @@ -289,6 +289,8 @@ function encryptSecret(plaintext, publicKeyPem, kid) { `deploy_component` accepts a `credentials` array so a component installed from a private **npm registry** or private **git repository** can authenticate. A provided token is ingested into the secrets store (as a reference, encrypted) rather than travelling in the operation body, persisting as a plaintext `.npmrc`, or being written to disk for git — so package-reference deploys survive rollback, reboot, and new peers joining. Ingested tokens are stored under a derived name (`deploy..` or `deploy..git.`) granted to the component. See [`deploy_component`](../operations-api/operations.md#deploy_component). +To provision one of these without handing the cluster a plaintext token at all, `harper deploy setup=true` (v5.2.0+) runs the [client-side encryption](#client-side-encryption-encrypt-before-it-leaves-the-client) flow above for you: it fetches the public key, seals the token locally into an `enc:v1:` envelope, stores only the ciphertext under that same derived name, and prints the `credentials` reference for the deploy to use. See [Provisioning a Deploy Credential](../components/applications.md#provisioning-a-deploy-credential). + ## Threat model **Protects against:** theft of on-disk config/`.env` files, the editor/operations read surface, secrets appearing in operations logs and replication payloads, and an operator observing traffic at the TLS-terminating layer. Client-side encryption additionally keeps plaintext off the operations API entirely.