Digital Preservation Security Policies: Enforcing Trust as Policy-as-Code

Security in digital preservation is not an ancillary concern but a foundational architectural requirement. Within an OAIS-Compliant Digital Preservation Architecture, security policies must be engineered to protect the authenticity, integrity, and confidentiality of information packages across their entire lifecycle. This page sits alongside the code-driven OAIS Reference Model implementation that structures the pipeline and the long-term storage architecture it ultimately hardens — its job is to define how every ingest, migration, and access event is authorized, verified, and recorded. For archivists, digital preservation specialists, and Python automation engineers, that means moving beyond perimeter defenses toward continuous, automated compliance validation: translating institutional policy mandates into executable code so that every preservation action is cryptographically verified, schema-validated, and fully auditable.

Policy Domains and Control Specification

A defensible security posture begins by decomposing “security” into discrete, testable control domains, each with an explicit owner, enforcement point, and audit obligation. Treating these as configuration rather than prose is what makes them enforceable: a policy that lives only in a PDF cannot halt a non-compliant transfer, but a validated policy object evaluated at each pipeline stage can. The domains below map directly onto OAIS functional entities and become the fields of the policy model that follows.

Control domain Enforcement point OAIS entity Primary compliance driver
SIP admission control Ingest gateway Ingest ISO 16363 4.1.5 (SIP validation)
Format allow-listing Characterization Ingest / Preservation Planning ISO 16363 4.2.3 (format monitoring)
Fixity enforcement AIP write / periodic sweep Archival Storage ISO 16363 4.2.4 (integrity)
Access authorization Access API Access ISO 16363 4.6.2 (access conditions)
Immutable audit logging All entities Administration ISO 16363 4.1.8 (traceability)
Key & credential custody Cross-cutting Administration ISO 16363 5.1.2 (security architecture)

Encoding these domains as a validated data structure lets every worker in the pipeline load the same policy and evaluate it deterministically. The Pydantic model below is the canonical representation — it is parsed once at startup, version-pinned, and passed to each enforcement point. Field-level constraints reject a malformed policy before a single package is touched, which itself is a security property.

python
"""preservation_policy.py — canonical, validated security policy object."""
from __future__ import annotations

import logging
from enum import Enum
from typing import Literal

from pydantic import BaseModel, Field, field_validator

logger = logging.getLogger("preservation.policy")


class Sensitivity(str, Enum):
    PUBLIC = "public"
    RESTRICTED = "restricted"
    EMBARGOED = "embargoed"


class PreservationPolicy(BaseModel):
    """Version-pinned security policy loaded at pipeline startup."""

    policy_version: str = Field(pattern=r"^\d+\.\d+\.\d+$")
    fixity_algorithm: Literal["sha256", "sha512", "blake2b"] = "sha256"
    allowed_puids: frozenset[str] = Field(min_length=1)
    max_sip_bytes: int = Field(gt=0, le=5 * 1024**4)  # 5 TiB ceiling
    require_mutual_tls: bool = True
    default_sensitivity: Sensitivity = Sensitivity.RESTRICTED
    audit_retention_days: int = Field(ge=3650)  # >= 10 years

    @field_validator("allowed_puids")
    @classmethod
    def _puids_well_formed(cls, value: frozenset[str]) -> frozenset[str]:
        for puid in value:
            if "/" not in puid:
                raise ValueError(f"malformed PRONOM PUID: {puid!r}")
        return value


def load_policy(raw: dict) -> PreservationPolicy:
    """Parse and validate a policy document; refuse to run on failure."""
    policy = PreservationPolicy.model_validate(raw)
    logger.info(
        "policy_loaded", extra={"policy_version": policy.policy_version,
                                "puid_count": len(policy.allowed_puids)}
    )
    return policy
Policy control domains mapped to OAIS functional entities One version-pinned PreservationPolicy object feeds three OAIS entity columns — Ingest (SIP admission control at the ingest gateway; format allow-listing at characterization), Archival Storage (fixity enforcement at AIP write and periodic sweep) and Access (access authorization at the access API) — each domain tagged with its ISO 16363 clause. A cross-cutting Administration band below holds immutable audit logging and key and credential custody, and every enforcement decision emits a PREMIS audit event into it. PreservationPolicy — validated once at startup, version-pinned the same object is evaluated at every enforcement point below Ingest Archival Storage Access SIP admission control ingest gateway · ISO 16363 4.1.5 Format allow-listing characterization · 4.2.3 Fixity enforcement AIP write / periodic sweep · 4.2.4 Access authorization access API · 4.6.2 · · · every enforcement decision emits a PREMIS audit event ADMINISTRATION · CROSS-CUTTING Immutable audit logging all entities · ISO 16363 4.1.8 Key & credential custody cross-cutting · 5.1.2

Lifecycle Enforcement and Policy-as-Code

