Security Boundaries for Retail Image Data

Within the Core Architecture for Shelf Analytics platform, this component owns one job that every other stage depends on: it decides what a store photo is allowed to become before anyone — a model, an engineer, or a category manager — is permitted to touch it. A planogram-compliance fleet ingests millions of high-resolution shelf frames a day, and those frames are not neutral pixels. They encode proprietary merchandising strategy, store-level metadata, and the occasional incidental capture of a shopper’s face or an employee badge. Treat that stream as ordinary image data and the failure is not a lower F1 score; it is a GDPR notification clock running, a competitor inferring your endcap strategy from a leaked bucket, or a compromised dashboard pivoting into raw imagery. This page specifies the classification contract, the redaction-and-signing implementation, the retention and access configuration, the debugging workflow for boundary failures, and how the tier scales — for the security layer specifically, sitting between the Retail Data Ingestion Pipelines for Store Photos that feed it and the compute that consumes it.

Defense-in-depth boundary for retail shelf imagery, from trust-zoned capture to role-gated access Three capture vectors — auditor handhelds, fixed aisle cameras, and third-party field apps — each enter from a labelled trust zone and converge on an edge gate that assigns a data class, blurs faces, plates and badges, strips EXIF geolocation and signs an Ed25519 envelope. Admitted frames cross mutual-TLS signed transit into an ephemeral inference enclave, then land in a KMS-encrypted classified store tiered hot, warm and cold with a ninety-day purge. A role-gated access stage splits into two lanes: category managers read aggregated dashboards with no object key, while engineers and auditors receive time-bound tokenized raw access. An immutable WORM audit log runs along the bottom, touching every stage. CAPTURE · TRUST ZONES EDGE BOUNDARY GATE CLASSIFIED STORE Auditor handheld zone: auditor_device Fixed aisle camera zone: fixed_camera Third-party field app zone: third_party_app Classify + redact · assign data_class · blur face / plate / badge · strip EXIF geo-tags · Ed25519 sign envelope PII never leaves the store Quarantine bucket bad signature · oversized · schema mTLS · TLS 1.3 signature verified Ephemeral enclave tmpfs · evicted on done KMS envelope keys Hot · ≤ 7d Warm · ≤ 30d Cold → purge 90d derived metrics kept 730d no imagery · no PII Role-gated access least privilege · logged deref Category managers aggregated dashboards · metadata_only never dereference object_key Engineers / auditors time-bound tokenized raw access 15-min presigned URL · IP allowlist IMMUTABLE WORM AUDIT LOG · touches every stage

Concept & Data Contract Jump to heading

The security tier consumes the same raw image binary the ingestion gate admits, but it produces something richer: a classification envelope that binds every downstream permission to the frame. Nothing reaches storage or compute without one. The envelope answers three questions that govern the rest of the frame’s life — what class of data this is, what PII it still contains, and who may dereference it and for how long.

Capture originates from three distinct vectors, each with its own threat profile: handheld auditor devices, fixed aisle cameras, and third-party field-service applications. Rather than trust the source network, every capture maps to an explicit trust zone and is classified at the edge into one of three data classes — raw_shelf_photo, planogram_reference, or metadata_only — before a single byte egresses. Incidental capture of faces, license plates, or employee badges triggers data-minimization obligations, so the contract also carries the residual-PII verdict from the on-device redaction pass.

The contract is enforced with a typed model, not ad-hoc dictionary access, so a frame missing its classification or carrying an un-cleared PII flag is rejected at the boundary instead of poisoning a storage tier three stages later.

from datetime import datetime, timezone
from enum import Enum
from pydantic import BaseModel, Field, field_validator


class DataClass(str, Enum):
    RAW_SHELF_PHOTO = "raw_shelf_photo"
    PLANOGRAM_REFERENCE = "planogram_reference"
    METADATA_ONLY = "metadata_only"


class TrustZone(str, Enum):
    AUDITOR_DEVICE = "auditor_device"
    FIXED_CAMERA = "fixed_camera"
    THIRD_PARTY_APP = "third_party_app"


