Skip to content

About

Security-first AI artifact registry — digest-based storage, policy-gated promotion, signed provenance, and reproducible trust metadata for local models, adapters, and tokenizers

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

10 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ai-model-registry

CI Go Report Card License

Security-first AI artifact registry — digest-based storage, policy-gated promotion, signed provenance, and reproducible trust metadata for local models, adapters, tokenizers, and related files.

What it does

ai-model-registry is a lightweight HTTP service that manages the lifecycle of AI artifacts (models, adapters, tokenizers) with a strict trust model:

  1. Default-deny trust — artifacts must be explicitly promoted through a policy-gated pipeline before they become available for runtime consumption.
  2. Explicit artifact states — every artifact has a lifecycle state (acquired, quarantined, trusted, revoked, deleted) that determines whether it can be loaded. Only trusted artifacts are available for runtime.
  3. Fail-closed auth — every registry API except /health requires a file-backed service token. Production startup fails if that credential is unavailable.
  4. Immutable digest storage — promotion copies verified bytes to a no-replace objects/sha256/... path. Runtime resolution returns the path, digest, size, and storage-contract version together.
  5. Replayable state — every event contains a complete post-transition artifact snapshot; startup replays the entire chain and requires it to reconstruct registry state exactly.
  6. Immutable boot fallback — supports a baked-in models.lock.yaml for deterministic boot with pre-approved, content-addressed artifacts.

Quick start

# Build. Production/container builds use the reviewed gguf-guard pin in
# build/gguf-guard.lock. Native builds must install that binary at
# /usr/local/bin/gguf-guard or inject a fixed path and binary digest:
GUARD=/absolute/path/to/reviewed/gguf-guard
GUARD_SHA256=$(sha256sum "$GUARD" | cut -d ' ' -f1)
go build -ldflags "-X main.ggufGuardBin=$GUARD -X main.ggufGuardSHA256=$GUARD_SHA256" -o registry .
go build -o securectl ./cmd/securectl/

# Create a service token (required for every non-health API)
mkdir -p /tmp/registry
openssl rand -hex 32 > /tmp/registry/service-token
chmod 600 /tmp/registry/service-token

# Run with authentication enabled (default, secure)
REGISTRY_DIR=/tmp/registry \
  SERVICE_TOKEN_PATH=/tmp/registry/service-token \
  ./registry

# In another terminal — use the same token for authenticated requests
TOKEN=$(cat /tmp/registry/service-token)
./securectl status
./securectl list

# After placing my-model.gguf in REGISTRY_DIR, record and quarantine it.
DIGEST=$(sha256sum /tmp/registry/my-model.gguf | cut -d ' ' -f1)
SIZE=$(wc -c < /tmp/registry/my-model.gguf | tr -d ' ')
curl -X POST http://127.0.0.1:8470/v1/model/acquire \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"name\":\"my-model\",\"filename\":\"my-model.gguf\",\"sha256\":\"$DIGEST\",\"size_bytes\":$SIZE}"
curl -X POST 'http://127.0.0.1:8470/v1/model/quarantine?name=my-model' \
  -H "Authorization: Bearer $TOKEN"

# Promote only after the external quarantine scanners pass.
curl -X POST http://127.0.0.1:8470/v1/model/promote \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"name\":\"my-model\",\"filename\":\"my-model.gguf\",\"sha256\":\"$DIGEST\",\"size_bytes\":$SIZE,\"scan_results\":{\"modelscan\":\"pass\"},\"scanner_versions\":{\"modelscan\":\"<version>\"},\"policy_version\":\"local-v1\"}"

Container

podman build -f Containerfile -t ai-model-registry .

# Run with a service token mounted
podman run --read-only --cap-drop=ALL --security-opt=no-new-privileges \
  --user "$(id -u):$(id -g)" --pids-limit=64 \
  -p 127.0.0.1:8470:8470 \
  -v /path/to/models:/registry \
  -v /path/to/service-token:/run/secure-ai/service-token:ro \
  ai-model-registry

