The rule I wanted to question

There's a rule most ingestion pipelines follow, and it's usually right: land the raw input first.

Store what arrived, byte for byte, before you interpret it. Transform and route from a durable copy, not from a request in flight. It's a rule I've followed without much argument for most of my career, because the reasons for it are good ones.

This piece is about a case where that rule deserves an exception โ€” and a small system built to test whether the exception holds, or just sounds plausible. It picks up where the first article in this series left off: that piece asked whether pre-persistence structural inspection was affordable; this one asks whether a pre-persistence policy boundary can be trusted.

The working thesis:

For sufficiently sensitive input, the safest place to apply policy may be before the system intentionally commits the input to durable storage.

That's deliberately narrower than "no infrastructure ever touches the source bytes." The gateway itself receives them, and so does whatever ingress sits in front of it. The claim is about what happens next: whether the system deliberately constructs a request carrying the source representation into durable, application-controlled storage, or whether policy runs first so only a transformed representation is ever offered to that storage. I wanted to find out whether an architecture built around that narrower claim would hold up under adversarial-minded testing against a real object store, not just in design.

Why landing raw first is usually right

Before arguing for an exception, the default deserves to be taken seriously โ€” it earns its place, for reasons that compound:

  • Source fidelity. Once something is transformed, you can no longer prove what it originally was. A raw copy is the only artifact that answers "what did we actually receive?" with confidence, months later.
  • Replayability and reversibility. If transformation logic has a bug โ€” and it will โ€” a raw copy lets you re-run corrected logic against real historical input, and gives you a way back if today's policy turns out too aggressive, permissive, or simply wrong. A transformed-only copy offers neither.
  • Separation of concerns. Ingestion and transformation are different jobs with different failure modes; coupling them tightly lets a transformation bug take down ingestion itself.
  • Future reinterpretation. Requirements change. A field discarded today might be exactly what a future use case needs, and raw retention keeps that door open.

All of this is real, and for most data, most of the time, it's the correct set of tradeoffs. I'm not arguing against it in general โ€” I'm arguing that it has a blind spot that gets worse as the sensitivity of the input goes up.

When possession becomes the problem

The blind spot is this: once you persist the raw copy โ€” even briefly, even "just at the edge" โ€” you've taken on more than storage. You've taken on possession, and possession doesn't stay where you put it.

A raw object landed in a primary store tends to propagate: into read replicas, backups, DR snapshots, observability pipelines that sample payloads for debugging, downstream systems that subscribe to "new object landed" events and make their own copies. Some of that propagation is intentional infrastructure design; some of it is nobody's explicit decision โ€” it's just what happens when a system is built for durability and availability.

Here's the part that matters: deleting or transforming the primary copy later does not undo any of that. If a raw object with sensitive content lived in your primary store for even a short window, "where else did it go" doesn't have a clean answer. Backups from that window exist on their own retention schedule, DR replicas may have already synced, and a log line captured at ingestion time doesn't get retroactively redacted because you fixed the primary record.

This is the actual cost of "land raw first, clean it up later." Cleanup only ever addresses the copy you know about, and by the time you're cleaning up, you may not know about all of them.

So the question I wanted to sit with is:

What if the policy boundary moves ahead of persistence, instead of behind it?

Not because raw landing is generally wrong. Because for a specific category of input โ€” where the sensitive content is small, well-defined, and separable from the bulk of the legitimate payload โ€” the cost of ever letting the source representation reach a persistence layer might be high enough to justify architecting around it entirely, rather than trusting a downstream cleanup step to catch up.

Why DICOM is a useful test case

I needed something concrete to build this against, and DICOM โ€” the imaging format used across radiology, cardiology, and most clinical imaging โ€” turned out to be a good fit, not because the problem is unique to it, but because it makes the problem's shape unusually legible.

A DICOM object is, structurally, two different things stapled together: a relatively small set of metadata elements โ€” patient identity, study and series information, device and institution details, private vendor tags โ€” some of which is directly sensitive; and a large imaging payload, Pixel Data, that a metadata-directed policy may have no reason to decode or modify.

That split matters for the architecture, because it forces a design principle that generalizes well beyond DICOM:

Do not decode or materialize data that does not need to be interpreted.

That principle is about efficiency and scope, not pixel content being safe by default โ€” a distinction this article returns to below. If policy only concerns metadata, you shouldn't need to fully deserialize gigabytes of pixel data to enforce it; you need to know where it is in the byte stream, so it can pass through untouched while the much smaller structural elements around it are inspected and selectively rewritten.

