Skip to content

Signed compliance reports

Audience: DevOps (signing keys, CI verify) and GRC (tamper evidence).

When you are done: You can generate, sign, verify, and archive compliance JSON reports for a completed run.

Reports are a commercial capability registered as report_renderer.json. The public engine's community fallback emits unsigned JSON — see Extending PrivaCI.

GRC workflow (evidence filing): Compliance evidence mapping.


What you get

Output Format Signed?
Full compliance JSON privaci report --format json Yes, when signing key set
Markdown summary sidecar PRIVACI_REPORT_SUMMARY_MD or summary-md Sidecar is derived from signed payload
Unsigned JSON Same command, no signing key No

Report body includes:

  • summary — verdict, coverage, schema object dispositions, attention register (commercial)
  • run — row from _privaci.runs
  • audit_events — drill-down trail

summary.schema_objects lists non-table catalog dispositions from the engine audit log:

List Audit event Meaning
created created_object View/function DDL replicated onto the target
definition_only definition_only_object Materialized-view shell (contents_copied: false)
skipped skipped_object Object deliberately not migrated (reason token)

attention_required.skipped_objects mirrors schema_objects.skipped for backward compatibility. Materialized views never imply a byte copy unless the engine payload explicitly sets contents_copied: true.

Data is read from the target database (TARGET_DB_URL) — see public state schema.


Configuration

Variable Required Description
TARGET_DB_URL Yes Target Postgres holding _privaci state
PRIVACI_REPORT_SIGNING_KEY_PEM For signing Ed25519 private key PEM
PRIVACI_REPORT_VERIFY_PUBLIC_KEY_PEM For verify Ed25519 public key PEM (auditors/CI)
PRIVACI_OPERATOR_ID No Operator identity in summary.provenance
PRIVACI_REPORT_SUMMARY_MD No Write Markdown sidecar when rendering JSON

Legacy alias: PRIVACI_REPORT_SIGNING_KEY (same as _PEM).


Quickstart

1. Generate a signing keypair (once per environment)

openssl genpkey -algorithm Ed25519 -out report-signing.pem
openssl pkey -in report-signing.pem -pubout -out report-signing.pub.pem

On macOS, the system openssl is often LibreSSL, which may fail with Algorithm ED25519 not found. Install OpenSSL 3 via Homebrew and call that binary explicitly:

brew install openssl@3
"$(brew --prefix openssl@3)/bin/openssl" genpkey -algorithm Ed25519 \
  -out report-signing.pem
"$(brew --prefix openssl@3)/bin/openssl" pkey -in report-signing.pem \
  -pubout -out report-signing.pub.pem

Store report-signing.pem in your secrets manager. Distribute report-signing.pub.pem to auditors and CI verify jobs.

2. Run masking (public engine CLI inside the container)

After a successful privaci run batch job, note the run UUID from stdout.

export SOURCE_DB_URL='postgresql://…/source'
export TARGET_DB_URL='postgresql://…/target'
export ANONYMIZATION_SALT='…'   # 64 hex chars — see public configuration

docker run --rm --read-only --tmpfs /tmp \
  -e SOURCE_DB_URL -e TARGET_DB_URL -e ANONYMIZATION_SALT \
  -e AWS_REGION='us-east-1' \
  -v "$(pwd)/mask-rules.yaml:/config/mask-rules.yaml:ro" \
  <marketplace-image-uri>:<tag> \
  run --config /config/mask-rules.yaml

CLI reference: public privaci run. Deployment patterns: public Deployment.

3. Render signed report

Run a second one-shot container (or the same image in your orchestrator) with the signing key injected from Secrets Manager:

docker run --rm --read-only --tmpfs /tmp \
  -e TARGET_DB_URL='postgresql://…/target' \
  -e PRIVACI_REPORT_SIGNING_KEY_PEM="$(aws secretsmanager get-secret-value )" \
  -e PRIVACI_OPERATOR_ID='ci-pipeline-42' \
  -e PRIVACI_REPORT_SUMMARY_MD='/tmp/run-summary.md' \
  <marketplace-image-uri>:<tag> \
  report --run <run-uuid> --format json --output /tmp/report.json

