External anchoring

External anchoring periodically writes each record chain's signed checkpoint to S3-compatible object storage, by default under Object Lock in COMPLIANCE mode. Once written, an anchor is immutable for its retention period — neither an attacker nor an administrator of the AGLedger Server can alter or delete it.

This is the layer that makes tamper-evidence hold against the operator of the system itself, not only against an outside attacker.

Why anchoring matters

Every chain entry is Ed25519-signed and hash-linked, so any modification to stored history is detectable on verification — provided the verifier trusts the signing key. An administrator who holds both the database and the active signing key can, in principle, rewrite a chain and re-sign it; the rewritten chain would verify cleanly. Anchoring closes this gap:

Anchoring detects tampering after the fact; it does not prevent it in real time. Without it, all tamper-evidence layers live in the same PostgreSQL database a privileged attacker would rewrite — which is why a production Server logs a boot warning while anchoring is disabled.

What gets anchored

Checkpoints are per record chain, swept incrementally on the anchor interval: each sweep checkpoints chains that advanced since their last checkpoint, up to 100 per sweep (the remainder rolls to the next sweep). Each anchor object is a small JSON document (well under a kilobyte) carrying the checkpoint's canonical COSE_Sign1 envelope (base64), the record ID, chain position, payload hash, signing key ID, the instance ID, a document version, and the anchor timestamp. Record payloads never leave your database — the anchor is a cryptographic commitment, not a copy.

Objects land at a deterministic key inside the configured bucket:

vault-anchors/<instance>/<YYYY-MM-DD>/<recordId>/<chainPosition>.json

where <instance> is AGLEDGER_INSTANCE_ID (default default) and the date is the checkpoint's creation date.

Because checkpoints are per-chain, the object count scales with how many chains advance per interval, not with a fixed per-tick rate. A low-volume install produces a handful of objects per sweep; a high-volume install produces up to the 100-chain sweep cap per sweep.

Checkpoints are created even when anchoring is disabled — they are the in-database layer of the same control. Enabling anchoring adds the external, immutable copy.

Enable it (Docker Compose)

Anchoring is disabled by default. In your .env:

VAULT_ANCHOR_ENABLED=true
VAULT_ANCHOR_S3_BUCKET=my-org-agledger-anchors
VAULT_ANCHOR_S3_REGION=us-east-1
# VAULT_ANCHOR_S3_ENDPOINT=http://minio:9000   # non-AWS S3-compatible stores (MinIO, etc.)
VAULT_ANCHOR_INTERVAL_MINUTES=360              # tamper-exposure window; see below
VAULT_ANCHOR_OBJECT_LOCK=true                  # COMPLIANCE-mode immutable retention (default)
VAULT_ANCHOR_RETENTION_DAYS=2555               # ~7 years (default)

Credentials come from the standard AWS credential chain (instance role, AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, or a profile). Two processes touch the bucket:

Grant only s3:PutObject to the worker and every verify surface in this runbook returns s3_error.

Defaults if unset: bucket vault-anchors, region from AWS_REGION (falling back to us-east-1), Object Lock on, retention 2555 days, interval 360 minutes.

Enable it (Kubernetes / Helm)

The chart passes these through extraEnv (there is no dedicated vault.anchor values block):

extraEnv:
  - name: VAULT_ANCHOR_ENABLED
    value: "true"
  - name: VAULT_ANCHOR_S3_BUCKET
    value: my-org-agledger-anchors
  - name: VAULT_ANCHOR_S3_REGION
    value: us-east-1

On EKS, prefer an IRSA role on the worker's service account over static credentials.

Prepare the bucket

Object Lock must be enabled at bucket creation — it cannot be added to an existing bucket:

aws s3api create-bucket \
  --bucket my-org-agledger-anchors \
  --region us-east-1 \
  --object-lock-enabled-for-bucket

AGLedger applies per-object COMPLIANCE-mode retention of VAULT_ANCHOR_RETENTION_DAYS as each anchor is written. In COMPLIANCE mode no one — including the account root — can delete the object or shorten its retention until it expires. That permanence is the point; choose the retention deliberately.

For the strongest posture, put the bucket in a separately-administered account (or a different provider), so no single set of credentials controls both the database and the anchors. A bucket in the same account as the Server is a single trust domain.

Air-gapped installs: point VAULT_ANCHOR_S3_ENDPOINT at MinIO (path-style addressing is handled automatically) and enable MinIO's object locking on the bucket.

Choose the interval

VAULT_ANCHOR_INTERVAL_MINUTES (default 360, clamped to 5–2880, snapped to cron granularity) is a risk-tolerance dial, not a performance knob. It bounds how much recent history has no external evidence yet:

The sweep is incremental, so short intervals stay cheap on the database; the cost of tightening is the permanent Object Lock object count.

Verify and monitor

Posture at a glance (anchoring enabled, interval, bucket) — GET /v1/admin/ops-summary.

List a record's anchors:

curl -H "Authorization: Bearer $ADMIN_KEY" \
  "$AGLEDGER_URL/v1/admin/vault/anchors?recordId=<uuid>"

Verify anchors against the database (exposes any divergence between the live chain and the pinned state):

curl -X POST -H "Authorization: Bearer $ADMIN_KEY" -H "Content-Type: application/json" \
  -d '{"recordId": "<uuid>"}' \
  "$AGLEDGER_URL/v1/admin/vault/anchors/verify"

A daily background job also verifies a random sample of 20 checkpoints against their anchors (checkpoints not yet anchored are skipped, so a run may verify fewer than 20). Two Prometheus counters back alerting:

Uploads are deliberately non-blocking: an S3 outage never stops notarization. The counter is the signal that anchors are not landing.

In-database checkpoints (anchored or not) are readable at GET /v1/audit-vault/checkpoints. Full request/response shapes are in the API reference.

Honest limitations

Related


Validated against API v1.0.2 on 2026-06-10.