The OAIS functional model dictates that security controls permeate every entity, from Ingest to Access, so the validated policy object must be applied at each stage rather than at a single perimeter. Ingest workflows enforce strict validation against submission information package (SIP) schemas before objects enter the archival domain, mirroring the batch validation schemas used upstream in the scanning pipeline. Automation pipelines, typically orchestrated in Python, integrate policy-as-code checks that halt non-compliant transfers, quarantine malformed packages, and trigger automated remediation. This programmatic enforcement eliminates human discretion at the boundary and ensures that preservation actions align with institutional risk thresholds and regulatory requirements.

During the characterization phase, security policies intersect with format registry integration and signature-based preservation format identification to validate file signatures against approved preservation formats. Any deviation from the institutional format allow-list — the allowed_puids set on the policy object — triggers an immediate policy exception, routing the package to a secure sandbox for forensic analysis. By treating format validation as a security boundary rather than merely a preservation-planning input, institutions prevent the ingestion of malicious payloads disguised as archival content, such as a polyglot file that presents a benign TIFF header while carrying an executable tail.

Cryptographic Integrity and Metadata Validation

Metadata security forms the backbone of long-term trust. When executing PREMIS metadata mapping, preservation engineers must treat rights, provenance, and fixity data as first-class security assets. PREMIS event and agent records require strict schema validation to prevent tampering or unauthorized modification, and each fixity check must itself be emitted as a PREMIS event so the integrity record is as auditable as the object it protects. Python validation routines parse the XML or JSON-LD representation against the official PREMIS Data Dictionary and its schema, flagging structural anomalies or missing checksums. By binding security policies to metadata structures, institutions guarantee that every preservation action is traceable, verifiable, and legally defensible across decades of technological change.

The following production-ready pattern demonstrates an automated fixity validation and audit-logging routine. It enforces policy compliance by verifying digests against a trusted manifest — using whichever algorithm the loaded PreservationPolicy mandates — and records immutable audit events:

python
import hashlib
import json
import logging
from datetime import datetime, timezone
from pathlib import Path

from preservation_policy import PreservationPolicy

# Configure structured audit logging for compliance tracking
logging.basicConfig(
    filename="preservation_audit.log",
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(message)s",
)
logger = logging.getLogger("preservation.fixity")


def compute_fixity(filepath: Path, algorithm: str = "sha256") -> str:
    """Compute a cryptographic digest for a preservation object."""
    hasher = hashlib.new(algorithm)
    with filepath.open("rb") as handle:
        while chunk := handle.read(8192):
            hasher.update(chunk)
    return hasher.hexdigest()


def validate_ingest_policy(
    package_dir: str, manifest: dict, policy: PreservationPolicy
) -> bool:
    """Enforce ingest security policy against a trusted manifest."""
    audit_event = {
        "event_type": "fixity_validation",
        "policy_version": policy.policy_version,
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "package_id": manifest.get("package_id"),
        "status": "PASSED",
    }

    for file_record in manifest.get("files", []):
        target_path = Path(package_dir) / file_record["path"]
        if not target_path.exists():
            audit_event.update(status="FAILED", reason=f"missing:{target_path}")
            logger.error(json.dumps(audit_event))
            return False

        computed = compute_fixity(target_path, policy.fixity_algorithm)
        if computed != file_record["expected_digest"]:
            audit_event.update(status="FAILED", reason=f"mismatch:{target_path}")
            logger.error(json.dumps(audit_event))
            return False

    logger.info(json.dumps(audit_event))
    return True

The digest comparison is the cryptographic core of the whole policy. For an object with byte content m, the manifest asserts H(m) = d; admission requires the recomputed digest equal the trusted value:

$$H(m) \stackrel{?}{=} d, \qquad H \in {\text{SHA-256}, \text{SHA-512}, \text{BLAKE2b}}$$

A single differing bit yields a fully divergent digest, so any silent corruption, truncation, or substitution between the producer’s manifest and the ingest sandbox is caught before the object is promoted to an archival information package.

Granular Access Control and Zero-Trust Enforcement

The following diagram shows how layered security controls gate every request before it reaches the AIP store, each layer rejecting non-compliant access.

Zero-trust layered access gate An access request flows left to right through four ordered gates — identity and authentication over mutual TLS, RBAC authorization for least-privilege role grants, fixity verification that the recomputed digest equals the manifest value, and immutable append-only audit logging — before it is admitted to the AIP store. Any failure at the authentication, authorization, or fixity gate breaks out of the pipeline down to a single Deny and log terminal, so the store is reached only when every layer passes. fail fail fail Access request 1 Authentication mutual TLS · identity 2 Authorization RBAC · least privilege 3 Fixity check digest = manifest 4 Audit logging immutable · append-only AIP store admitted Deny & log rejected request recorded

Each layer (identity, authorization, fixity, audit) must pass before access to the archival store is granted; any failure routes to a logged denial.