The image listens on 0.0.0.0:8470 inside its own network namespace so host port publishing works. Keep the host publish address at 127.0.0.1, as above, unless a TLS/mTLS identity-aware proxy is the only external entry point. The final image includes gguf-guard built from the commit and archive SHA-256 in build/gguf-guard.lock; registry startup verifies the installed binary digest and fails if it is missing or replaced.

API

The optional standalone helper admission v2 contract adds capability/prepare/commit and typed historical receipts. Its companion producer and sandbox qualification are in progress; see that document before selecting helper-v2-required mode.

Endpoint Method Auth Description
/health GET No Minimal liveness check
/v1/stats GET Token Artifact and lifecycle-state counts
/v1/models GET Token List all artifacts (all states)
/v1/model?name=X GET Token Get single artifact by name
/v1/model/path?name=X or ?filename=X GET Verify/admin scope Freshly verify a content-addressed locator (trusted artifacts only; exactly one selector)
/v1/model/verify?name=X POST Token Verify artifact hash (returns safe_to_use)
/v1/models/verify-all POST Token Batch integrity verification
/v1/integrity/status GET Token Last integrity check result
/v1/model/verify-manifest?name=X POST Token Verify GGUF per-tensor manifest via gguf-guard
/v1/model/acquire POST Token Register newly received artifact (state: acquired)
/v1/model/quarantine?name=X POST Token Move artifact to quarantine for scanning
/v1/model/promote POST Token Promote artifact to trusted state
/v1/model/revoke?name=X POST Token Revoke artifact (mark as untrusted)
/v1/model/delete?name=X DELETE Token Soft-delete artifact (metadata retained for audit)
/v1/audit GET Token Return hash-chained lifecycle events committed atomically with registry state

See schemas/openapi.yaml for the full specification.

Artifact lifecycle

[External source]
    |
    v
[POST /v1/model/acquire]  --> [State: acquired]
    |
    v
[POST /v1/model/quarantine] --> [State: quarantined]
    |
    v
[Quarantine pipeline: scan, verify, test]
    |
    v
[POST /v1/model/promote]  --> [State: trusted]  (service token + hash verified)
    |                              |
    |                              +---> GET /v1/model/path  (runtime consumption)
    |
    +---> [POST /v1/model/revoke]  --> [State: revoked]  (audit trail preserved)
    |
    +---> [DELETE /v1/model/delete] --> [State: deleted]  (soft-delete, metadata retained)

Key invariant: /v1/model/path only resolves trusted artifacts that have a content-addressed object. Its response binds path, sha256, size_bytes, and storage_contract: content-addressed-v1, with numeric schema_version: 1, status: verified, safe_to_use: "true", name, exact enrolled filename, and format. Duplicate, empty, ambiguous or combined name/filename selectors fail closed. Verification measures current disk bytes on every request. This is point-in-time evidence, not a lease or proof of loaded memory or GPU VRAM. Consumers must open the returned path without following links and verify the returned digest and size on the opened file immediately before parsing; a path string alone is never a trust decision.

The standalone helper currently supports regular GGUF and safetensors objects. It does not implement the main appliance's diffusion-directory storage/client contract; the main appliance embeds its own registry. This additive helper API change does not alter an already qualified main image.

Scoped deployments configure all four REGISTRY_{READ,VERIFY,PROMOTE,ADMIN}_TOKEN_PATH files with distinct credentials of at least 32 bytes, private permissions and no links. Partial, duplicate, missing or unsafe credentials stop startup; the legacy key is not a fallback once scoped configuration is selected. Read credentials authorize stats, capacity, metadata listings and admission capability reads; verify credentials authorize path resolution and integrity operations. Promote credentials authorize admission v2 capability/prepare/commit and single-model receipt lookup, plus the legacy promotion endpoint when v2 is not required. Admin credentials authorize every operation, including delete/revoke and audit administration. Read/verify credentials cannot prepare or commit admission, and a promote credential cannot delete/revoke, read admin audit or resolve runtime paths. A read credential cannot resolve a verified path.

