Zero-Trust Security for Spatial Endpoints

Treating every tile request, feature query, and geocoding call as untrusted until a cryptographic workload identity and an explicit authorization policy say otherwise — never trusting the network the request arrived on. In a federated geospatial estate, the vector engine, raster tile server, and geocoding resolver each live in a different failure domain owned by a different team, and the flat internal network between them is exactly where lateral movement happens once a single token leaks. This page sits within the Federated Ownership & Routing Architecture and reframes ingress security as a per-request identity decision rather than a perimeter firewall: mutual TLS proves who is calling, SPIFFE identities bind that proof to a specific domain workload, and an external authorization policy decides whether this caller may touch this OGC API endpoint at all. It builds directly on the edge contract described in API Gateway Mapping for GIS Services and shares its enforcement surface with the Schema Contracts for Vector/Tile Data validated one hop later.

Figure — A caller presents a client certificate at the mTLS ingress; a SPIFFE identity is derived and passed to the authorization policy, which either forwards to the owning domain endpoint or returns a deny.

Zero-trust request path for spatial endpoints A client workload presents a client certificate to the ingress, which terminates mutual TLS and verifies the peer certificate against the trust bundle. The verified SPIFFE identity is extracted and handed to an external authorization policy engine, which evaluates whether the caller may reach the requested OGC API tile or vector endpoint. On allow, the request is forwarded to the owning domain endpoint; on deny, the policy returns a 403 without ever contacting the backend. Client workload presents client cert Ingress mTLS terminate + verify peer against trust bundle Authorization policy SPIFFE identity in allow / deny out Domain endpoint OGC API tiles / vector allow path 403 deny backend never contacted

Architectural Boundaries & Design Rationale

Zero trust for spatial endpoints rests on one inversion: the network is assumed hostile, so authority comes from a verifiable workload identity rather than from an IP range, a VPC boundary, or a shared secret baked into a sidecar. Each domain workload — the cadastral vector engine, the imagery mosaic service, the geocoding resolver — is issued a short-lived X.509 SVID (SPIFFE Verifiable Identity Document) whose trust domain and path encode exactly which service it is. A request from spiffe://mesh.internal/ns/geocode/sa/resolver is a first-class, cryptographically attested fact, not a claim in a header that any compromised pod could forge. This is what makes ownership enforceable at the wire: the same domain model that drives Spatial Domain Boundary Design is now the identity a policy engine evaluates before a single tile byte is served.

The boundary this closes is lateral traversal. In a flat mesh, a token stolen from a low-sensitivity geocoding consumer can be replayed against a cadastral feature store because the network path exists and nothing re-checks identity per hop. Zero trust breaks that by requiring mutual TLS on every internal edge and an explicit authorization decision at every endpoint, so a caller with a valid geocode identity but no cadastral entitlement is denied at the vector service itself — not merely at the perimeter it already crossed. Three enforcement layers stack to make this deterministic:

  1. Transport identity — mTLS with peer-certificate verification against a rotating trust bundle; no plaintext internal hop is permitted.
  2. Workload identity — the SPIFFE SVID is extracted from the verified peer certificate and becomes the authenticated principal.
  3. Authorization — an external policy engine (OPA, or Envoy’s native AuthorizationPolicy) evaluates principal, method, path, and spatial context before the backend is reached.

Because identity is verified independently at each edge, this layer composes cleanly with routing rather than replacing it. The gateway still resolves which domain owns a path; zero trust decides whether this caller may reach it. The finest-grained rule set — enforcing STRICT mTLS between individual domain workloads and requiring a valid identity in the authorization policy — is covered operationally in Enforcing mTLS Between Spatial Domains.

Specification & Contract Reference

A zero-trust endpoint publishes an explicit contract: which identities may call it, over what transport, and what an authorization decision guarantees. The surface below is the minimum a spatial domain enforces before serving an OGC API request.