class ClassificationEnvelope(BaseModel):
    """The security contract every frame must satisfy before storage or compute."""

    store_id: str = Field(..., pattern=r"^[A-Z]{2}\d{4}$")
    fixture_id: str = Field(..., min_length=3)
    capture_timestamp: datetime                       # UTC, normalized at the edge
    trust_zone: TrustZone
    data_class: DataClass
    residual_pii: bool                                # post-redaction verdict
    redaction_confidence: float = Field(..., ge=0.0, le=1.0)
    exif_stripped: bool                               # geolocation removed at edge
    retention_days: int = Field(..., ge=1, le=2555)
    payload_sha256: str = Field(..., min_length=64, max_length=64)
    signature: str                                    # Ed25519 over the canonical envelope
    object_key: str                                   # pointer into the classified store

    @field_validator("residual_pii")
    @classmethod
    def block_uncleared_pii(cls, v: bool, info) -> bool:
        # A frame that still carries PII may not be admitted to shared storage.
        if v and info.data.get("data_class") == DataClass.RAW_SHELF_PHOTO:
            raise ValueError("raw_shelf_photo with residual PII cannot cross the boundary")
        return v

Downstream, those fields are load-bearing. Category managers consuming aggregated compliance scores never dereference an object_key; they read metadata_only and derived metrics. Python vision engineers receive time-bound, sanitized access to raw_shelf_photo for retraining. The same envelope also constrains how the Integrating Legacy POS Data with Modern Vision APIs join is allowed to run, so transactional truth and visual reality can be correlated without ever co-mingling raw imagery with sales figures.

Implementation Architecture Jump to heading

The boundary is enforced at the edge first, because the cheapest PII is PII that never leaves the store. The reference implementation runs a quantized on-device detector (YOLOv8n or an OpenCV cascade where compute is scarce), masks any privacy-sensitive region, strips EXIF geolocation, then signs the resulting envelope with an Ed25519 key provisioned per device. Frames are admitted only over TLS 1.3 with mutual authentication, so the cloud ingress trusts the signature and the client certificate, not the source IP.

import hashlib
import io
from typing import Sequence

import cv2
import numpy as np
from nacl.signing import SigningKey
from pydantic import ValidationError

PII_CONFIDENCE_FLOOR = 0.35   # below this, a region is masked defensively, not ignored


def redact_and_classify(
    image_bytes: bytes,
    detector,                      # callable -> Sequence[(label, conf, x, y, w, h)]
    signing_key: SigningKey,
    *,
    store_id: str,
    fixture_id: str,
    trust_zone: str,
    captured_at,
    retention_days: int,
) -> ClassificationEnvelope:
    """Mask PII at the edge, strip EXIF, and emit a signed classification envelope."""
    arr = np.frombuffer(image_bytes, dtype=np.uint8)
    frame = cv2.imdecode(arr, cv2.IMREAD_COLOR)        # imdecode drops EXIF entirely
    if frame is None:
        raise ValueError("undecodable image payload rejected at boundary")

    residual_pii = False
    min_conf = 1.0
    detections: Sequence = detector(frame)
    for label, conf, x, y, w, h in detections:
        if label in {"face", "license_plate", "employee_badge"}:
            if conf < PII_CONFIDENCE_FLOOR:
                residual_pii = True                    # uncertain -> flag, do not trust
            roi = frame[y : y + h, x : x + w]
            frame[y : y + h, x : x + w] = cv2.GaussianBlur(roi, (99, 99), 30)
            min_conf = min(min_conf, conf)

    ok, encoded = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 92])
    if not ok:
        raise RuntimeError("re-encode after redaction failed")
    payload = encoded.tobytes()
    digest = hashlib.sha256(payload).hexdigest()

    unsigned = {
        "store_id": store_id,
        "fixture_id": fixture_id,
        "capture_timestamp": captured_at,
        "trust_zone": trust_zone,
        "data_class": "raw_shelf_photo",
        "residual_pii": residual_pii,
        "redaction_confidence": round(min_conf if detections else 1.0, 4),
        "exif_stripped": True,
        "retention_days": retention_days,
        "payload_sha256": digest,
        "signature": signing_key.sign(digest.encode()).signature.hex(),
        "object_key": f"{store_id}/{fixture_id}/{digest}.jpg",
    }
    try:
        return ClassificationEnvelope(**unsigned)
    except ValidationError as exc:
        # A boundary rejection is a security event, not a dropped frame.
        raise PermissionError(f"frame failed classification contract: {exc}") from exc

