Core Portal Architecture & Security Boundaries: Operational Guide for Open-Source Geospatial Infrastructure

Deploying and scaling an open-source geospatial portal demands rigorous infrastructure design, strict service isolation, and continuous security validation. This guide gives GIS administrators, open-source maintainers, platform engineers, and government technology teams a production baseline for running a resilient, auditable, horizontally scalable portal — one that coordinates cleanly with edge caching, identity federation, and the spatial data tier while holding configuration-as-code, zero-trust networking, and tested failover as non-negotiable defaults.

This architecture reference is one half of the wider open-source portal operations program: it owns runtime topology and trust boundaries, while the day-two provisioning surface lives in Infrastructure Orchestration & Config Management and the discoverability surface in Metadata Catalog Automation & Ingestion Workflows.

Architectural Foundations & Service Topology

The reference topology below shows how requests cross trust zones — from the public edge, through the application tier, and into the restricted data zone — with each boundary enforcing its own controls.

Reference request topology across trust zones A map or OGC client enters the public edge zone at the edge proxy. The edge forwards authenticated, TLS-terminated, rate-limited traffic to the GeoNode portal and cacheable tile traffic to the MapProxy cache in the application zone. The portal then reads and writes the PostGIS database and dispatches asynchronous jobs to the worker queue in the restricted data zone. PUBLIC EDGE ZONE APPLICATION ZONE RESTRICTED DATA ZONE authN · TLS · rate-limit cacheable tiles reads / writes async jobs Map / OGCclient Edge proxy /API gateway GeoNode portal MapProxy cache PostgreSQL /PostGIS Async worker queue

A production-grade geospatial portal decouples the web application framework from the components that render maps, manage metadata, and publish data. The Django-based GeoNode application owns catalog state, permissions, and the administrative UI; GeoServer (or a compatible OGC backend) executes GetMap, GetFeatureInfo, and WFS operations; MapProxy fronts tile-heavy WMS/WMTS traffic with a disk or object-store cache; PostgreSQL with the PostGIS extension holds spatial tables, and a Celery worker pool drains an async queue for thumbnail generation, metadata harvesting, and bulk imports. Treating each of these as an independently deployable, independently scalable unit is what lets the portal absorb spiky tile demand without coupling it to slow analytical writes.

The central composition decision is how much rendering and caching to fold into the application server versus a dedicated proxy layer. A tightly integrated deployment is simpler to reason about but couples cache invalidation to application releases and concentrates failure; a distributed proxy-and-render topology adds moving parts but isolates latency-sensitive tile paths from the metadata control plane. Those trade-offs decide request latency, cache-warming behaviour, and how far each tier scales horizontally, so platform teams should consult the GeoNode vs MapProxy Architecture Comparison to align component selection with throughput targets and operational maturity. Browser-based clients add a further constraint: any split between portal origin and tile origin must be reconciled with cross-origin policy, which is covered in Managing Cross-Domain CORS for OpenLayers Clients.

Whatever topology you choose, the provisioning of it must be declarative. Cloud resources belong in Terraform, Kubernetes workloads in Helm charts, and node-level configuration in Ansible roles, with every network policy, ingress rule, and service account version-controlled, reviewable, and reproducible across staging and production. The deeper provisioning patterns — module layout, immutable image promotion, reconciliation loops — are the domain of the Infrastructure Orchestration & Config Management guide; this page focuses on the boundaries those manifests must encode.

Security Boundary Mapping & Zero-Trust Enforcement

Geospatial portals expose a wide matrix of OGC-compliant endpoints, and each one must be governed without breaking interoperability or client compatibility. Boundaries are enforced first at the API gateway and reverse proxy, then reinforced with explicit egress controls on the data stores and rendering workers behind them. Network segmentation isolates public tile endpoints from administrative metadata APIs, from internal database clusters, and from the asynchronous message queue, so that a compromise in one zone does not grant lateral movement into the next.

When defining these perimeters, document exactly which protocols cross each trust zone and how TLS termination, mutual authentication, and payload inspection are applied at each hop, aligning the design with NIST SP 800-207 Zero Trust Architecture. The practical reference model for translating that doctrine into concrete OGC exposure rules lives in Security Boundary Mapping for OGC Services, which maps network policy directly onto service-exposure patterns. At the edge, TLS terminates at the reverse proxy, HTTP headers are validated, and OGC request parameters — REQUEST, LAYERS, BBOX, FILTER — are inspected before any request reaches a backend renderer. Pairing declarative Nginx vhosts with a ModSecurity rule set that targets malformed spatial queries hardens this layer; the baseline rule chains, CRS validation, and promotion hooks are detailed in Securing MapProxy with Nginx and ModSecurity.