Existing deployments may retain SERVICE_TOKEN_PATH with no scoped paths. That legacy mode gives one credential authority over all non-health operations and provides no scope separation. securectl accepts REGISTRY_ADMIN_TOKEN_PATH for scoped deployments and otherwise retains its legacy service credential. Its path output remains a JSON locator, including the new fields when present; typed parsing accepts both the legacy locator and numeric version 1 metadata.

Artifact states

State Description Path access Can promote Can revoke Can delete
acquired Downloaded/received, not yet scanned Blocked No Yes Yes
quarantined Being scanned by quarantine pipeline Blocked Yes Yes Yes
trusted All checks passed, available for runtime Allowed No Yes Yes
revoked Revoked, blocked from runtime use Blocked No No-op Yes
deleted Soft-deleted, metadata retained for audit Blocked Blocked Blocked No-op

Metadata stored per artifact

The registry stores the following metadata for each artifact:

  • name — human-readable artifact identifier
  • filename — untrusted/quarantine input filename within the registry directory
  • object_path — immutable digest-derived runtime path assigned at promotion
  • format — file format (e.g., gguf, safetensors)
  • sha256 — SHA-256 digest of the artifact file
  • size_bytes — file size in bytes
  • state — lifecycle state (acquired, quarantined, trusted, revoked, deleted)
  • promoted_at — timestamp of last state change (RFC 3339)
  • source — optional provenance URL
  • scan_results — summary of scan outcomes (pass/fail per scanner)
  • scanner_versions — versions of scanners that produced results
  • policy_version — version of the promotion policy applied
  • source_revision — Git revision of the source (if applicable)
  • gguf_guard_fingerprint — per-tensor fingerprint data (GGUF files only)
  • gguf_guard_manifest — path to gguf-guard manifest file (GGUF files only)

What is NOT stored:

  • Raw model weights are stored on disk, not in the manifest metadata
  • No user identity or account information (no user accounts exist)
  • No download IP addresses or client fingerprints
  • No usage telemetry or analytics
  • No external service tokens or credentials in the manifest

Privacy note: The registry is designed for localhost/appliance use. It does not phone home, collect telemetry, or communicate with any external service. All data remains on the local machine.

Configuration

Environment variable Default Description
BIND_ADDR 127.0.0.1:8470 HTTP listen address
REGISTRY_DIR /registry Absolute, non-symlink artifact storage directory
REGISTRY_LOCK_PATH /etc/secure-ai/policy/models.lock.yaml Baked-in fallback manifest
SERVICE_TOKEN_PATH /run/secure-ai/service-token Legacy all-service Bearer token; used only when no scope paths are selected
REGISTRY_{READ,VERIFY,PROMOTE,ADMIN}_TOKEN_PATH unset All four distinct private credential files select scoped authorization
INTEGRITY_RESULT_PATH /var/lib/secure-ai/logs/integrity-last.json Last integrity check result
ALLOW_REMOTE_BIND unset Must be exactly true before using a non-loopback BIND_ADDR; terminate TLS/mTLS in front
ALLOW_LEGACY_MANIFEST_MIGRATION unset Reserved one-time migration opt-in; validate legacy state offline before enabling

Legacy runtime-manifest migration

Writable manifests created before full event snapshots are rejected when they contain models but no replayable history. After taking an offline backup and independently reviewing the model states, scanner evidence, and expected digests, start once with ALLOW_LEGACY_MANIFEST_MIGRATION=true. The registry verifies each trusted artifact, copies it into content-addressed storage, writes bootstrap snapshots, and atomically persists the upgraded manifest. Unset the variable immediately afterward. This opt-in cannot establish the authenticity of legacy metadata; it only makes the reviewed state replayable.

Deployment profiles

Appliance mode (default)

  • Bind to 127.0.0.1 only
  • Require a service token for every non-health request
  • Use baked-in models.lock.yaml entries with valid object_path values for deterministic boot
  • Enable periodic integrity checks via systemd timer
  • No public network exposure

