5.3 Release Notes
Patch Releases
All patch release notes for 5.3.x are available on the releases page.
Node Identity
node.hostname and replication.hostname must now be a bare hostname or IP literal. This value is the node's identity — it becomes the node's TLS certificate common name and the host that replication advertises to peers and dials to reach the node — so a URL or host:port value corrupted certificate matching and replication without surfacing an error: a node configured as http://host:9926 advertised and dialed a host literally named http (harper#2218).
Configuration validation now rejects these values at startup, so an existing install whose node.hostname or replication.hostname is a URL, host:port, or a non-string value will fail to start until it is corrected to a bare host. The startup error names the offending value and the reason it was rejected. A bare hostname (server-one), an IPv4 literal (10.0.0.5), and an unbracketed IPv6 literal (::1) are valid; a scheme, port, path, credentials, query string, fragment, bracketed IPv6 literal ([::1]), or non-string value is rejected. Route entries under replication.routes are unaffected and may still be URLs.
When node.hostname is unset, Harper resolves the identity to the first valid bare host among replication.hostname, the host in replication.url, the TLS certificate common name, and the Operations API host, falling back to 127.0.0.1. A derived source that is empty or unusable is skipped rather than failing startup.
Replication URLs now also bracket a bare IPv6 literal correctly (::1 becomes ws://[::1]:9933), which the URL parser previously rejected.
Component Deploys
Staged Build Retention
deploy_component builds each release under <componentsRoot>/.deploy-staging/<deploymentId> and swaps it live only once it has built and installed. A build that completes but is never activated now survives startup, bounded per component by the new deployment.stagingRetention.maxCount setting (default 5; 0 keeps none). Older builds are removed at the start of the component's next deploy and at startup; anything belonging to an in-flight or unsettled deploy is never touched. Groundwork for deploying from an already-staged build (harper#2315). See Configuration Options.
Going Back to a Previous Release
When a deploy replaces the release that is live, Harper now keeps the replaced one under the deployment_id that deployed it, and deploy_component with that deployment_id makes it live again. Nothing is rebuilt, resolved, or installed, and the root config entry published by that deployment comes back with it. Kept releases share deployment.stagingRetention.maxCount with staged builds (default 5). Each is a full installed copy of the component, so a component can now use up to that many extra copies' worth of disk; set it to 0 to keep none. Activating the id that is already live succeeds without a swap, so an activation that failed partway can be retried with the same id. Nodes that already switched answer success, and the rest switch (harper#2315). See Going back to a previous release.
Root Config Follows the Live Release
A component's entry in the root harper-config.yaml now changes only once its new release is live. A package deploy used to write the entry before it built, so a deploy that failed to install or validate still left config naming the failed release, and the next restart installed it. The entry is now recorded with the deploy's activation and published after the swap, and startup settles a deploy that was interrupted partway through, so config and the live component agree after a crash, including whether the component runs isolated (harper#2315).
A payload deploy now removes package, install, and credentials from the component's entry, keeping its other keys such as host, urlPath, and isolated. Previously they stayed, so a node installing the component from its config entry installed the old package instead of the payload release that was live. See How a deploy updates the root config.
SSH Deploy Keys Are Checked When Added
add_ssh_key and update_ssh_key now refuse a private key that ssh couldn't use, with a 400 that names the problem. Such a key used to be stored as sent, and the next deploy from a private repository failed with a generic authentication error. Refused keys include a public key sent in place of the private one, PuTTY and passphrase-protected keys, DSA and FIDO security keys, SSH certificates, unsupported algorithms, and damaged or mismatched keys. An accepted key is stored normalized (lines trimmed, blank lines dropped, a final newline), so an indented or CRLF paste now works. A host or hostname that would break the ssh config every key on the node shares, such as one containing a space, a quote or an =, or a pattern in host, is refused too.
Keys already stored are not re-checked, and a value already sealed as enc:v1: is checked only as an envelope for this cluster. When a node is cloned, a key on the leader that the new node refuses is skipped and logged by name, and the other keys are still cloned. If the leader keeps failing to return the keys, or the node can't store one, the clone stops Unavailable and unfinished, and a later start retries it, instead of completing without them. See What key, host and hostname must be.
SSH Key Blocks Are Marked in the ssh Config
Each SSH key's block in <rootPath>/ssh/config now runs from its #<name> line through a # END harper ssh key <name> line, with a # BEGIN harper ssh key <name> line directly under the first. get_ssh_key, list_ssh_keys and delete_ssh_key read those lines instead of guessing where a block ends, so a line you add outside a key's lines is never removed with the key or read as its setting. Each node adds the lines to its existing config when it starts, without changing what ssh resolves, and a node rolled back to an earlier version still reads and deletes the blocks as it did before. See The key's block in the ssh config.
Backups
Subscriptions End When Their Database Is Restored (5.3.1)
An online restore_backup now ends every subscription to the database that was opened before the restore, once Harper reloads the database. Each one's last message is a DatabaseGenerationChangedError (status code 409, code DATABASE_GENERATION_CHANGED). Resubscribe to resynchronize against the restored state. A restore that fails before it changes anything reloads the database as it was, and its subscriptions end with a retryable DatabaseClosingError (status code 503, code DATABASE_CLOSING) instead. Previously such a subscription could stall silently, or start receiving the restored database's writes once another subscriber attached. See restore_backup.
Subscriptions
Resuming a Subscription From a Checked Position (5.3.1)
A subscribe request can now carry the databaseGeneration its position came from. Harper then checks the position before and during the replay, and refuses one whose history was replaced (DatabaseGenerationChangedError, status code 409) or pruned (ResumeHistoryUnavailableError, status code 410) instead of replaying what is left. The subscription's resumeVerified promise resolves true once the replay is complete and checked. Requests without the field behave as before, and the check applies to RocksDB databases only. See Resuming from a position.
Durable MQTT Sessions No Longer Replay Short (5.3.1)
A durable MQTT session now resumes through the same checked replay. When a reconnect's saved position names a database that was restored or copied since, or history that audit retention has removed, Harper deletes the session and reports sessionPresent: false (or, if the problem appears after CONNACK, sends an MQTT v5 DISCONNECT with reason code 0x83 and closes the connection) instead of delivering a partial catch-up. A quiet topic's position is kept current, so retention on other tables does not reset it, and positions no longer skip unacknowledged messages that share a transaction. Resubscribing to a topic the session already holds continues from its saved position, and QoS 0 subscriptions are kept with the session and resume live. Each connection's Last Will is kept separately, so a connection that is taken over publishes its own will, never the newer connection's. See Durable Sessions.
Security
Route-Owned Authentication
Harper now defers rejection of an unrecognized Basic or Bearer credential on the application HTTP port until a handler claims the request. Harper-owned routes still return the normal unauthorized response. Unowned routes can fall through to application middleware with the original Authorization header unchanged. This lets catch-all applications and reverse proxies apply their own authentication schemes without URL exemptions or header rewriting, and the unauthorized response such an application returns — including its own WWW-Authenticate challenge — is passed back to the client unchanged. Internal authentication failures continue to fail closed. See Authentication and route ownership.
Operation Allowlist Enforcement
A role's permission.operations allowlist is now checked before every other privilege check on the role, so an operation the list omits is denied whatever else the role carries. Table DDL and SQL previously went around it.
A role that relied on either will lose access on upgrade. Two cases to audit, both fixed by adding the operation to the list:
operationscombined withstructure_user—create_table,create_attribute,drop_table, anddrop_attributenow have to be listed.structure_userstill scopes them to its databases.operationsomittingsql— the role can no longer run SQL.
Roles with no operations field are unaffected, as are roles that already list everything they call. See Operation Permissions.
The reverse also changes: twenty operations that a list could name but never grant are now granted as listed — deploy_component, add_component, drop_component, package_component, set_component_file, set_custom_function, drop_custom_function, drop_custom_function_project, restart_service, set_configuration, get_status, set_status, clear_status, install_node_modules, delete_files_before, delete_audit_logs_before, delete_transaction_logs_before, cleanup_orphan_blobs, search_jobs_by_start_date, and registration_info. A role that already lists one gains it on upgrade, with no edit. A least-privilege CI user can therefore deploy through OIDC Trusted Publishing without being super_user. get_backup and read_transaction_log still cannot be granted by listing, and neither can the legacy catchup operation any more — it applies writes to any table with no table permission check, so a role that lists it loses it on upgrade.
OIDC Trusted Publishing
A CI runner can now authenticate to Harper with no stored credential. It presents an identity token minted by its own provider, and if that token verifies against a trust policy configured on the instance, Harper returns a one-hour operation token for the user the policy names — the same exchange npm, PyPI, and AWS STS AssumeRoleWithWebIdentity use. This replaces a 30-day refresh-token secret with a rule you configure once and revoke with drop_oidc_trust.
Policies are managed with add_oidc_trust, list_oidc_trust, and drop_oidc_trust (all super_user), and live in the replicated system.hdb_oidc_trust table. A policy pins the issuer, an instance-specific audience, and a set of claim constraints. For GitHub Actions those constraints must pin the repository, pin the workflow, and gate the ref — a policy naming only a repository and workflow is refused, because anyone who can push a branch could otherwise add the trusted workflow to it and mint a token. Issuers with no registered profile must pin sub, which makes Kubernetes service accounts, GCP service accounts, and SPIFFE identities work with no provider-specific configuration.
A policy can also carry an operations list, narrowing the minted token to a subset of its user's role on the Operations API and SQL paths — narrowing only, and not covering an application's REST/GraphQL data path, so the policy's user should still be a least-privilege role for the data it can reach.
Identity tokens are single-use, though the replay record replicates asynchronously, so two simultaneous replays against different nodes can both succeed. Exchanges are recorded in the authentication audit stream when logging.auditAuthEvents.logSuccessful / logFailed is enabled — they are off by default, so turn them on before you need the trail. Every rejection returns the same message, with the specific reason written to the oidc-trust log. In the CLI, the exchange ranks below every configured credential, so enabling it does not silently re-point a pipeline that still has a token secret set. See OIDC Trusted Publishing and Workload identity.