Zero-trust here means no implicit trust between tiers even inside the Kubernetes cluster. Service-to-service traffic runs over mutually authenticated channels — mTLS via a service mesh or an internal CA — and every component assumes a least-privilege IAM role scoped to exactly the data and queues it needs. The enforcement points are explicit: the gateway authenticates and rate-limits external callers, a NetworkPolicy denies pod-to-pod traffic by default and allows only named flows, and the database accepts connections only from the application and worker service accounts. The Kubernetes-native expression of that default-deny posture looks like the policy below.

# default-deny-with-explicit-allows.yaml
# Deny all pod ingress in the data zone, then allow only the portal
# and worker service accounts to reach PostGIS on 5432.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: postgis-restricted-ingress
  namespace: data-zone
spec:
  podSelector:
    matchLabels:
      app: postgis
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              zone: application
          podSelector:
            matchLabels:
              role: portal
        - namespaceSelector:
            matchLabels:
              zone: application
          podSelector:
            matchLabels:
              role: celery-worker
      ports:
        - protocol: TCP
          port: 5432
Trust-zone boundary map with the control enforced at each crossing An inbound request descends from the untrusted internet through three boundaries. Crossing one into the public edge zone enforces TLS termination, the ModSecurity WAF, and rate-limiting. Crossing two into the application trust zone enforces mutual TLS plus JWT signature, audience, expiry, and tenant-claim validation. Crossing three into the restricted data zone enforces a default-deny NetworkPolicy and scoped IAM roles. UNTRUSTED INTERNET inbound request path 1 · TLS termination · WAF (ModSecurity) · rate-limit 2 · mTLS · JWT signature, aud / exp & tenant claim 3 · NetworkPolicy default-deny · scoped IAM roles Public edge zone Edge proxy / API gateway Application trust zone GeoNode · GeoServer · MapProxy · Celery Restricted data zone PostGIS · async worker queue

Identity, Access Control & Multi-Tenancy

Multi-tenant geospatial environments need granular access control to prevent data leakage and guarantee tenant isolation. Role-based access control belongs at two layers: the application layer, where GeoNode maps organisational roles to layer visibility, edit rights, and per-tenant API rate limits, and the data layer, where row-level security or per-tenant schemas stop a misconfigured query from returning another tenant’s features. The full placement model — where to enforce, how to scope cache keys per tenant, and how GetCapabilities is filtered to authorised layers — is documented in Implementing RBAC for Multi-Tenant GIS Portals, with the concrete agency walkthrough in How to Configure GeoNode User Roles for Agency Teams.

Agency deployments rarely manage credentials locally. They federate to an existing identity provider — Keycloak, Dex, or Azure AD — over SAML 2.0 or OIDC, so that single sign-on, session lifetime, and group membership stay under central control. The portal validates the resulting JWT at the ingress before any request reaches an OGC service, extracts tenant and role claims, and uses them for attribute-based routing and authorisation. Token validation must be the first hop, not an afterthought inside the application, because tile and feature requests fan out to multiple backends and re-validating downstream is both slow and error-prone.

OIDC / JWT request flow with gateway-side validation The client completes an authorization-code login with the identity provider (Keycloak or Dex), which issues a signed RS256 JWT. The client sends the request with the bearer token to the edge gateway. The gateway validates the JWKS signature, audience, expiry, and tenant claim at the gate, then proxies the request to GeoServer or MapProxy with scoped tenant and role headers. The application returns tiles or features to the client. Client Identity provider Keycloak / Dex Edge gateway GeoServer / MapProxy authorization-code login signed JWT (RS256) issued request + Bearer JWT validate at the gate JWKS sig · aud · exp · tenant claim required proxied + scoped headers tiles / features returned

The gateway-side validation contract is small and explicit: verify the signature against the IdP’s JWKS, check audience and expiry, and reject anything without a tenant claim before it is ever proxied.

# edge_jwt_gate.py — validate the IdP-issued token at the gateway,
# not inside each OGC backend. Runs as an auth subrequest / plugin.
import jwt
from jwt import PyJWKClient

JWKS_URL = "https://idp.example.gov/realms/geo/protocol/openid-connect/certs"
AUDIENCE = "geospatial-portal"
_jwks = PyJWKClient(JWKS_URL)

def authorize(auth_header: str) -> dict:
    if not auth_header.startswith("Bearer "):
        raise PermissionError("missing bearer token")
    token = auth_header.split(" ", 1)[1]
    signing_key = _jwks.get_signing_key_from_jwt(token).key
    claims = jwt.decode(
        token,
        signing_key,
        algorithms=["RS256"],
        audience=AUDIENCE,
        options={"require": ["exp", "aud", "iss"]},
    )
    if "tenant" not in claims:
        raise PermissionError("token carries no tenant claim")
    return {"tenant": claims["tenant"], "roles": claims.get("roles", [])}