Access control mechanisms must be granular, auditable, and dynamically enforced. Implementing role-based access control for digital archives maps institutional roles to precise preservation functions: archivists, researchers, and system administrators operate within distinct privilege boundaries enforced at the API, storage, and application layers. Automated provisioning scripts synchronize directory services with preservation-system permissions so that grants and revocations propagate instantly across the ecosystem rather than lingering as orphaned entitlements.

Modern preservation environments cannot rely on implicit network trust. A zero-trust architecture mandates continuous verification of every request, regardless of origin. Policy engines evaluate access requests against contextual attributes — data sensitivity, embargo status, and authentication strength — which is exactly why Sensitivity and require_mutual_tls live on the policy object. Mutual TLS, short-lived tokens, and cryptographic request signing ensure that even a compromised credential cannot traverse lateral paths within the archival infrastructure.

Integration Points

Security policies are not a standalone stage; they intercept data as it crosses between pipeline entities, which makes their integration contracts explicit rather than incidental. Upstream, the admission control domain consumes the same JSON Schema profiles enforced by batch validation schemas, so a package that failed structural validation during scanning never reaches the ingest gateway. When a policy check does fail, it hands the package to the error handling and retry logic subsystem, which decides between quarantine, retry, and dead-letter routing based on the failure class recorded in the audit event.

Downstream, every enforcement decision emits a PREMIS event via the PREMIS metadata mapping layer, and the resulting audit records are written to the same immutable tier described in the long-term storage architecture. Fixity policy in particular is bidirectional: the storage layer’s periodic integrity sweeps re-invoke the same compute_fixity routine used at ingest, guaranteeing that the algorithm and manifest semantics never drift between write-time and audit-time verification.

Validation and Compliance Rules

Each control domain must satisfy a specific, testable obligation drawn from the OAIS reference model and ISO 16363. Expressing these as a lookup table lets an automated conformance test assert coverage — an empty PREMIS event type or an unmapped ISO clause becomes a failing test rather than an unnoticed gap.

Policy control PREMIS event type Required outcome record ISO 16363 clause
SIP admission validation SIP schema pass/fail + validator agent 4.1.5
Format allow-listing format identification PRONOM PUID + registry version 4.2.3
Fixity at write message digest calculation algorithm + digest + timestamp 4.2.4
Periodic fixity sweep fixity check prior vs. current digest + verdict 4.2.4
Access grant dissemination requesting agent + authorization basis 4.6.2
Access denial deletion/decryption (as applicable) denial reason + evaluated attributes 4.6.2

The binding rule is absolute: no preservation action may complete without emitting its corresponding event. A fixity check that runs but is not logged is, for audit purposes, a check that never happened — the immutable audit trail, not the in-memory result, is the object of record for accreditation.

Troubleshooting Reference

The failures below are the ones that most often surface in production security enforcement, together with their root cause and the concrete remediation that restores a compliant state.

Error condition Root cause Remediation
ValidationError on policy load Malformed or unpinned policy document Reject startup; pin policy_version; fail closed until a valid policy parses
Fixity mismatch at ingest Corruption or substitution in transit Quarantine package, notify producer, request re-transmission against original manifest
Format outside allowed_puids Unregistered or spoofed format Route to forensic sandbox; update registry via format registry integration before any allow-list change
Audit log write failure Storage tier unavailable or full Halt the pipeline (fail closed) — never proceed with an unrecorded action; page on-call
mTLS handshake rejected Expired or revoked client certificate Rotate short-lived credentials; verify certificate chain; confirm clock skew under tolerance
Orphaned access grant after role change Provisioning not synchronized Re-run directory sync; reconcile entitlements against source-of-truth roles

Resilience, Synchronization, and Future-Proofing

Security extends beyond active workflows into the foundational infrastructure. A robust storage tier must incorporate immutable object storage, write-once-read-many (WORM) configurations, and geographically distributed replication, as detailed in the best practices for cold storage tiering. Automated reconciliation scripts detect manifest drift, quarantine divergent copies, and trigger secure re-synchronization without manual intervention.

Disaster recovery planning must be treated as a continuous security posture rather than a reactive measure. Air-gapped backups, cryptographically sealed recovery manifests, and automated failover orchestration are baseline requirements. As computational threats evolve, institutions should proactively evaluate lattice-based or hash-based signature schemes so that today’s fixity validations remain cryptographically binding against future adversarial capabilities — a migration best sequenced now, while the audit trail linking legacy and post-quantum digests can still be established with full provenance.

Conclusion

Digital preservation security policies are executable contracts between institutional mandates and technical infrastructure. By embedding cryptographic verification, schema validation, and zero-trust principles directly into Python automation pipelines, cultural heritage teams transform abstract compliance requirements into continuous, auditable operations. As preservation architectures scale, the integration of policy-as-code, immutable audit trails, and forward-looking cryptographic standards remains the definitive measure of institutional trust and archival resilience.