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:
- Entries covered by an anchor cannot be rewritten without exposure: the rewritten chain no longer matches the immutable checkpoint in the bucket.
- Entries written since the last anchor sweep rely on the signature and hash chain alone. The
span between sweeps is your tamper-exposure window — that is what
VAULT_ANCHOR_INTERVAL_MINUTESsets.
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:
- The worker runs the anchor sweep and the daily sample-verify job: it needs
s3:PutObject(pluss3:PutObjectRetentionwhen Object Lock is on) ands3:GetObject. - The API serves the admin anchor endpoints: it needs
s3:GetObjectands3:ListBucket.
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:
- Tighten (15–60 min) for high-value, irreversible workloads where hours of unpinned records is too much exposure.
- Default 360 (6 h) is a reasonable posture for most deployments.
- Loosen (up to 2880 = 48 h) where object economy matters more than a tight window.
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:
agledger_vault_anchor_upload_total{outcome}— alert onoutcome="error": a misconfigured bucket or IAM regression silently widens the tamper-exposure window.agledger_vault_anchor_verify_total{outcome}— alert onoutcome="tamper"(and investigateno_such_key).
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
- Detect, not prevent. An operator with the signing key can still rewrite within the current window; the next sweep exposes everything already pinned.
- One bucket = one trust domain. Separate the account administering the anchors from the one running the Server.
- COMPLIANCE retention is irreversible. You cannot shorten it after the fact.
Related
- Key-compromise runbook — anchors are what distinguish pre-compromise history from a forged rewrite.
- Audit runbook — offline chain verification with published keys.
- Day-2 operations — ops-summary and alerting.
Validated against API v1.0.2 on 2026-06-10.