This is what the stack beneath fastDICOMgateway does, across two layers. fastDICOMattrs is a C++20 library that parses a DICOM object's structure โ€” tags, VRs, value lengths, byte offsets, sequence and item nesting, bulk-data boundaries โ€” without requiring full decode of bulk content, representing Pixel Data as a reference into the source byte range rather than copying it into an object model. fastDICOMstructure sits above it: a policy layer that expresses removals, replacements, and private-tag rules declaratively and applies them against that structure, deterministically, before the result is serialized back out.

I won't turn this into a library walkthrough โ€” the repositories document the API, ABI, and round-trip contract in detail, including honest edge cases (Explicit VR Little Endian is the fully validated path; Implicit VR is less exhaustively tested; mutation produces valid but not byte-identical output). What matters here is narrower: together, attrs and structure are the mechanism that makes "transform before persist, without materializing the whole object" practical for DICOM objects. This piece is about the operational architecture built on top of them: fastDICOMgateway.

Moving the boundary

Here's the shape of the demonstrated system, fastDICOMgateway:

HTTP DICOM input
      โ”‚
      โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  fastDICOMgateway            โ”‚
โ”‚                               โ”‚
โ”‚  in-memory structural parse  โ”‚
โ”‚  โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„  โ”‚
โ”‚  โ”† fixed demonstration     โ”†  โ† policy boundary
โ”‚  โ”† policy transformation   โ”†
โ”‚  โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„โ”„  โ”‚
โ”‚  in-memory serialization     โ”‚
โ”‚  post-transform verification โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
               โ”‚
          STOW-RS
               โ”‚
               โ–ผ
   Google Cloud Healthcare API
       (DICOM store)
               โ”‚
          WADO-RS
               โ”‚
               โ–ผ
    independent retrieval

The load-bearing sentence, and the one I'd want a reader to hold onto:

No outbound persistence request is constructed or sent until the source DICOM has completed parse, policy transformation, in-memory serialization, and post-transform verification. Only the resulting transformed bytes are supplied to the Healthcare API client.

That ordering is the entire architectural claim. Everything else in this piece โ€” the demonstration policy, the escalating validation, the negative controls โ€” exists to make that one sentence something you can trust rather than something you have to take on faith.

For this demonstration, the policy applied is intentionally small and fixed:

Element             Action
PatientName          remove
PatientID            replace with DEMO
PatientBirthDate     remove
Private elements     remove
Pixel Data           preserve, byte-identical
Everything else      preserve

Pixel Data is deliberately outside the demonstration policy, preserved byte-for-byte to prove the gateway can transform selected metadata without altering the imaging payload โ€” not to claim the result is de-identified. It isn't, on two counts: pixels themselves may carry identifying information (burned-in annotations, recognizable visual features) that this policy never inspects, and real de-identification has to reckon with a much larger identifier surface than three tags โ€” DICOM PS3.15's Basic Application Level Confidentiality Profile lists dozens of attributes, plus UID re-mapping and date-shifting rather than blunt removal. What this table demonstrates is narrower and, I think, still useful: a place in the pipeline where whatever policy an organization actually needs can run deterministically, before persistence, without decoding the bulk payload it doesn't need to touch.

Building the mechanism

The mechanism came first: parse the incoming DICOM entirely in memory, apply the policy, serialize the transformed object entirely in memory, and only then produce output โ€” no temp files, no memory-mapped files standing in for "in-memory," no second DICOM implementation quietly doing the real work behind a demo layer.

Getting that right took an extra pass: the first working version still round-tripped certain paths through the filesystem to move bytes into the underlying library. Closing that gap meant adding genuine in-memory serialization through the attrs ABI (re-exported by fastDICOMstructure) and its Python binding, so the production transformation path no longer depended on a filesystem-backed intermediate. That's the baseline the boundary testing below was run against. "In-memory" describes that application-level design choice, not a guarantee about the layers beneath it โ€” the OS, hypervisor, or managed-cloud infrastructure could still page, buffer, or dump memory in ways this testing doesn't observe.

Making the claim progressively harder

A mechanism that works once, in a controlled run, isn't evidence of much. The next phase wasn't about building more โ€” it was about trying to break the claim, across environments each less forgiving than the last, each asking a sharper question than the one before:

Stage      Question                                    What changed
M1/M1.1    Can transformation stay in memory?           Removed filesystem-backed intermediate
M2         Does the process persist source content?     Syscall/filesystem/log observation
M3         Can writes be prevented architecturally?     Read-only, non-root container, no writable mounts
M4         Does behavior survive managed infra?         Cloud Run + available-log inspection
M5         What crossed into durable storage?           STOW-RS โ†’ independent WADO-RS retrieval