General standalone mode

  • Same secure defaults; a non-loopback bind additionally requires ALLOW_REMOTE_BIND=true
  • Service token remains required for every non-health API
  • Can be used outside the full appliance stack
  • Does not by itself reproduce the full appliance threat model — the appliance provides additional protections (sealed runtime, quarantine, egress controls)

securectl CLI

securectl list                     List all models (all states)
securectl info <name>              Show artifact details (JSON)
securectl verify <name>            Verify artifact hash + state
securectl path <name>              Print digest-bound locator JSON (trusted only)
securectl revoke <name>            Revoke artifact
securectl delete <name>            Soft-delete artifact (metadata retained)
securectl status                   Show registry health

Signed registry checkpoints and recovery

These checkpoint commands and their filesystem guarantees are Fedora/Linux production features. They rely on POSIX UID ownership, no-follow opens, hard links, and directory fsync; POSIX macOS is suitable for development testing, but Windows filesystems are not supported by this recovery boundary.

The registry binary can export the complete runtime manifest and replayable event history as a provider-neutral, owner-only JSON checkpoint. The checkpoint contains the exact manifest bytes, SHA-256 digest, event count and head, signed timestamp and key identity, and an Ed25519 signature. Verification uses a separately supplied public key and reruns strict manifest validation plus full event replay.

Generate a dedicated checkpoint keypair once, keep it separate from service credentials and artifact-signing keys, and stop the registry before each export. In production, keep the private key under independent restricted custody and make it available only for the stopped-writer checkpoint procedure:

install -d -m 0700 /registry/checkpoints
registry audit-keygen \
  -priv /etc/secure-ai/credentials/registry-checkpoint.key \
  -pub /etc/secure-ai/registry-checkpoint.pub

registry audit-checkpoint \
  -manifest /registry/manifest.json \
  -key /etc/secure-ai/credentials/registry-checkpoint.key \
  -output /registry/checkpoints/registry-20260802.json

registry audit-verify \
  -checkpoint /registry/checkpoints/registry-20260802.json \
  -pubkey /etc/secure-ai/registry-checkpoint.pub \
  -require-head PREVIOUSLY_RETAINED_CHAIN_HEAD

Run the exporter as the owner of both input and key. If the service uses a different or dynamic UID, stop it and stage an exact owner-only manifest copy under the export identity first. Inputs and outputs must live in trusted root/export-user-owned directories that are not group/world-writable; never relax the source or private-key mode. All checkpoint source, key, checkpoint-output, and recovery-output paths must be canonical absolute paths.

Export and recovery write and sync a randomized same-directory temporary file, publish it with an atomic no-replace hard link, remove the temporary name, and sync the parent directory. A failed pre-publication write is cleaned up and cannot leave a partial final path blocking retry. After verification, copy the checkpoint to the deployment's chosen immutable retention target and retain the reported chain head independently. Local O_EXCL protection is not WORM storage, and the signed local timestamp is not an external trusted timestamp.

Recovery requires an independently retained head to occur in the checkpoint and only writes a new manifest path:

registry audit-recover \
  -checkpoint /recovery/registry-20260802.json \
  -pubkey /etc/secure-ai/registry-checkpoint.pub \
  -require-head INDEPENDENTLY_RETAINED_CHAIN_HEAD \
  -output /recovery/new-registry/manifest.json

Restore the matching content-addressed objects/ tree into that fresh registry directory, let normal startup verify the recovered manifest, and run authenticated /v1/models/verify-all before switching consumers. A checkpoint intentionally does not embed model bytes.

Hardening

The systemd unit includes:

  • DynamicUser=yes — no persistent system user
  • LoadCredential= — copies the root-owned service token into the unit's private credential mount
  • ProtectSystem=strict — read-only filesystem except registry directory
  • Loopback-only TCP binding with restricted address families
  • MemoryDenyWriteExecute=yes — no JIT or writable-executable pages
  • SystemCallFilter — strict seccomp allowlist
  • CapabilityBoundingSet= — no capabilities
  • Resource limits: 512M memory, 50% CPU, 64 tasks

