Authenticated Range-readable Managed Object Repository
ARMOR is an S3-compatible proxy. It encrypts objects before storing them in Backblaze B2 and serves reads through a Cloudflare domain. Clients use ARMOR credentials and an S3 endpoint; ARMOR holds the B2 credentials and master encryption key (MEK). Its source tests cover a scoped set of S3 operations, including single-PUT writes, reads, listing, and documented multipart cases.
Install Docker with the Compose plugin, then clone this repository for its
compose.yaml and .env.example:
git clone https://github.com/jedarden/ARMOR.git
cd ARMORSelect an image tag that has been published. The release-status
register is the evidence boundary: it records which
tags have published images and what each publication does and does not
prove. The checkout's VERSION and the Compose default move
together at release time, and Compose uses that default when
ARMOR_VERSION is unset. For the commands below, set the tag once:
export ARMOR_VERSION=0.1.2001
docker pull ghcr.io/jedarden/armor:"$ARMOR_VERSION"The published image targets linux/amd64. On Apple Silicon, set
DOCKER_DEFAULT_PLATFORM=linux/amd64 before pulling or starting Compose.
To build from source instead, use Go as specified by go.mod and make build;
AGENTS.md has the build and test commands.
This profile stores data in a temporary filesystem and uses fixed demo
credentials. It needs no B2 or Cloudflare account, and its demo-bucket is
pre-created. The round trip below writes an object, shows that the stored
bytes are ciphertext, and reads the object back byte-identical.
echo "hello, ARMOR" > demo.txt
docker compose --profile demo up -d armor-demo
curl --fail --retry 30 --retry-connrefused --retry-delay 1 \
-so /dev/null http://127.0.0.1:9001/healthz
aws() {
docker run --rm --network container:"$(docker compose --profile demo ps -q armor-demo)" \
-v "$PWD:/work" -w /work \
-e AWS_ACCESS_KEY_ID=armor -e AWS_SECRET_ACCESS_KEY=armor-demo-secret \
-e AWS_DEFAULT_REGION=us-east-1 amazon/aws-cli:2.29.0 \
--endpoint-url http://127.0.0.1:9000 s3 "$@"
}
aws ls # the pre-created demo-bucket
aws cp demo.txt s3://demo-bucket/
docker compose --profile demo cp armor-demo:/tmp ./demo-storage
find ./demo-storage -type f -name demo.txt # the object as stored
if grep -rq "hello, ARMOR" ./demo-storage; then
echo "PLAINTEXT ON DISK — this would be a bug"
else
echo "stored bytes are ciphertext"
fi
rm -rf ./demo-storage
aws cp s3://demo-bucket/demo.txt demo-out.txt
cmp demo.txt demo-out.txt && echo "round trip: byte-identical"
docker compose --profile demo down
rm -f demo.txt demo-out.txtThe aws() helper mounts the current directory so the AWS CLI container can
read and write the demo object; it exists only for this demo. The demo exposes
ports 9000 (S3) and 9001 (admin) and sets a fixed demo admin token. Its
temporary MEK is lost when the container stops, so this Compose profile is for
disposable data. Run go run ./cmd/armor demo -dir ./armor-demo-data for the
CLI demo. It prints generated dashboard credentials, an admin token, the
dashboard URL, and S3 credentials. The command saves its MEK in a mode-600
file under that directory and reuses it after restarts. Its structured startup
logs are JSON; the ready-to-paste AWS CLI configuration is printed as a plain
text block.
These steps run one B2-backed instance with its S3 API on port 9000 and its
admin API bound to host loopback on port 9001. Keep the chosen image tag in
your shell or set ARMOR_VERSION in .env.
-
Prepare B2. Create a bucket and an application key with access to it. Record the bucket name, region, application key ID, and application key. ARMOR uses the B2 S3 API for writes. For Cloudflare reads, configure the bucket for public ciphertext downloads as described in the Cloudflare setup guide.
-
Prepare Cloudflare. Create a proxied CNAME to the bucket's B2 download hostname and set up the read path in the Cloudflare setup guide. Record the Cloudflare hostname without
https://. Set it asARMOR_CF_DOMAIN. If you leave that variable empty, ARMOR reads directly from B2 and B2 egress charges apply. -
Create the environment file and keys. The following generates a random 32-byte MEK and a separate admin bearer token directly into the ignored
.envfile; it prints neither value. Back up the MEK securely before writing objects. Losing it makes existing objects unreadable.umask 077 cp -n .env.example .env python3 - <<'PY' from pathlib import Path import secrets path = Path('.env') values = path.read_text() for name in ('ARMOR_MEK', 'ARMOR_ADMIN_TOKEN'): values = values.replace(f'{name}=\n', f'{name}={secrets.token_hex(32)}\n', 1) path.write_text(values) PY
-
Fill in the B2 and client credentials in
.env. SetARMOR_B2_REGION,ARMOR_B2_ACCESS_KEY_ID,ARMOR_B2_SECRET_ACCESS_KEY,ARMOR_BUCKET,ARMOR_CF_DOMAIN,ARMOR_AUTH_ACCESS_KEY, andARMOR_AUTH_SECRET_KEY. Use distinct ARMOR client credentials; they are separate from the B2 application key. Required configuration lists the minimum, and the configuration reference explains every setting. SetARMOR_VERSIONthere if the shell export above will not persist. -
Start and verify. Compose passes the MEK and admin token from
.envto the server.armor checkloads that same environment and probes the bucket, Cloudflare path, and MEK/canary state. An empty new bucket can produce warnings until the canary exists; investigate anyFAILresult.docker compose --profile production up -d armor-production docker compose --profile production exec -T armor-production /armor check curl --fail http://127.0.0.1:9001/healthz
Keep .env out of version control. The admin token enables key management and
format migration; follow the linked runbooks before using either operation.
| Variable | What to provide |
|---|---|
ARMOR_B2_REGION |
The bucket's B2 region, for example us-east-005 |
ARMOR_B2_ACCESS_KEY_ID, ARMOR_B2_SECRET_ACCESS_KEY |
The bucket-scoped B2 application key |
ARMOR_BUCKET |
The B2 bucket name |
ARMOR_CF_DOMAIN |
The Cloudflare read hostname; leave empty only if direct B2 reads are intended |
ARMOR_MEK |
A stable 32-byte key encoded as 64 hex characters |
ARMOR_AUTH_ACCESS_KEY, ARMOR_AUTH_SECRET_KEY |
Credentials that S3 clients use with ARMOR |
ARMOR_ADMIN_TOKEN |
A separate bearer token for admin operations |
Point an S3 client at http://<armor-host>:9000 and use the ARMOR client
credentials from .env. Use path-style addressing. The connection
guide gives AWS CLI, rclone, boto3, DuckDB,
litestream, and barman examples. armor client-config --for aws-cli --endpoint http://<armor-host>:9000 --bucket <bucket> prints the client settings for a
specific endpoint; see the CLI reference.
The documentation index links the operator guides. Start with key rotation, format migration, disaster recovery, and the release process. Keep the MEK and B2 credentials available to recovery tooling, and verify restores with the intended image and object population.
Legacy v1 objects have the counter defect recorded in ADR-005;
the v2/v3 confidentiality guarantee applies to them only after migration.
Follow the migration reference
before running armor migrate --target v3, then audit with armor verify or
the continuous restore verifier.
armor help lists serve, demo, check, client-config, decrypt,
verify, migrate, and version. The CLI reference
documents flags, exit codes, credentials, and safe use.
The admin listener exposes health, version, and metrics endpoints. Token-gated routes cover key management, migration, and other operations. See the HTTP API reference for the route and authorization contract.
The repository tests a defined S3 operation and client matrix. Publication proof, deployment verification, active regressions, and limits on range performance, multipart/DR checks, and named-key reads are tracked in the release-status register. Use that register for the exact image and deployment boundary before a production rollout.