M2 and M3 differ in kind, not just degree. M2 used strace -f -tt -yy and a filesystem scan to confirm the application didn't write source bytes anywhere observed, across four scenarios including a deliberately induced failure โ€” a bounded claim, covering only the surfaces actually watched. M3 asked a harder question: could the environment be built so the application couldn't write source bytes, regardless of behavior? Running the container read-only, non-root, with zero writable mounts, and confirming zero filesystem changes via docker diff, moved the claim from "we watched and saw nothing" to "the kernel would have refused the write."

M4 is worth stating carefully, since it's tempting to read the boundaries as monotonically stronger and this one isn't. Cloud Run provides a writable ephemeral /tmp that M3's read-only filesystem didn't have. The application doesn't use it, and this testing found no evidence that it did โ€” checked across both of Cloud Run's log streams, the container's own output and its separately generated per-request log โ€” but the guarantee here is behavioral, the kind M2 offered, not the architectural enforcement M3 achieved. This makes no claim about Google's own infrastructure beneath the container; it reports what was checked at the surfaces available to check.

None of the first four boundaries touch a durable store, which is what makes M5 the one that matters most: it submitted the transformed object via DICOMweb STOW-RS to a dedicated Google Cloud Healthcare API DICOM store, then retrieved it back independently via WADO-RS โ€” a separate query path, separate credentials, no reliance on the gateway's own account of what it sent.

What actually reached durable storage

This is the central empirical result, and it's worth stating precisely: the "stored" column below wasn't read off the bytes about to be submitted. It came from an independent retrieval against the durable store, after the fact, using a separate tool. That distinction is the whole point of the test โ€” it answers "what's in the store," not "what did we mean to send." It verifies the state of this retrieved object, not every possible network transmission or provider-internal event; the broader no-intentional-transmission claim rests on that result plus the sequencing in ยง5 and the behavioral evidence in M2โ€“M4 โ€” not on WADO-RS retrieval alone.

Attribute          Source          Transformed    Stored (independently retrieved)
PatientName        present         absent         absent
PatientID          source value    DEMO           DEMO
PatientBirthDate   present         absent         absent
Private elements   present         absent         absent
Pixel Data         hash A          hash A         hash A

Every synthetic canary planted in the source object (a distinctive PatientName, PatientID, and PatientBirthDate) was absent from the retrieved, durably stored object; the expected transformed values were present. Pixel Data โ€” the large imaging payload policy never touched โ€” round-tripped with an identical hash, all the way through parse, transform, serialize, submit, and independent retrieval: hash A โ†’ A โ†’ A, on the exercised fastDICOMgateway โ†’ Healthcare API โ†’ WADO-RS path โ€” a property of this path, not a guarantee about DICOM stores generally, since other stores or transfer-syntax conversions can alter pixel bytes.

That last row matters as much as the identity-removal rows, though not because it proves the pixels are safe โ€” ยง5 already qualified that. It matters because it's easy to build a system that strips everything and call it safe. The harder, more useful claim is that the policy boundary is selective: it removes exactly what policy says to remove, and preserves the rest โ€” including a large binary payload โ€” without corruption, through the full path to a durable clinical-imaging store.

Testing the tests

This is the part of the project I'd most want a skeptical reader to look at closely โ€” it's what keeps the rest of this article honest.

Here's the problem with validation harnesses: it's easy to write a checker that always says PASS โ€” not through dishonesty, but through a checker that quietly can't detect the failure mode it claims to cover. A harness that greps for a canary string in the wrong log stream, or checks a file's current state instead of every state it passed through, will report success indefinitely, right up until it matters.

So starting at M2, every boundary stage included a negative control: a deliberate, temporary, never-committed sabotage of the system under test, whose entire purpose was to prove the checker could actually catch a real violation โ€” not to prove the system was safe, but to prove the thing checking the system wasn't blind.

The results were more interesting than I expected, because three of the four negative controls found bugs in the checker, not the system:

  • M2's negative control exposed a real scoring gap: when a file was overwritten by more than one request in sequence, naive content-matching attributed the violation only to the last writer, even though independent syscall evidence showed every request had touched it. Fixed to use the syscall evidence as the source of truth.
  • M3's negative control found the same class of bug in a different mechanism: docker diff reports a path's first observed change-kind, so a file created and then modified could be scored incorrectly. Read-only mode blocked the write outright (OSError: [Errno 30] Read-only file system); writable mode exercised the detection logic and exposed โ€” then fixed โ€” the gap.
  • M4's negative control exposed a log-attribution bug: matching a log entry to a scenario by canary name alone, without a time window, over-attributed hits across scenario boundaries when scenarios ran close together. Fixed with real per-scenario UTC time windows and deliberate gaps between them.
  • M5's negative control worked differently, confirming the checker rather than breaking it: a deliberately source-bearing object, stored directly into an isolated store, bypassing the gateway, was correctly flagged โ€” useful confirmation, since three prior rounds had already hardened the methodology.