Library choices are deliberate. pynacl (Ed25519) gives small, fast, deterministic signatures suitable for low-power capture hardware, and verification on the ingress side is a constant-time operation that resists tampering of the payload_sha256 digest. cv2.imdecode/imencode is used precisely because the round-trip discards EXIF — geolocation never survives to storage. Where a device genuinely lacks the compute for on-device detection, the same function runs unchanged on a dedicated in-store edge gateway that performs synchronous redaction before the frame is allowed onto the WAN.

Transit security extends past the signature. Vision-processing clusters must never share a subnet with corporate IT, HR, or point-of-sale networks, and a service mesh enforces east-west mTLS between every microservice so a compromised dashboard cannot pivot to raw image storage. Once admitted, models run in ephemeral inference enclaves — containers with strict resource quotas, GPU partitioning via MIG or Kubernetes device plugins, /tmp mounted as tmpfs, and automatic pod eviction on completion so residual image tensors are scrubbed between jobs rather than lingering in a long-lived process.

Production Configuration & Tuning Jump to heading

The boundary’s behaviour is driven entirely by configuration so security teams can tighten it without redeploying code. The values below are production starting points, not absolutes — tune them against your own audit cadence and regulator posture.

# security-boundary.yaml
classification:
  pii_confidence_floor: 0.35      # mask-or-flag threshold for face/plate/badge
  default_data_class: raw_shelf_photo
  strip_exif: true
transit:
  tls_min_version: "1.3"
  mutual_tls: true
  max_payload_mb: 12              # reject oversized uploads at the gateway
  quarantine_unverified: true     # signature mismatch -> forensic bucket, not prod queue
storage:
  hot_to_warm_days: 7
  warm_to_cold_days: 30           # raw photos archive at 30
  raw_purge_days: 90              # and are purged at 90 unless audit-flagged
  metrics_retention_days: 730     # derived Parquet/JSON kept longer
  kms_envelope_encryption: true
access:
  raw_token_ttl_minutes: 15       # time-bound presigned access for engineers
  ip_allowlist_enforced: true
  category_manager_scope: aggregated_only
audit:
  log_retention_months: 12        # SOC 2 / ISO 27001 floor
  immutable_worm: true

Three levers deserve explicit attention. The pii_confidence_floor of 0.35 is intentionally low: below it, an uncertain region is masked and flagged as residual PII rather than waved through, trading a few over-blurred shelf edges for zero un-cleared faces in storage. Retention is enforced as a state machine — raw raw_shelf_photo objects transition to cold storage at 30 days and are purged at 90 unless an audit hold is set, while derived compliance metrics persist for 730 days because they carry no imagery. Raw-access tokens default to a 15-minute TTL bound to an IP allowlist; raw image dumps are never distributed via shared drives or unversioned buckets, only through a registry that mints short-lived presigned URLs. All envelope encryption uses cloud KMS with per-object data keys so a single leaked key never exposes the tier.

Failure Modes & Debugging Workflow Jump to heading

Boundary failures rarely announce themselves directly; the symptom is usually a frame that silently never appeared, a 403/422 loop, or a compliance count that drifted. Work the steps in this order — it resolves the common incidents fastest.

  1. Reproduce the mTLS handshake before blaming the app. A frame that never reaches ingress is almost always a transit failure, not a logic bug. Verify the chain with openssl s_client -connect <endpoint>:443 -cert client.pem -key client-key.pem and check SAN matching and intermediate-cert expiry. Misaligned CA roots and expired intermediates are the single most common transit blocker after a certificate rotation.
  2. Inspect the quarantine bucket grouped by reject reason. Signature-mismatched and oversized payloads route here, not to production. A sudden climb in signature_mismatch for one store usually means a device’s Ed25519 key fell out of sync after a re-provision; a climb in schema_invalid points at an edge SDK rollout that changed the manifest shape.
  3. Audit the residual-PII flag ratio. A rising ratio of residual_pii: true envelopes means the on-device detector is degrading — new uniform colours, store lighting changes, or a camera firmware update softening focus below the redaction model’s working range. Raise the issue against the detector, not the boundary; the boundary is doing its job by flagging.
  4. Trace one object_key end to end. Attach a correlation id at the edge and log it at classify, sign, ingress-verify, store, and access. A frame that vanishes between sign and ingress-verify points at a clock-skew or signature-canonicalization bug; one that vanishes between store and access points at an IAM scope that is too tight.
  5. Diagnose enclave eviction, not data loss, on CUDA_ERROR_OUT_OF_MEMORY. When inference pods die mid-batch, fragmented VRAM from un-scrubbed prior jobs is the usual cause. Set PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True, confirm /tmp is tmpfs, and throttle batch size; persistent tensors surviving eviction is itself a leakage finding worth a ticket.

