Explanation

Security Model

How Evidence Packs use cryptographic digests and Sigstore signatures to provide security guarantees.

Core Security Principles

epack is designed with security as a primary concern. The tool handles sensitive compliance evidence and must protect both the integrity of packs and the credentials used during collection.

Design Principles

  • Cryptographic Integrity: SHA-256 digests for all artifacts
  • Provenance Verification: Sigstore signatures with identity binding
  • Least Privilege: Only specified secrets passed to collectors
  • Defense in Depth: Multiple validation layers
  • Fail Secure: Default to strictest verification mode

Integrity Verification

Every Evidence Pack includes cryptographic integrity guarantees through artifact digests. Run epack verify --integrity-only to check digests without requiring signatures.

How artifact digests work

Each artifact's SHA-256 hash is computed and recorded in the manifest when you run epack build. During epack verify, the hash is recomputed and compared. Any modification to the artifact will produce a different hash, causing verification to fail.

Terminal
# Verify integrity only (no signature required)
$ epack verify --integrity-only evidence.pack

✓ Pack integrity verified
  Pack digest: sha256:a3f2b8c9d4e5f6...
  Artifacts: 12 verified
  No attestations present
manifest.json (excerpt)
{
  "artifacts": [
    {
      "path": "artifacts/soc2-report.pdf",
      "digest": "sha256:a3f2b8c9d4e5f6...",
      "size": 1048576
    }
  ]
}

Pack digest

The pack_digest is a hash of all artifact paths and their individual digests, providing a single value that covers the entire pack contents. This is what gets signed, so a single signature authenticates all artifacts. View it with epack inspect:

$ epack inspect evidence.pack --format json | jq .pack_digest
"sha256:a3f2b8c9d4e5f6a7b8c9d0e1f2..."

What integrity verification detects

  • Any modification to artifact content
  • Addition or removal of artifacts
  • Renaming artifacts
  • Manifest tampering (causes pack_digest mismatch)

Sigstore Signing

epack uses Sigstore for signing via epack sign. Keyless signatures are tied to OIDC identities, eliminating key management while providing strong provenance guarantees.

Terminal
# Sign a pack (opens browser for authentication)
$ epack sign evidence.pack

Opening browser for authentication...
Authenticated as: security@vendor.com (Google)

✓ Pack signed successfully
  Signer: security@vendor.com
  Issuer: https://accounts.google.com
  Rekor entry: https://rekor.sigstore.dev/api/v1/log/entries/...

How Sigstore signing works

  1. User authenticates via OIDC (Google, GitHub, Microsoft, etc.)
  2. Fulcio issues a short-lived certificate (~10 minutes)
  3. The certificate embeds the authenticated identity
  4. The manifest digest is signed with the temporary key
  5. The signature is recorded in the Rekor transparency log
  6. The complete bundle is stored in the pack

What's in an attestation bundle

Each attestation stored in the pack includes:

  • The digital signature over the manifest digest
  • The signing certificate with identity claims
  • The certificate chain to Fulcio root
  • The Rekor transparency log entry
  • Timestamp (optional, for long-term verification)

View attestation details with epack inspect:

$ epack inspect evidence.pack --attestations

Attestations (1):
  [0] Sigstore Bundle
      Subject: security@vendor.com
      Issuer:  https://accounts.google.com
      Signed:  2024-02-15T10:30:00Z
      Rekor:   24296fb24b8ad77a...

CI/CD signing with OIDC

In GitHub Actions, grant id-token: write and run epack sign; epack mints the Sigstore OIDC token automatically. Other CI systems can still pass EPACK_OIDC_TOKEN explicitly. The Sigstore certificate embeds additional claims about the source:

GitHub Actions
permissions:
  id-token: write

- name: Sign pack
  run: epack sign evidence.pack

The certificate includes:

  • The commit SHA that triggered the workflow
  • The repository URL
  • The workflow path
  • The ref (branch or tag)

This means the signature proves exactly which code generated the evidence. Verifiers can check the exact immutable version that ran.

Signing with a key

A run with nobody at the keyboard can sign with a key instead: epack sign --key or epack run --key. The attestation then carries the public key rather than a certificate, so the signature names no person. It is still recorded in the Rekor transparency log unless you sign with epack sign --no-tlog. A verifier checks it against the signer's public key, and the key that verifies names the signer by the SHA-256 of its public key:

epack verify evidence.epack --public-key signer.pem

epack key create makes a key for the machine, keeps the private half in ~/.epack/keys/ readable only by you, and registers the public half with the remote, which can hold it until someone approves it.

Configurations from a Remote

epack remote clone and epack run <name> fetch a project a remote wrote: its epack.yaml, hooks, and lockfile. That makes the remote an author of what runs on your machine, so epack holds a fetched configuration to these rules:

  • Files are written only inside the folder you named, never into .epack/collectors/, .epack/remotes/, or .git/
  • Every binary the lockfile names is downloaded through the registry and checked against its recorded Sigstore identity before it runs
  • A hook script that still matches the delivered template never runs, so a fetched configuration cannot run shell on your machine
  • Files you edited are never overwritten without --force
  • After a fetch that changed something, epack prints each collector, tool, and remote it will run with its publisher and version, and the environment variables it reads. A variable counts as set when the environment or your keychain holds it for that configuration
  • Collectors, tools, and remote adapters may come only from publishers you trust. A publisher is the GitHub owner of a component's source repository
  • A run by name checks the revision it fetched before installing anything. Trust in a new publisher is offered only for that revision, never for an earlier one the run falls back to
  • A job with no folder signs in for its configuration only through an adapter from a publisher it trusts, checked before the adapter is installed or run