Resilience, Routing & Operational Continuity

High-availability portals must absorb traffic spikes, upstream degradation, and regional outages without a hard failure surfacing to users. Tile generation is the most volatile path, so load balancing must spread render requests evenly across rendering nodes, and circuit breakers must trip before a slow backend drags the whole tier into cascading timeouts. When a primary tile cache is unavailable or saturated, the request should degrade — serve stale-while-revalidate content, fall back to a secondary cache, or render on demand — rather than return an error tile. Those mechanisms, including failover ordering and graceful-degradation paths, are specified in Fallback Routing Strategies for Tile Servers, and the end-to-end production wiring is walked through in Setting Up Fallback Tile Routing in Production.

Resilience also depends on honest health signalling. Liveness probes restart wedged processes; readiness probes pull a pod out of rotation while a cache warms or a database connection pool drains, so traffic is never routed to an instance that cannot serve it. Probe thresholds, autoscaling targets, and circuit-breaker counts should be tuned against real telemetry — request latency percentiles, cache hit ratio, queue depth — not guessed once and forgotten. At the database edge, unbounded connection pools turn a tile spike into a denial-of-service against PostGIS; capping sessions per tenant with PgBouncer and enforcing statement_timeout keeps a single heavy spatial join from exhausting the pool, as detailed in Optimizing PostgreSQL PostGIS Connection Limits.

Configuration-as-Code & GitOps Enforcement

Every boundary described above — network policy, ingress rule, RBAC binding, connection cap — is only trustworthy if it is declared, reviewed, and continuously reconciled. Imperative changes made by hand on a running cluster are invisible to audit and drift away from the intended posture within weeks. The discipline is to express the platform as code in three complementary layers and let a GitOps controller, rather than an operator’s shell, apply it.

Cloud-level resources — VPCs, subnets, managed Postgres, object storage for the tile cache — belong in Terraform, where remote state and plan review gate every change:

# postgis.tf — managed spatial database with private networking only.
# No public IP; reachable solely from the application subnet.
resource "google_sql_database_instance" "postgis" {
  name             = "geo-portal-postgis"
  database_version = "POSTGRES_15"
  region           = var.region

  settings {
    tier              = "db-custom-4-16384"
    availability_type = "REGIONAL" # synchronous HA standby

    ip_configuration {
      ipv4_enabled    = false                 # no public endpoint
      private_network = var.app_vpc_self_link # peered app VPC only
    }

    database_flags {
      name  = "max_connections"
      value = "200"
    }
    backup_configuration {
      enabled                        = true
      point_in_time_recovery_enabled = true
    }
  }
  deletion_protection = true
}

Cluster workloads — the portal, GeoServer, MapProxy, and the Celery pool — are templated in Helm so that replica counts, resource limits, and the trust-zone labels the network policies key on stay parameterised per environment:

# values.production.yaml — portal release, application trust zone.
portal:
  replicaCount: 4
  podLabels:
    zone: application
    role: portal
  resources:
    requests: { cpu: "500m", memory: "1Gi" }
    limits:   { cpu: "2",    memory: "4Gi" }
  readinessProbe:
    httpGet: { path: /healthz/ready, port: 8000 }
    initialDelaySeconds: 20
    periodSeconds: 10

celeryWorker:
  replicaCount: 6
  podLabels:
    zone: application
    role: celery-worker
  autoscaling:
    enabled: true
    minReplicas: 3
    maxReplicas: 12
    targetQueueDepth: 50 # scale on broker backlog, not CPU

A GitOps reconciler — Argo CD or Flux — watches the manifest repository and continuously converges live cluster state onto it, raising an alert and (optionally) self-healing whenever an out-of-band change introduces drift. The same pipeline runs policy validation before merge: terraform plan review, helm template | kubeconform schema checks, and synthetic OGC requests against the candidate config in staging. Promotion is a Git merge, rollback is a Git revert, and the audit trail is the commit history. The reconciliation-loop patterns, drift-detection tuning, and pipeline gates are expanded in the Infrastructure Orchestration & Config Management guide.

GitOps reconcile loop with drift detection A merge into the manifest repository of Terraform and Helm passes a CI gate that runs terraform plan, kubeconform schema checks, and a synthetic OGC test. Validated manifests flow to the reconciler, Argo CD or Flux, which converges the Kubernetes cluster onto Git. A dashed drift-detection path runs from the live cluster back to the repository, raising an alert and self-healing. Promotion is a git merge and rollback is a git revert. ! drift detected → alert · self-heal merge validated converge Manifest repo Terraform · Helm CI gate plan · kubeconform · OGC test Reconciler Argo CD · Flux Cluster live state promotion = git merge · rollback = git revert · audit = commit history

