This is the operator and client reference for ARMOR's HTTP surfaces. It
describes the routes registered by internal/server/server.go; the route
inventory test in internal/server/http_api_reference_test.go fails when a
registered path is not represented here.
The S3 listener is ARMOR_LISTEN (default :9000). It is path-style S3:
/bucket/key, not virtual-hosted style. The admin listener is
ARMOR_ADMIN_LISTEN (default 127.0.0.1:9001). Keep the admin listener on a
private network or loopback address; it contains key-management and recovery
operations.
Every response from the server carries Server: ARMOR/<version>. Responses
from the S3 handler also carry x-amz-request-id and x-amz-id-2. Use those
IDs when correlating a client failure with logs.
| Surface | Authentication | Failure behavior |
|---|---|---|
S3 object and bucket API (/) |
AWS SigV4 using an ARMOR access-key/secret-key pair. Header authentication uses Authorization and X-Amz-Date; query authentication is accepted for AWS-style S3 presigned requests. ACLs are evaluated after signature verification. |
Authentication or ACL failures are HTTP 403 with an S3 XML error. The error code identifies cases such as MissingAuthenticationToken, InvalidAccessKeyId, SignatureDoesNotMatch, or AccessDenied. |
| S3 health, readiness, and version | None | These probes are public. |
Share URL (/share/<token>) |
The HMAC-signed, expiring token is the credential. No SigV4 or admin bearer token is expected. | Invalid, expired, or malformed tokens are rejected before object access. |
| Admin API | Authorization: Bearer <ARMOR_ADMIN_TOKEN> for every route except /healthz, /version, /armor/canary, and /metrics. |
Missing/invalid bearer token is 401. If ARMOR_ADMIN_TOKEN is unset, gated routes are disabled and return 403. Gated calls are audit-logged without logging the token or MEK. |
/admin/presign |
Both admin bearer authentication and a valid S3 SigV4 credential with get permission for the requested object. |
The admin gate runs first; the endpoint then returns the normal S3-style authorization error for an invalid signer or denied object. |
Dashboard (/dashboard...) |
HTTP Basic (ARMOR_DASHBOARD_USER + ARMOR_DASHBOARD_PASS) or a dashboard bearer token. If no dashboard credential is configured, the dashboard is still protected by the admin gate rather than becoming anonymous. |
Dashboard authentication failures are 401 with WWW-Authenticate: Basic realm="ARMOR Dashboard". |
The ACL action mapping is documented in Authentication:
get covers reads, put covers writes including multipart, delete covers
committed-data deletion, abort covers only aborting an incomplete multipart
upload, and list covers listings. A credential with no ACL action segment
retains full access to its allowed bucket/prefix.
S3 handler errors are XML and have this shape (fields can be omitted when no request context is available):
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>AccessDenied</Code>
<Message>Access Denied</Message>
<RequestId>...</RequestId>
<Resource>/bucket/key</Resource>
</Error>Treat Code and HTTP status as the stable contract; messages may include a
backend diagnostic. The common S3 statuses are:
| Status | Meaning and examples |
|---|---|
| 200 | Successful read, write, list, copy, configuration operation, or bulk-delete response. |
| 204 | Successful DeleteObject, DeleteBucket, lifecycle deletion, or multipart abort. |
| 206 | Successful uncompressed byte-range GetObject or share download. |
| 304 | Conditional object request was not modified. |
| 400 | Malformed XML, invalid query/body, invalid range/part, or a multipart contract violation. |
| 403 | Authentication/ACL denial, access to the reserved .armor/ namespace, or a quarantined object. |
| 404 | Missing object, bucket, or multipart upload. |
| 405 | Unsupported method on the S3 router or an endpoint that explicitly checks methods. |
| 412 | If-Match/If-Unmodified-Since precondition failed. |
| 416 | Invalid/out-of-bounds object range, or a range requested for a compressed object. |
| 500 | Backend, encryption, decryption, manifest, or serialization failure. |
| 503 | Readiness failure, or a temporary multipart ordering/back-end condition (SlowDown). |
Admin JSON endpoints generally return Content-Type: application/json; the
few B2-key and method-gate failures use http.Error, so callers should use
the status and treat the body as diagnostic rather than requiring one error
schema. Metrics is Prometheus text. Dashboard failures are plain text unless
the handler returns JSON for a successful response.
With ARMOR_PREFIX=tenant/, a client still addresses bucket/key. ARMOR
stores the object at tenant/key, prepends the prefix to list filters,
markers, multipart state, HMAC sidecars, manifests, provenance records, and
canary state, and strips it from keys and common prefixes in client responses.
The prefix is not a second client-visible path component. This behavior also
applies to ListObjectVersions, ListMultipartUploads, and the manifest and
migration admin operations.
The configured ARMOR_BUCKET is canonical. Names in
ARMOR_BUCKET_ALIASES resolve to that bucket for S3 requests and presign
requests, so aliases do not create another storage or ACL scope. The
/.armor/ namespace is reserved for ARMOR's internal state: client S3 access
to keys beginning .armor/ is denied with 403. Do not repair or delete that
state through S3; use the dedicated admin routes and the runbooks.
The S3 handler is the prefix-transparent data path. The direct backend paths
used by /share/<token> and the dashboard object operations do not prepend
ARMOR_PREFIX to the requested object key. Consequently, do not use presigned
share/download/upload/delete dashboard paths as a prefix-transparent API in a
shared-bucket deployment; use the SigV4 S3 routes, or explicitly validate the
physical key behavior before enabling those convenience routes.
All requests in this section use the S3 listener and SigV4 unless explicitly marked public. The bucket and object forms below are path-style. XML response operations use the S3 namespace; encrypted object sizes and ranges are plaintext sizes.
| Operation | Request | Success | Common errors and safe-use notes |
|---|---|---|---|
ListBuckets |
GET / |
200 XML | 403/500. Lists the backend's visible buckets; it is not a substitute for testing one configured bucket. |
ListObjectsV2 |
GET /<bucket> (optionally ?list-type=2, prefix, delimiter, continuation-token, max-keys) |
200 XML with client-visible keys, plaintext sizes, and continuation token | 403/500. Prefix and continuation markers are translated through ARMOR_PREFIX; never include .armor/ in a client prefix. |
HeadBucket |
HEAD /<bucket> |
200, no body | 403/404. It checks bucket reachability, not object encryption health. |
CreateBucket |
PUT /<bucket> |
200 with Location: /<bucket> |
403/500. Bucket creation is passed to the backend; use the configured bucket in production. |
DeleteBucket |
DELETE /<bucket> |
204 | 403/500. This is destructive and backend-dependent; verify the target bucket before issuing it. |
GetObject |
GET /<bucket>/<key>; supports Range, conditional headers, and S3 metadata response headers |
200 plaintext; 206 for a valid uncompressed range; 304 for a satisfied not-modified condition | 403/404/412/416/500. Compressed objects do not support byte ranges; ARMOR verifies block HMACs while decrypting. |
HeadObject |
HEAD /<bucket>/<key>; supports conditional headers |
200 headers only; 304 for a satisfied not-modified condition | 403/404/412/500. Content-Length, ETag, and metadata describe the plaintext object where ARMOR has encryption metadata. |
PutObject |
PUT /<bucket>/<key> with plaintext body; optional If-None-Match: * |
200 | 400 for invalid compression/arguments, 403, 412 for create-only collision, 500, or 501 when the backend cannot provide atomic conditional writes. Overwrite is replacement, not version-safe archival. The precondition is checked before the body is read when the manifest/HEAD sees an existing object; If-None-Match: * is atomic only within one ARMOR process, so create-only writers must be pinned to a single replica. An append-only credential (put without delete) is create-only on every write: a conflict on its own explicit If-None-Match: * is 412 PreconditionFailed, while an unconditional overwrite attempt is 403 AccessDenied (ADR-017). |
DeleteObject |
DELETE /<bucket>/<key> |
204 | 403/500. It removes the logical object and updates ARMOR bookkeeping; it is not an abort operation. |
CopyObject |
PUT /<bucket>/<destination> with x-amz-copy-source: /<source-bucket>/<source-key> |
200 XML | 400 invalid/missing source, 403 source or destination ACL denial, 404 source missing, 500. ARMOR re-wraps encrypted object key material for the destination key; authorize both source read and destination write. |
DeleteObjects |
POST /<bucket>?delete with S3 Delete XML |
200 XML; per-key AccessDenied entries can be returned inside a successful batch response |
400 malformed/empty XML, 403/500. ACLs are checked per key. Treat the response's <Error> entries as failures even when HTTP is 200. |
GetBucketLocation |
GET /<bucket>?location |
200 XML | 403/404/500. Returns the static location contract needed by S3 clients. |
GetBucketVersioning |
GET /<bucket>?versioning |
200 XML | 403/404/500. ARMOR returns its supported versioning configuration; do not infer that overwrites are recoverable. |
GetBucketLifecycleConfiguration |
GET /<bucket>?lifecycle |
200 XML | 403/500. Lifecycle is passed through and is not encrypted by ARMOR. |
PutBucketLifecycleConfiguration |
PUT /<bucket>?lifecycle with lifecycle XML |
200 | 403/500. Validate retention policy before applying it; it can delete ciphertext needed for recovery. |
DeleteBucketLifecycleConfiguration |
DELETE /<bucket>?lifecycle |
204 | 403/500. Destructive configuration change. |
GetObjectLockConfiguration |
GET /<bucket>?object-lock |
200 XML | 403/404/500. Backend configuration is passed through. |
PutObjectLockConfiguration |
PUT /<bucket>?object-lock with XML |
200 | 403/500. Use the backend's object-lock semantics; this does not encrypt or version data itself. |
GetObjectRetention |
GET /<bucket>/<key>?retention |
200 XML | 403/500. Retention is backend metadata, not an ARMOR envelope field. |
PutObjectRetention |
PUT /<bucket>/<key>?retention with XML |
200 | 403/500. Treat changes as a compliance operation. |
GetObjectLegalHold |
GET /<bucket>/<key>?legal-hold |
200 XML | 403/500. Backend metadata read. |
PutObjectLegalHold |
PUT /<bucket>/<key>?legal-hold with XML |
200 | 403/500. Backend metadata write. |
OPTIONS on the S3 catch-all is handled as a CORS preflight and returns 200
without SigV4 authentication. It advertises GET, PUT, DELETE, HEAD, POST, OPTIONS and the request headers ARMOR accepts. Do not mistake a preflight
success for authorization to perform the following S3 operation.
Multipart uses the standard S3 query dispatch. An upload has one logical key, one upload ID, and ARMOR-managed encryption state; only the completed object is visible to readers.
| Operation | Request | Success | Common errors and safe-use notes |
|---|---|---|---|
CreateMultipartUpload |
POST /<bucket>/<key>?uploads |
200 XML containing upload ID | 400 when compression is enabled, 403, 500. Multipart is intentionally rejected when ARMOR_COMPRESS or compression rules are active; use a single PUT for compressed objects. If-None-Match is not honored for multipart initiation. |
UploadPart |
PUT /<bucket>/<key>?uploadId=<id>&partNumber=<n>; ARMOR also accepts the equivalent POST form |
200 with part ETag | 400 InvalidRequest, InvalidPart, or InvalidPartSize; 404 NoSuchUpload; 503 SlowDown while required earlier state is unavailable; 500. Part numbers are 1–10,000. |
ListParts |
GET /<bucket>/<key>?uploadId=<id> |
200 XML | 403/404 NoSuchUpload/500. Returned sizes are plaintext part sizes. |
CompleteMultipartUpload |
POST /<bucket>/<key>?uploadId=<id> with CompleteMultipartUpload XML |
200 XML with location, key, bucket, and ETag | 400 MalformedXML, InvalidRequest, InvalidPart, or InvalidPartSize; 404 NoSuchUpload; 500. ARMOR validates the submitted part set and writes the manifest/sidecar before exposing the completed object. |
AbortMultipartUpload |
DELETE /<bucket>/<key>?uploadId=<id> |
204 | 403/404 NoSuchUpload/500. Abort abandoned uploads; an upload ID is not a delete credential for a completed object. |
ListMultipartUploads |
GET /<bucket>?uploads (optionally prefix, markers) |
200 XML | 403/500. Client-visible keys and markers have ARMOR_PREFIX stripped. |
Safe multipart use:
- For current v3 writes, parts may be uploaded out of order and part sizes are not pinned by the v3 layout. For v2-compatible writes, upload part 1 first, use one uniform block-aligned size for non-final parts, use at least 5 MiB for a multi-part object, and allow only the final part to be short.
- Retry the same part with the same part number and size. A size-changing retry or a violated uniform-size contract can poison a v2 upload; abort it and start a new upload. Do not complete a poisoned upload.
- Preserve the ETag returned for every part and submit the complete XML with the exact part numbers. Keep the upload ID private until completion or abort. See Multipart Client Compatibility and Multipart Layout for the format details.
| Endpoint | Method | Auth | Responses |
|---|---|---|---|
/healthz on S3 or admin listener |
GET (the liveness handler does not inspect the method) |
Public | 200 with OK; it only proves the process is alive. |
/readyz on S3 listener |
GET (the readiness handler does not inspect the method) |
Public | 200 JSON when the small-object canary is healthy, canary is explicitly disabled, or the manifest writer flushed recently; 503 JSON otherwise. The body includes ready, canary_age_s, multipart_canary_healthy, manifest_flushed_s, and reason. The multipart field is diagnostic and does not gate readiness. |
/version on S3 or admin listener |
GET |
Public | 200 JSON containing version, format_write_version, and go; other methods return 405. |
/armor/canary on admin listener |
GET |
Public | 200 JSON with the canary result. If no monitor is configured, 200 with {"status":"unknown","error":"canary monitor not configured"}. Other methods return 405. |
/metrics on admin listener |
GET for Prometheus scrapers |
Public | 200 Prometheus text (text/plain; version=0.0.4). The exposition includes request, encryption, backend, replication, key-rotation, provenance, and canary families; see Metrics. |
The canary performs real encrypted round trips. A healthy small-object canary does not imply a healthy multipart canary; inspect both the canary JSON and the multipart metric families. The canary writes internal objects beneath the configured prefix and they must not be manipulated as user objects.
S3 SigV4 presigned GET URLs use query authentication on the normal S3 object
route. They require X-Amz-Expires from 1 through 604800 seconds (seven
days). Missing or invalid values return HTTP 400 with
AuthorizationQueryParametersError; an expired valid URL returns
RequestExpired. This is separate from the share-token routes below.
The request body is JSON:
{
"bucket": "optional-bucket-or-alias",
"key": "reports/report.parquet",
"expires_in": "1h",
"content_disposition": "attachment; filename=report.parquet",
"range": "bytes=0-1023"
}key is required. expires_in defaults to one hour and must use a duration
accepted by ARMOR. The endpoint returns 200 JSON with url, expires_in, and
UTC expires_at. The generated URL is a GET-only share link. It is allowed
only when ARMOR_PRESIGN_ENABLED=true; otherwise it returns 404. Invalid JSON,
missing key, or invalid expiration returns 400; signer/ACL failures return
403; URL-generation failures return 500.
The request's key is used as the token's backend key; /admin/presign does
not add ARMOR_PREFIX. For a prefixed shared bucket, use the normal S3 GET
operation instead of minting a share URL for a logical client key.
The share handler verifies the token, HEADs the object, and returns decrypted
plaintext for ARMOR objects or passthrough bytes for non-ARMOR objects. A full
download returns 200; an uncompressed valid range returns 206 with
Content-Range. The token can carry a fixed range and content-disposition;
otherwise the request's Range header is used.
| Condition | Status |
|---|---|
| Missing/malformed token or invalid range | 400 |
| Invalid HMAC signature | 403 |
| Expired token | 410 |
| Object absent | 404 |
| Range over a compressed object | 416 |
| Backend/decryption failure | 500 |
| Non-GET | 405 |
Share URLs are bearer credentials. Use HTTPS, choose the shortest practical
expiry, do not put them in logs or tickets, and assume anyone who receives one
can download the object until it expires. A range link cannot be used for
compressed objects because compression destroys fixed plaintext offsets. In an
ARMOR_PREFIX deployment, the share handler reads the token's key directly
from the backend; it does not add the client namespace prefix, so use the S3
GET path for prefixed objects.
All routes in this section are on the admin listener and require the admin
bearer token unless the table says public. Responses are JSON unless noted.
Long-running or destructive routes should be run from a private operator
network and observed to completion.
| Endpoint | Method | Success and behavior | Errors / safe-use constraints |
|---|---|---|---|
/admin/key/verify |
GET |
200 verified when canary decrypt and HMAC checks pass; 200 unknown when no canary monitor exists |
503 unverified; 405 otherwise. This is a diagnostic, not a key-rotation action. |
/admin/key/rotate |
POST with an empty body |
200 JSON fingerprint rotation result against the configured active key; key-id selects a named key. |
400 for any non-empty body or invalid key; 500 rotation failure. Follow the key-ring runbook, take a backup/escrow first, expect a long-running bucket walk, and verify the canary afterward. |
/admin/key/ring |
GET with optional census=head |
200 JSON active/ring fingerprints and object histogram. Default census uses the manifest; census=head verifies live metadata. |
500 key/backend failure. census=head can take hours on a production bucket; run it detached and reconcile head_failures and unattributed fingerprints. |
/admin/key/export?confirm=yes |
GET |
200 JSON break-glass escrow containing the active MEK, active_key_fingerprint, every default-key ring_keys[] entry (fingerprint + mek), and B2 connection credentials |
400 without exact confirm=yes; 405 otherwise. Treat the response as a root credential: use only for recovery, transmit over HTTPS, store in an approved secret escrow, and delete local copies after use. |
/admin/format/migrate |
GET |
200 JSON current migration state or no_migration |
500 when no encryption key is available. |
/admin/format/migrate |
POST with dry_run, target, include, concurrency query parameters |
200 JSON migration result | 400 invalid target/include/concurrency; 500 migration failure. Start with dry_run=true, ensure target matches the configured write version, and do not run overlapping migrations. |
/admin/manifest?key=<key>[&bucket=<bucket>] |
GET |
200 JSON manifest freshness/quarantine state | 400 missing key, 404 missing manifest, 409 incomplete freshness state, 500. key is the logical unprefixed object key. |
/admin/manifest/repair?key=<key>[&bucket=<bucket>] |
POST |
200 JSON repaired state; re-stamps completion to the ciphertext timestamp | 400/404/409/500. Use only after verifying the ciphertext is canonical; this changes the read gate. |
/admin/manifest/quarantine?key=<key>&reason=<reason>[&bucket=<bucket>] |
POST |
200 JSON quarantined state | 400/404/409/500. This deliberately makes reads return 403 rather than retryable 500; record the reason in the incident evidence. |
/admin/manifest/release?key=<key>[&bucket=<bucket>] |
POST |
200 JSON released state; idempotent | 400/404/409/500. Release only after the object has been repaired or otherwise proven readable. |
/admin/creds |
GET |
200 JSON credential names, ACLs, source, and load times; secrets are omitted | 405 otherwise. Still sensitive operational metadata; restrict access and do not publish the response. |
/admin/provenance/compact?writer=<id> |
POST |
200 JSON legacy-chain compaction result | 400 missing writer, 500 compaction failure. Maintenance operation; use the provenance runbook and preserve the audit evidence. |
/armor/audit |
GET |
200 JSON provenance-chain audit result | 500 audit failure, 405 otherwise. Read-only but can walk a large chain; ARMOR_PREFIX is applied to the chain namespace. |
/admin/b2/keys |
GET |
200 JSON B2 application-key list (count and cursor supported) |
503 when B2 key management is unavailable, 500 backend/API failure. |
/admin/b2/keys |
POST |
201 JSON newly-created B2 application key | 400 malformed body or missing name/capabilities; 503 unavailable; 500 B2 failure. The response contains a secret key: capture it only into an approved secret destination. |
/admin/b2/keys/<id> |
DELETE |
204 | 400 missing ID, 404 unknown key, 503 unavailable, 500 B2 failure. Revocation is destructive and can immediately break clients using that key. |
/admin/key/export and B2-key creation are the two admin responses that can
contain credential material. Do not paste either response into logs, shell
history, bead notes, or documentation.
The dashboard is registered only when dashboard support is configured. All dashboard routes use the dashboard authentication described above; they do not use S3 SigV4 from the browser. The dashboard's configured credential performs the underlying object operations and is subject to its own ACL.
| Endpoint | Method | Success | Common errors |
|---|---|---|---|
/dashboard or /dashboard/ |
GET |
200 HTML bucket view; prefix and continuation_token select the view |
401 dashboard auth, 500 listing/template failure. |
/dashboard/agentation.js |
GET |
200 JavaScript module for the visual-feedback toolbar | 401 dashboard auth. |
/dashboard/object?key=<key> |
GET |
200 JSON object details, including ARMOR metadata when available | 400 missing key, 404 missing object, 500 backend failure. |
/dashboard/metrics |
GET |
200 JSON live request/cache/canary/replication metrics | 401 dashboard auth. |
/dashboard/encryption-stats?prefix=<prefix> |
GET |
200 JSON encrypted/plaintext counts and key usage | 401/500. |
/dashboard/api/list?prefix=<prefix>&continuation_token=<token> |
GET |
200 JSON listing with objects, common prefixes, and pagination | 400/401/500. |
/dashboard/credential-activity |
GET |
200 JSON per-credential allow/deny activity | 401 or 502 when the loopback admin credential query fails; 500 decode failure. |
/dashboard/upload |
POST multipart form (file, optional key, content_type) |
200 JSON upload result | 400 malformed/missing file, 403 dashboard credential not configured or backend ACL denial, 500. Maximum parsed form size is 100 MiB. |
/dashboard/download?key=<key> |
GET |
200 object bytes with attachment headers | 400 missing key, 403 disabled/denied, 404 missing object, 500. |
/dashboard/delete?key=<key> |
DELETE or POST |
200 JSON delete result | 400 missing key, 403 disabled/denied, 500. This is destructive. |
/dashboard/presign |
POST JSON (key, optional expires_in) |
Proxies the 200 JSON result from /admin/presign |
400 invalid body/key, 401 dashboard auth, 404 disabled, 502 loopback/admin failure. |
/dashboard/admin/key/status |
GET |
200 JSON rotation state, including none when idle |
401/405/500. |
The dashboard's prefix query is passed to the backend as a listing prefix.
Dashboard object operations are direct backend calls and do not apply the S3
handler's ARMOR_PREFIX translation. In a shared-bucket prefixed deployment,
use the S3 API for object reads and mutations and treat dashboard object
actions as unsafe unless their physical-key behavior has been explicitly
validated. Callers should never send the internal .armor/ prefix.