Field / Control Scope Required Constraint / Default
mTLS mode Mesh peer policy Yes STRICT; plaintext peers rejected at transport
Peer trust bundle Ingress + sidecar Yes Rotated ≤ 24h; verified against SPIFFE trust domain
SPIFFE SVID Workload identity Yes spiffe://<trust-domain>/ns/<domain>/sa/<service>
SVID TTL Identity lifetime Yes ≤ 1h; auto-rotated by SPIRE agent
source.principals Authz rule Yes Exact SPIFFE ID or namespace glob; empty → deny
operation.methods Authz rule Yes GET for tiles/features reads; writes scoped separately
operation.paths Authz rule Yes /collections/*, /tiles/*, /tileMatrixSets/*
Default action Authz policy Yes DENY (allow-list model; unmatched request → 403)
Idempotency-Key Header Write ops Replays return original result within TTL, unchanged auth outcome

Two properties are non-negotiable. First, the policy is an allow-list: the absence of a matching rule is a deny, so a newly deployed endpoint is closed until an identity is explicitly granted access, never open by default. Second, authorization is idempotent and side-effect-free — evaluating the same principal, method, path, and spatial context yields the same decision every time, and a retried request (carrying the same Idempotency-Key for writes) is authorized identically without double-charging a downstream mutation. These mirror the versioned enforcement already applied by Schema Contracts for Vector/Tile Data: identity is checked at the same edge where the payload schema is, so a request that passes one gate meets the other in the same hop.

Production Implementation

Zero trust is configured declaratively and shipped through GitOps so the identity and authorization posture is versioned alongside the routes it protects. The first manifest turns on STRICT mutual TLS for a domain namespace and pins an authorization policy that only admits the gateway and named sibling domains carrying a valid SPIFFE identity. Zero-trust is explicit here: the DENY-by-default AuthorizationPolicy means an unauthenticated or unlisted principal never reaches the tile server, and the STRICT PeerAuthentication guarantees the identity was cryptographically verified rather than asserted in a header.

yaml
# Istio: require verified mTLS on every inbound edge of the cadastral domain,
# then allow ONLY named workload identities to reach its OGC API endpoints.
# Zero-trust: STRICT rejects any plaintext peer; the AuthorizationPolicy is an
# allow-list, so an unmatched principal falls through to an implicit 403 DENY.
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
  name: cadastral-mtls-strict
  namespace: cadastral
spec:
  mtls:
    mode: STRICT            # no plaintext hop permitted into this domain
---
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: cadastral-endpoint-authz
  namespace: cadastral
spec:
  selector:
    matchLabels:
      app: wfs-feature-engine
  action: ALLOW            # default action for unmatched requests is DENY
  rules:
    - from:
        - source:
            principals:     # SPIFFE identities, derived from the verified cert
              - "cluster.local/ns/gateway/sa/geospatial-gateway"
              - "cluster.local/ns/routing/sa/cross-domain-router"
      to:
        - operation:
            methods: ["GET"]
            paths:
              - "/collections/*"     # OGC API Features
              - "/tiles/*"           # OGC API Tiles
              - "/tileMatrixSets/*"

The second layer handles decisions too rich for a static match — for example “this geocoding identity may read parcel features only when the requested bbox falls inside its licensed extent.” That logic lives in a Rego policy evaluated by OPA, keeping the spatial authorization rules in one auditable, testable place. Idempotency again matters: opa eval is a pure function of its input document, so replaying the same decision request returns the same allow/deny without side effects.

rego
# authz.rego — decide whether a SPIFFE identity may read a spatial collection
# within a bounded extent. Pure function: same input -> same decision (idempotent).
package spatial.authz

import rego.v1

default allow := false

# Allow reads when the caller's identity is entitled to the collection AND
# the requested bbox is fully contained by the identity's licensed extent.
allow if {
    input.method == "GET"
    some grant in data.entitlements[input.principal]
    grant.collection == input.collection
    bbox_within(input.bbox, grant.extent)
}

bbox_within(req, lic) if {
    req[0] >= lic[0]   # minx
    req[1] >= lic[1]   # miny
    req[2] <= lic[2]   # maxx
    req[3] <= lic[3]   # maxy
}
bash
# Evaluate the policy for a concrete request. A denied caller returns false
# and the endpoint is never contacted. Bbox is in EPSG:4326 (lon/lat).
opa eval -d authz.rego -d entitlements.json \
  -i request.json 'data.spatial.authz.allow'
# request.json: {"principal":"cluster.local/ns/geocode/sa/resolver",
#   "method":"GET","collection":"parcels",
#   "bbox":[-1.2,52.6,-1.0,52.8]}   # -> "result": false  (no cadastral grant)

Both manifests are policy-as-code: they are reviewed, promoted through CI that runs opa test on the Rego and validates the Istio CRDs against a staging mesh, and rolled out only after the identity plane confirms every named principal resolves to a live SPIFFE SVID.

Diagnostic Runbook

When a spatial call is rejected under zero trust, the failure is almost always identity, trust-bundle freshness, or an authorization rule — not the endpoint’s business logic. Work the steps in order; each isolates one layer of the decision.

  1. Read the response code and flag. A 403 with an Envoy RBAC: access denied log means the request authenticated but no authorization rule matched. A TLS handshake failure (no 403, connection reset) means the request never reached the policy layer — jump to step 3.
  2. Confirm the caller’s SPIFFE identity. Extract the principal the sidecar actually saw; a request routed through an unexpected service account will carry the wrong SVID and fall through the allow-list to a deny.
  3. Verify mTLS mode on the edge. Confirm the destination namespace is STRICT and the caller is injected into the mesh. A plaintext caller into a STRICT peer is rejected at transport before any authorization runs.
  4. Check trust-bundle freshness. A handshake failure with certificate signed by unknown authority points to a stale or unrotated trust bundle; confirm the SPIRE agent rotated SVIDs within their TTL.
  5. Inspect the authorization rule set. Dump the effective AuthorizationPolicy and confirm the caller’s principal, HTTP method, and path prefix all match a single rule. A path outside /collections/* or /tiles/* is denied even for an entitled identity.
  6. Evaluate the Rego decision in isolation. For extent-scoped denials, run opa eval with the exact request document; a bbox that exceeds the licensed extent returns false and is the intended, not erroneous, outcome.
  7. Correlate with the trace. Tie the deny to its trace_id and confirm no retry masked it — a retried write must return the identical authorization outcome, never a second, differently-authorized attempt.

Zero trust degrades safely: a denied request is a fast, cheap 403 that never loads the backend, which is precisely why it must be distinguished from a genuine outage before paging on-call.

SLA Targets & Performance Baselines

The identity and authorization plane must add bounded, predictable overhead so domain SLAs stay meaningful. The budgets below are what the zero-trust layer must hold.

Metric Target Alert Threshold Remediation Action
mTLS handshake success > 99.99% < 99.9% for 5m Rotate trust bundle; check SPIRE agent health
SVID rotation freshness < 60m age > 55m without rotation Restart SPIRE agent; verify workload attestation
Authorization decision latency < 3ms p99 > 10ms p99 for 5m Pin OPA bundle in memory; profile policy evaluation
External-authz availability > 99.99% < 99.95% Fail-closed to DENY; scale OPA replicas
Unauthorized (403) rate < 0.5% baseline > 5% sudden spike Inspect for credential drift or a bad policy push
Policy sync drift < 5s > 30s Reconcile GitOps; re-apply AuthorizationPolicy

The authorization engine must fail closed: if the external policy service is unreachable, the endpoint denies rather than admits, so a control-plane outage can never silently downgrade a spatial domain to open access. Cache the compiled policy bundle in the sidecar to hold the latency budget without trading away that fail-closed guarantee.

Governance & Compliance Notes

Every authorization decision is an auditable governance artifact. The mesh records the SPIFFE principal, method, path, spatial context, and allow/deny outcome in append-only, domain-partitioned streams, and injects W3C Trace-Context at ingress so each decision correlates with the downstream spatial-engine span. Because the policy is committed as code, a reviewer can diff exactly which identity was granted access to which collection and extent, and CI proves the change with opa test before it merges. This keeps the security posture aligned with the governed data products catalogued in Metadata Cataloging for Raster/Vector, so a consumer-visible endpoint maps to a documented, access-controlled product rather than an ambient network reachability.

Data-residency and least-privilege obligations are enforced at this layer, not documented and hoped for. Extent-scoped Rego rules bind a tenant’s vector features to a permitted bbox, refusing any request whose spatial context falls outside the licensed region even when the caller’s identity is otherwise valid. SVIDs are short-lived by design so a leaked credential expires within the hour, and the allow-list default means a newly onboarded domain is closed until an explicit grant is reviewed and merged. Together these give compliance a deterministic, replayable record: the same request, evaluated against the same committed policy version, always yields the same decision.