Operational Troubleshooting

When a request fails, the diagnostic question is always the same: which boundary rejected it? A systematic workflow correlates the HTTP status against the logs at each tier — edge, application, data — to localise the failure before changing anything. A 401 at the gateway means the JWT never validated; a 403 that reaches GeoServer means authentication succeeded but the layer ACL denied the role; a 502/504 points at an unhealthy or timed-out backend rather than an authorisation problem. Start at the edge access log, follow the request ID inward, and stop at the first tier that logged the rejection.

Symptom Likely boundary Where to look First fix
401 Unauthorized on every OGC call Edge JWT validation gateway access log, IdP JWKS reachability Confirm aud/iss claims and JWKS rotation; check clock skew
403 Forbidden on specific layers only Application RBAC GeoServer logs/geoserver.log, Django geonode.log Verify role-to-layer ACL and tenant claim mapping
502 Bad Gateway under load Edge → app routing Nginx error.log, readiness probe status Check pod readiness, raise upstream timeout, inspect circuit breaker
504 Gateway Timeout on GetMap Render / cache tier MapProxy log, GeoServer request log Warm cache, add render replicas, cap heavy BBOX requests
FATAL: remaining connection slots Data tier pool PostgreSQL postgresql.log, PgBouncer stats Lower per-tenant pool cap, enforce statement_timeout
CORS error in browser, server logs clean Edge CORS policy browser console, gateway response headers Align Access-Control-Allow-Origin with the tile origin

The browser-only failure mode is the easiest to misdiagnose, because the server returns 200 while the client blocks the response — the resolution path is in Managing Cross-Domain CORS for OpenLayers Clients. For protocol-level questions about which parameters and exceptions an OGC service must return, the authoritative reference is the OGC Web Service Standards. A quick edge-to-app trace for a single request ID makes the tier obvious:

# Follow one request across tiers by its propagated request id.
REQ_ID="b1f3c9a2"
kubectl -n edge        logs deploy/nginx       | grep "$REQ_ID"
kubectl -n application logs deploy/geonode      | grep "$REQ_ID"
kubectl -n application logs deploy/geoserver    | grep "$REQ_ID"
kubectl -n data-zone   exec deploy/pgbouncer -- \
  psql -p 6432 pgbouncer -c "SHOW POOLS;"

Extensibility is where regressions sneak back in: custom GeoNode apps and GeoServer extensions must run inside the same guardrails as the core, with explicit API contracts and pinned dependencies, or they reopen boundaries the platform just closed. Sandbox plugins, isolate their dependencies, and route their traffic through the same gateway validation as first-party services.

Operational Maturity Checklist

Maturity is the property that the boundaries above stay enforced without heroics. The following runbook items make each major concern auditable — a reviewer should be able to confirm every line from version-controlled config or a dashboard, not from tribal knowledge.

  • Topology — Every component (portal, GeoServer, MapProxy, PostGIS, Celery) is independently deployable, labelled with its trust zone, and provisioned from Terraform/Helm in the manifest repo.
  • Boundaries — A default-deny NetworkPolicy governs every namespace; TLS terminates at the edge; mTLS or an internal CA secures service-to-service calls; egress from the data zone is explicitly listed.
  • Identity — JWTs are validated at the gateway before any OGC backend; tenant and role claims drive routing; IdP federation (Keycloak/Dex/Azure AD) is the only credential source; sessions expire centrally.
  • Multi-tenancy — RBAC is enforced at both application and data layers; cache keys are tenant-scoped; GetCapabilities is filtered to authorised layers; cross-tenant access is tested, not assumed.
  • Resilience — Liveness and readiness probes are distinct and tuned to telemetry; circuit breakers and fallback tile routing are configured; PgBouncer caps connections per tenant with enforced timeouts.
  • Configuration-as-code — A GitOps reconciler converges the Kubernetes cluster on Git; drift triggers an alert; promotion is a merge and rollback is a revert; CI gates run plan review, schema validation, and synthetic OGC tests.
  • Observability & audit — Request IDs propagate edge-to-data; per-tier logs are aggregated and searchable; regular security audits, automated compliance scans, and capacity reviews are scheduled, not ad hoc.

Holding these lines — strict service boundaries, zero-trust networking, and declarative configuration converged by GitOps — is what lets an open-source geospatial portal meet enterprise and government compliance bars while continuing to evolve with new spatial workloads and threats.