I'd rather report this plainly than bury it: most of the value here came from finding flaws in the evidence-gathering, not the system under test. That's not a footnote โ€” it's close to the central methodological finding of the whole project.

A validation harness that has never been shown capable of failing has not demonstrated much about the system it's checking.

Diagram: a claim is tested by a validation apparatus; a deliberate negative control asks whether the checker can detect the violation; if no, the checker is fixed and validation repeats; if yes, the checker has been shown capable of failing, and the validation result can be trusted.
The loop this section actually ran, four times: sabotage the system on purpose, and find out whether the checker notices.

If a checker always says PASS, that's not evidence of a well-built system. It might just be evidence of a checker that can't fail. The only way to know which one you have is to deliberately break the thing you're checking and confirm the checker notices.

None of this proves the production system is correct on its own โ€” a negative control tests the checker, not the system under test twice over. What it establishes is narrower and, I'd argue, more useful: that when M2 through M5 report PASS on the real system, that PASS came from a checker already shown capable of saying FAIL.

What this demonstrates โ€” and what it doesn't

Kept narrow, deliberately, because a claim this specific is more useful than a claim this vague.

This demonstrates:

  • A pre-persistence policy transformation boundary that runs before any outbound persistence request is constructed.
  • In-memory structural transformation of a DICOM object, without full decode of bulk Pixel Data.
  • Byte-identical preservation of the demonstrated Pixel Data payload through the full transform-and-persist path.
  • Consistent transformation and failure behavior across a bare host process, a read-only container, and Cloud Run's managed environment.
  • That, in the exercised Healthcare API path, only the transformed representation โ€” not the source representation โ€” was present in durable storage upon independent retrieval.
  • A falsifiable validation approach: negative controls shown capable of detecting real violations, not just reporting PASS by default.

This does not establish:

  • Complete DICOM de-identification, PS3.15 compliance, or HIPAA compliance โ€” Pixel Data risk is out of scope for this policy, as already noted.
  • Production-grade authentication, authorization, or scalability.
  • Absence of the source representation from any system, buffer, or log beyond the surfaces actually queried โ€” Google-internal infrastructure, RAM, kernel/network buffers, swap, and crash dumps included.
  • General proof across every DICOM transfer syntax or object type, or superiority over other architectures. This is one architecture, tested for one property, on one exercised path.

Where else this boundary might matter

The generalized version of the engineering principle, stripped of DICOM specifics:

Move policy ahead of durable persistence when the cost of ever committing the source representation to storage is high enough to justify proving โ€” not merely assuming โ€” that it doesn't happen.

That's not a universal replacement for "land raw first." It's a design choice for when possession of the source representation is itself a material risk: payment instrument data ahead of a fraud-scoring pipeline, government ID documents ahead of a KYC workflow, genomic data ahead of a research data lake.

The cost is sharpest when the receiver is becoming the system of record โ€” the one place an object will ever durably exist. Discarding the only authoritative copy there is dangerous: nothing to replay against, nothing to walk back to. Raw-first earns its keep exactly here.

It's a different calculation when the receiver isn't the system of record. In a clinical trial, the original clinical image can remain upstream at the acquiring clinical organization while a CRO delivers a copy downstream to a trial sponsor for a defined purpose. The sponsor's incoming object is therefore a delivery copy rather than the only surviving source representation. If that delivery contains identifiers it should not contain, rejecting it and requesting a corrected resubmission need not destroy the underlying source โ€” the upstream image remains available.

That makes the boundary more than a de-identification step: nothing should cross into durable persistence until it satisfies the receiver's acceptance policy โ€” accepted unchanged, transformed and accepted, or rejected pending correction. This demonstration exercises only the transform-and-accept path; reject-and-resubmit is a design implication of the boundary, not something the gateway has been shown to do.

Comparing notes

If you operate a pipeline where possession of the raw input is itself a risk โ€” not just what you eventually do with it, but the fact of having received and stored it โ€” I'd be interested in comparing where you've placed the persistence boundary, and what made you put it there. The reasoning behind that placement is usually more interesting than the diagram.

None of this is presented as finished. All of it is presented as checked.

Foundation and Next Steps

Research Program

DICOM Trust Boundary Research

Follow the larger research program examining DICOM ingestion, structural parsing, and where trust and policy boundaries belong in imaging pipelines.

Evidence & Reproduction

fastDICOMgateway on GitHub

The HTTP reference application, the M1โ€“M5 milestone reports and evidence artifacts, and the reproduction steps for every boundary described in this article (v0.1.0).

Mechanism

fastDICOMstructure & fastDICOMattrs

The policy layer (v0.1.0) and the underlying structural engine (v0.2.0) discussed in ยง4 โ€” see Article 01 for the corpus-validation and benchmark evidence behind the structural layer.