Signed envelope shape:

{
  "algorithm": "ed25519",
  "payload": { "report_version": "1", "run_id": "…", "summary": {  } },
  "signature": "<base64>"
}

Without a signing key, output is unsigned canonical JSON (still valid for dev).

4. Verify (CI or auditor workstation)

export PRIVACI_REPORT_VERIFY_PUBLIC_KEY_PEM="$(cat report-signing.pub.pem)"

python - <<'PY'
from pathlib import Path
from privaci_commercial.report_verify import verify_report_file

payload = verify_report_file(
    Path("report.json"),
    public_key_pem=Path("report-signing.pub.pem").read_bytes(),
)
print("verdict:", payload["summary"]["verdict"]["status"])
PY

Or set PRIVACI_REPORT_VERIFY_PUBLIC_KEY_PEM and use verify_report_bytes() in your pipeline.

Verification confirms:

  • Valid UTF-8 JSON
  • Ed25519 signature matches canonical payload bytes
  • Byte-for-byte reproducible re-serialization

Markdown summary export

Human-readable one-screen summary for reviewers:

# Sidecar on JSON render (env var above), or programmatically:
python - <<'PY'
from uuid import UUID
from privaci_commercial.reports import SignedJsonReportRenderer

md = SignedJsonReportRenderer().render(
    UUID("019ed1bf-97be-730a-8bb0-e019cea366c9"),
    output_format="summary-md",
)
print(md.decode())
PY

CI/CD integration

- name: Generate signing key (ephemeral — dev/CI pattern)
  run: |
    openssl genpkey -algorithm Ed25519 -out /tmp/sign.pem
    echo "PRIVACI_REPORT_SIGNING_KEY_PEM<<EOF" >> "$GITHUB_ENV"
    cat /tmp/sign.pem >> "$GITHUB_ENV"
    echo "EOF" >> "$GITHUB_ENV"

- name: Render and verify report
  env:
    TARGET_DB_URL: ${{ secrets.TARGET_DB_URL }}
  run: |
    privaci report --run "${RUN_ID}" --format json -o report.json
    python -c "
    from pathlib import Path
    from privaci_commercial.report_verify import verify_report_file
    verify_report_file(Path('report.json'), public_key_pem=Path('/tmp/sign.pem').read_bytes())
    "

Production: use a stable key from secrets manager, not ephemeral CI keys.


Security notes

  • Never commit private signing keys or log report payloads containing PII.
  • PRIVACI_OPERATOR_ID should identify a role or pipeline — not a person's email.
  • Public key rotation: keep overlap period where both keys verify; document in your key-management runbook.

Troubleshooting

Symptom Cause Fix
CommercialConfigError on signing key Malformed PEM or non-Ed25519 key Regenerate with openssl genpkey -algorithm Ed25519 (Homebrew OpenSSL 3 on macOS — see above)
Algorithm ED25519 not found macOS LibreSSL lacks Ed25519 in genpkey Use Homebrew OpenSSL 3 — keygen
Unsigned output PRIVACI_REPORT_SIGNING_KEY_PEM unset Set key or accept unsigned for dev
Verify fails Report edited after signing Re-render; investigate tampering
TARGET_DB_URL is required Env not set for report Export target DSN
Empty audit_events Run failed or audit disabled Check run status; see public configuration — audit_log

FAQ

openssl genpkey fails with Algorithm ED25519 not found on a Mac?
Yes — stock macOS LibreSSL often cannot mint Ed25519 keys. Use Homebrew OpenSSL 3 (brew install openssl@3) and the $(brew --prefix openssl@3)/bin/openssl commands in Generate a signing keypair.

Does verify need the private key?
No. Auditors use the public key only.

Relationship to privaci verify?
Public privaci verify checks masking quality (value-free stats). Report verify checks document integrity (signature). Use both.

PDF reports?
Use --format pdf (commercial layer). The PDF is a human-readable view of the same signed JSON summary; verify integrity on the JSON companion file with Ed25519 — not on PDF bytes. Optional sidecar when rendering JSON:

export PRIVACI_REPORT_SUMMARY_PDF=/evidence/run-summary.pdf
privaci report --run <uuid> --format json --output report.json