The most common root causes map cleanly to these steps: certificate rotation (handshake failures), device key drift (signature quarantine spike), detector degradation (residual-PII ratio), over-tight IAM (access-stage disappearance), and un-scrubbed enclaves (OOM eviction). To reproduce a suspected classification regression safely, replay a quarantined payload through redact_and_classify in a staging harness and assert on the raised PermissionError reason rather than re-running the fleet. Genuine model-drift incidents — a detector quietly mis-counting facings — are out of scope here and belong to the retry and drift paths described alongside Best Practices for Securing Retail Shelf Images in AWS.

Scaling & Performance Benchmarks Jump to heading

The redaction-and-sign step is the only CPU-heavy work on the boundary, and it is embarrassingly parallel: each frame is independent, so the tier scales horizontally on burstable CPU instances with no shared state. Edge redaction on a quantized YOLOv8n model holds well under 120ms per 1920x1080 frame on modest auditor hardware, and Ed25519 signing adds sub-millisecond overhead, so the boundary is never the latency bottleneck — that budget belongs to the GPU detection tier behind it.

P95 time-to-classification, measured from capture to a signed envelope landing in the classified store, should hold under 1.5s in stable conditions. Sustained breaches point at gateway mTLS-renegotiation overhead or KMS throttling on the envelope-encryption path, not at the detector. Watch two saturation signals: KMS request rate (per-object data keys can hit account quotas during a morning reset spike — provision scheduled capacity or batch key generation), and quarantine-bucket write rate (a sudden surge means an upstream device-fleet problem amplifying load, not organic growth).

Cost discipline comes from doing security work at the cheapest possible point. Redacting at the edge keeps PII off the WAN and off shared storage entirely, which is both the privacy posture and the largest cost lever. Tiering raw photos to cold archival at 30 days and purging at 90 keeps the expensive hot tier small, while derived metrics — which carry no imagery and need no redaction — live cheaply for 730 days. At a fleet of a thousand stores on a four-hour reset cadence, a correctly partitioned boundary sustains the load on a handful of CPU consumers, with KMS calls and cold-storage egress, not compute, as the dominant line items.

Frequently Asked Questions Jump to heading

Why classify and redact at the edge instead of in the cloud? Because the cheapest and safest PII is the PII that never leaves the store. Masking a face on the capture device costs a few milliseconds and keeps privacy-sensitive pixels off the WAN, off shared storage, and out of every later backup and snapshot. Redacting in the cloud means the raw face already crossed your boundary and now lives in logs, transit buffers, and ingress storage you have to prove you purged.

What is the residual-PII flag for if the frame is already blurred? It records uncertainty. When the detector’s confidence on a face or badge falls below 0.35, the region is still masked, but the envelope is flagged residual_pii: true and a raw_shelf_photo carrying that flag is refused at the boundary. The flag turns a silent maybe-miss into an explicit, auditable rejection, and a rising flag ratio is your early warning that the edge detector is degrading.

Can category managers ever see raw shelf photos? By default, no. Category managers and retail-ops teams are scoped to aggregated dashboards and tokenized compliance reports — they read derived metrics, never an object_key. Direct raw-image access is restricted to security-cleared engineers and compliance auditors through short-lived presigned URLs bound to an IP allowlist, with every dereference written to an immutable audit log.

How long is raw imagery kept, and why two different retention windows? Raw raw_shelf_photo objects tier to cold storage at 30 days and are purged at 90 unless an audit hold is set, because the imagery is the liability. Derived compliance metrics carry no PII and no merchandising photo, so they persist for 730 days to support time-series trend analysis. Splitting the windows shrinks the high-risk surface without losing the analytics history.

How do I keep POS data from contaminating image storage? Never co-mingle them. The classification envelope keeps transactional and visual data in separate stores, and any correlation runs in a clean room or federated query engine that joins on tokenized identifiers without exposing raw imagery or sales figures — the pattern detailed in Integrating Legacy POS Data with Modern Vision APIs.

Back to top