In a terminal, epack asks once for each new publisher and records your answer in ~/.epack/config.yaml. Signing in to a remote with epack remote login trusts its adapter's publisher. Without a terminal, the run stops before any download unless EPACK_TRUSTED_PUBLISHERS or --trust-publisher names the publisher, and --yes never widens trust. A run by name runs the newest earlier revision that needs no new publisher instead, when the remote has one. Folders without a fetch record, such as a committed repository, are not affected.

The collectors, tools, and secrets a remote names remain your trust decision. Treat admin access on a remote you sign in to accordingly.

Threat Model

In scope (what epack protects against)

  • Tampered packs: Detecting modifications to artifacts or manifest
  • Forged signatures: Rejecting invalid or unauthorized attestations
  • Malicious collectors: Preventing arbitrary code execution from untrusted sources (via lockfile verification)
  • Secret leakage: Protecting credentials from exposure in logs or errors
  • Path traversal: Preventing extraction outside target directory

Out of scope

  • Physical access to the machine running epack
  • Compromise of your OIDC identity provider
  • Compromise of Sigstore infrastructure
  • Side-channel attacks during cryptographic operations

Security non-goals

  • Validating the accuracy of evidence (only integrity)
  • Preventing authorized users from creating misleading packs
  • Sandboxing collectors beyond environment isolation

Two Editions, Different Security Profiles

epack comes in two editions with different security profiles:

epack-core Minimal Attack Surface
  • No subprocess execution
  • No binary downloads
  • No network requests (except Sigstore)
  • Read/write local files only
  • Ideal for verification pipelines
epack (full) Extended Capabilities
  • Downloads collector binaries
  • Executes collectors as subprocesses
  • Manages credentials via environment
  • Requires lockfile verification
  • Use --frozen mode in CI
Recommendation: Use epack-core for verification-only workflows. Use the full edition only when automated collection is required.

Collector Security

The full edition downloads and executes collector binaries, which requires careful security controls.

Binary verification

  • Collectors are downloaded from GitHub releases
  • SHA-256 digest is recorded in lockfile
  • Digest is verified before execution
  • Signer identity is verified via Sigstore

Environment isolation

  • Only explicitly listed secrets are passed to collectors
  • An entry marked optional is passed through only when set, and the run never requires it
  • Reserved environment prefixes are blocked (EPACK_, LD_, etc.)
  • Collectors cannot access other environment variables

File artifact containment

  • Each collector gets a private staging directory for file artifacts
  • File artifact paths must be relative and stay beneath that staging directory
  • Symlinks and non-regular files are rejected before bytes enter the pack
  • Staged file bytes count toward artifact and pack size limits

Lockfile security

The lockfile (epack.lock.yaml) provides reproducible builds by pinning exact versions, platform-specific binary digests, and signer identities. Each locked collector, tool, and remote also records a verification status: verified when its signature bundle was checked at lock time, or skipped for external binaries that have no bundle to verify.

Important: Always use --frozen mode in CI/CD to ensure lockfile verification. This prevents unexpected changes to collector binaries.

Secret Management

Configuration

Secrets and managed credentials are specified in epack.yaml by reference only. Values come from environment variables or from the Locktivity-managed broker, which resolves short-lived env bundles at runtime.

epack.yaml
credential_sets:
  github_repo: credset_abc123

collectors:
  github:
    source: locktivity/epack-collector-github@^0.2.0
    credentials:
      - github_repo  # Broker resolves a short-lived GITHUB_TOKEN
    # Or for manual setups:
    # secrets:
    #   - GITHUB_TOKEN

Secret protection

  • Redaction: Collector and adapter output in error messages has every passed secret value replaced, then secret-looking patterns redacted. Values shorter than six characters are not matched
  • Process isolation: On Unix, collectors and adapters run in their own process group, and a timeout kills the group, so nothing a collector started outlives the run with its environment
  • Secret scanning: The build command warns about potential secrets in artifacts
  • Minimal exposure: Only listed secrets or resolved credential bundles are passed to components
  • OIDC isolation: Raw GitHub OIDC request variables are blocked from explicit pass-through to untrusted components

Stored secrets

On a laptop, epack secret set keeps a listed variable in the operating system keychain, scoped to one configuration on one remote. A run reads the environment first and the keychain second; CI never reads the keychain. What the keychain protects is encryption at rest and isolation from other user accounts, not isolation from code running as the same user: on every platform a same-user process can read what epack stored, as it can read an exported variable.

Each stored secret is approved for the components that read it, as they were when it was stored, with a digest of each reader's entry in epack.yaml. A revision that changes a reader's entry, or adds a reader, is shown and must be approved in epack secret approve or epack secret setup before the value is used again. epack run and epack run --check never prompt and stop with the command instead. Storing or approving one secret never approves another. The keychain is consulted only after the publisher trust gate, and the lookup behind a fetch summary never prompts.

What epack records about stored secrets, names, readers, and times but never values, lives in ~/.epack/secrets.yaml, readable only by the user. Values set in the environment stay outside the approval guarantee; the check shows them with the source env.

Vulnerability Reporting

If you discover a security vulnerability in epack, please report it responsibly. Do not open public GitHub issues for security vulnerabilities.

Report security issues to: security@locktivity.com

See Also