Provision the unit credential from a root-only source; do not create a shared runtime file for the dynamic user:

sudo install -d -o root -g root -m 0700 /etc/secure-ai/credentials
sudo install -o root -g root -m 0400 /path/to/generated-token \
  /etc/secure-ai/credentials/ai-model-registry.service-token
sudo systemctl daemon-reload
sudo systemctl restart ai-model-registry.service

See deploy/seccomp/ai-model-registry.json for the seccomp profile.

Releases accept only annotated vMAJOR.MINOR.PATCH SemVer tags (including valid SemVer prerelease/build suffixes). The release workflow verifies that the peeled tag target matches the event commit and is reachable from origin/main, then reruns the security gates before any job receives publishing permissions. Container releases publish only two immutable tags: the exact release SemVer (mapping its + delimiter to _, which cannot occur in SemVer) and a full commit-SHA tag. Moving major/minor and latest tags are intentionally omitted.

Security considerations

This registry preserves the following trust invariants:

  • No implicit trust on upload — promotion is policy-gated and requires authenticated requests
  • State isolation — runtime can only consume trusted artifacts; all other states are blocked
  • Deterministic verification — SHA-256 hash drives all admission and verification decisions
  • No insecure dev mode in production — INSECURE_DEV_MODE must be explicitly set; default is fail-closed
  • Human-auditable decisions — all state transitions are logged with artifact details
  • Atomic durable state — bounded manifests are strictly parsed, synced to a private temporary file, and atomically renamed
  • Fully replayed lifecycle history — complete event snapshots are hash chained, transition checked, and required to reconstruct manifest state exactly
  • Immutable runtime objects — promotion publishes with no-replace semantics into digest-derived paths; resolution returns the digest contract, not a bare trusted path
  • Filesystem containment — only local relative paths are accepted; escapes, symlinks, hard links, and files changing while hashed are rejected
  • Soft delete — deleted artifacts retain metadata for audit trail; only the file is removed from disk
  • Portable audit checkpoints — signed manifest exports bind exact bytes to the replayed event head; anchored recovery can create only a new path

Appendix: Development-only mode

WARNING: Never use INSECURE_DEV_MODE in production. It bypasses authentication for every non-health API and is only suitable for isolated loopback development.

For local development and testing only, you can bypass token authentication:

# DEVELOPMENT ONLY — do not use in production
mkdir -p /tmp/registry
REGISTRY_DIR=/tmp/registry INSECURE_DEV_MODE=true ./registry

When INSECURE_DEV_MODE=true is set:

  • All non-health endpoints accept requests without a Bearer token
  • A warning is logged on startup: INSECURE_DEV_MODE=true — auth will not be enforced
  • The /health endpoint remains a minimal unauthenticated liveness response

When INSECURE_DEV_MODE is not set (the default):

  • Startup fails if the token is missing, empty, weak, oversized, or a symbolic link
  • Every non-health endpoint requires a valid Bearer token
  • This is the correct and expected behavior for any non-development deployment

WARNING: Never set INSECURE_DEV_MODE=true in container images, systemd units, or any deployment configuration. The service is designed to fail closed — without authentication, no non-health registry API is available. This protects artifact metadata, paths, verification results, and lifecycle operations.

See SECURITY.md, THREAT_MODEL.md, and the security audit before production deployment.

License

Apache-2.0

Manifest capacity and safety transitions

Admission reserves enough manifest bytes and audit events to revoke and delete every live model. Authenticated GET /v1/capacity reports limits, safety reserve and maintenance status; an ordinary mutation that consumes the reserve returns HTTP 507. Existing history is retained. See capacity and recovery for legacy full-manifest limitations, persistence faults, and evidence-preserving checkpoint maintenance. Checkpoint export does not reclaim live manifest space.

About

Security-first AI artifact registry — digest-based storage, policy-gated promotion, signed provenance, and reproducible trust metadata for local models, adapters, and tokenizers

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages