System Security & Access Boundaries

In modern flight operations and crew scheduling environments, system security and access boundaries function as the operational perimeter that separates regulatory rule evaluation from raw crew data, pairing optimization engines, and external scheduling interfaces. These boundaries are not merely network-level firewalls; they are logical enforcement layers embedded directly into the compliance architecture. When designing or maintaining these perimeters, engineering teams must align data-flow controls with the structural requirements of the broader Core Architecture & Regulatory Mapping framework. Without precise boundary definitions, automated schedulers risk exposing sensitive duty records, bypassing regulatory validation gates, or propagating permission drift across microservices.

Access boundary enforcement gates An inbound sync payload or API request first hits an authentication gate that checks the OAuth2 scope and mutual TLS; a failure is rejected at the boundary. On success the request reaches a second gate that validates the JSON schema and RBAC role; a failure is likewise rejected. Only a request that clears both gates reaches the stateless read-only rule engine, which writes to the signed audit log. If the primary store is unreachable the engine degrades closed to a cached conservative rule snapshot, which also feeds the signed audit log. Sync payload / API request OAuth2 · mTLS scope valid? JSON schema + RBAC role? Rule engine stateless · read-only Signed audit log hash-chained Reject at boundary Cached conservative rule snapshot · degrade closed yes yes no no primary down
The access boundary as a logical enforcement layer: requests must clear the authentication and schema-plus-RBAC gates before reaching the read-only rule engine; failures are rejected, and a primary-store outage degrades closed to a cached conservative snapshot rather than failing open.

The Boundary Problem This Topic Solves

The scoped challenge here is narrow and security-critical: given a scheduling platform where dozens of services, human operators, and third-party rostering tools all reach for the same duty-time data, guarantee that every read and every write crosses a boundary that is authenticated, authorized, schema-validated, and audit-logged — without adding latency that breaks real-time crew swaps. A compliance system is only as trustworthy as the weakest path to its data. If a custom ETL job can reuse an elevated service account to poll duty records, or if a rostering vendor can write a field the data contract never authorized, then the deterministic verdicts produced downstream by the duty time validation rule engines are no longer defensible in an audit.

Three properties make this boundary hard to get right. First, the same crew-duty record is read by services with radically different privilege needs — a read-only rule evaluator, a write-capable dispatch API, and a read-only regulator export — so a single coarse credential is never adequate. Second, boundary decisions must be stateless and reproducible: an inspector must be able to replay why a request was admitted or rejected months later, which forces every gate to emit a signed, hash-chained record. Third, the boundary must degrade closed — when the primary compliance store is unreachable, the safe failure is to enforce conservative limits, never to wave traffic through. These three properties, not network topology, are what this topic addresses.

Schema and Access-Contract Design

The boundary is modeled as data, not as scattered if statements. Four entities capture it: a principal (the authenticated identity behind a request — a service or an operator), a role that grants one or more scopes (fine-grained, action-plus-resource entitlements), a data contract that pins the exact field set a principal may read or write, and an append-only audit event that records every admitted or rejected crossing. Keeping scopes and contracts in their own effective-dated tables is what lets a permission change be reviewed as a diff and rolled back without redeploying the scheduler.

Access-contract entity-relationship diagram A principal (a service or operator identity) holds one role. A role grants many scopes through the role_scope link table, and a scope may be granted to many roles. Each scope pins exactly one data contract: an explicit field allowlist tagged read or write, so a payload is projected down to those fields at the boundary. Separately, every crossing the boundary admits or rejects appends one audit_event keyed on principal_id; each event folds the previous event's hash into its own entry_hash, forming a tamper-evident chain. principal principal_id PK principal_kind is_active role role PK scheduler · auditor dispatcher scope scope_id PK action read / write resource data_contract field_allow[] allowlist projects payload read | write audit_event event_id PK principal_id FK decision admit/reject prev_hash → entry_hash 1 : 1 · holds N : M · role_scope 1 : 1 · pins 1 : N · writes hash-chained · append-only
The access contract modeled as data: a principal holds one role, roles grant scopes through the role_scope link, each scope pins a field-allowlist data contract, and every admitted or rejected crossing appends one hash-chained audit event keyed on the principal.
CREATE TABLE principal (
    principal_id   uuid PRIMARY KEY,
    display_name   text        NOT NULL,
    principal_kind text        NOT NULL
        CHECK (principal_kind IN ('service', 'operator')),
    is_active      boolean     NOT NULL DEFAULT true
);

CREATE TABLE scope (
    scope_id     uuid PRIMARY KEY,
    action       text NOT NULL CHECK (action IN ('read', 'write')),
    resource     text NOT NULL,                 -- e.g. 'duty_record', 'rule_snapshot'
    field_allow  text[] NOT NULL,               -- explicit field allowlist
    effective_at timestamptz NOT NULL,
    retired_at   timestamptz                    -- NULL while current
);

CREATE TABLE role_scope (
    role         text NOT NULL,                 -- 'scheduler' | 'auditor' | 'dispatcher'
    scope_id     uuid NOT NULL REFERENCES scope (scope_id),
    PRIMARY KEY (role, scope_id)
);

CREATE TABLE audit_event (
    event_id     bigserial PRIMARY KEY,
    occurred_at  timestamptz NOT NULL DEFAULT now(),
    principal_id uuid NOT NULL REFERENCES principal (principal_id),
    decision     text NOT NULL CHECK (decision IN ('admit', 'reject')),
    reason       text NOT NULL,
    prev_hash    bytea NOT NULL,                -- hash of the preceding event
    entry_hash   bytea NOT NULL                 -- HMAC over this row + prev_hash
);

The field_allow array is the mechanical heart of the contract: a boundary never trusts a payload’s shape, it projects the payload down to the allowlisted fields and rejects anything that arrives outside them. The audit_event table is deliberately append-only and hash-chained through prev_hash, so any tampering with a historical decision breaks the chain and is detectable on replay.

Regulatory Mapping

The boundary exists because specific regulations make data segregation and record integrity mandatory, not optional. Under 14 CFR Part 119 and the operational-control provisions it establishes, an operator must be able to demonstrate who was authorized to alter operational records and when. The duty-time limits the boundary protects are themselves defined by 14 CFR Part 117: §117.13 keys the flight-duty-period ceiling to report time and number of segments, §117.19 layers extension provisions on top, §117.23 imposes the cumulative 60-hour/168-hour and 190-hour/672-hour caps, and §117.25 fixes rest requirements. Because a §117.23 verdict depends on a crew member’s entire recent history, the read path into that history is exactly the surface the boundary must lock down — a single leaked or mutated duty record silently corrupts every downstream cumulative total.

For carriers operating into European airspace, the equivalent obligations flow from the EASA FTL compliance framework: ORO.GEN.205 governs contracted activities and the data handoffs they imply, while ORO.FTL.205 and ORO.FTL.210 define the flight-duty-period and cumulative limits whose inputs must not be tampered with in transit. The EASA Easy Access Rules for Air Operations consolidate the current Annex III (Part-ORO) requirements that boundary policies must reflect. Access boundaries enforce jurisdictional separation by routing EASA-specific validation calls through isolated service partitions that reject cross-tenant credential reuse, preventing regulatory bleed where a permissive FAA rest calculation could otherwise override a stricter EASA cumulative-duty limit.

Python Implementation Walkthrough

The evaluation layer consumes normalized duty data and returns pass/fail determinations, so its boundary contract is strictly read-only. A typed model expresses that contract explicitly, and every request is projected to its allowlisted fields before any rule logic runs. The example below uses only the standard library plus Pydantic-style typing conventions; token generation uses Python’s secrets and signing uses hmac from the standard library.

import hashlib
import hmac
from dataclasses import dataclass, field
from datetime import datetime, timezone


@dataclass(frozen=True)
class Scope:
    action: str          # 'read' | 'write'
    resource: str        # e.g. 'duty_record'
    field_allow: frozenset[str]


@dataclass(frozen=True)
class Principal:
    principal_id: str
    role: str            # 'scheduler' | 'auditor' | 'dispatcher'
    scopes: tuple[Scope, ...] = field(default_factory=tuple)


class BoundaryError(Exception):
    """Raised when a request fails an access-boundary gate."""


def authorize(principal: Principal, action: str, resource: str) -> Scope:
    """Return the matching scope or reject the crossing."""
    for scope in principal.scopes:
        if scope.action == action and scope.resource == resource:
            return scope
    raise BoundaryError(
        f"{principal.role} lacks {action}:{resource}"
    )


def project(payload: dict, scope: Scope) -> dict:
    """Strip every field outside the authorized data contract."""
    extra = set(payload) - scope.field_allow
    if extra:
        raise BoundaryError(f"unauthorized fields: {sorted(extra)}")
    return {k: payload[k] for k in payload if k in scope.field_allow}

The authorize and project functions are the two gates every crossing passes: the first proves the principal may touch the resource, the second proves the payload only carries fields the contract permits. Neither gate can be skipped by a downstream service because the rule engine never receives a raw payload — it receives the projected result. Each decision is then committed to the hash-chained log:

def append_audit(secret: bytes, prev_hash: bytes,
                 principal_id: str, decision: str, reason: str) -> bytes:
    """Append one tamper-evident audit entry, return its hash."""
    body = "|".join([
        datetime.now(timezone.utc).isoformat(),
        principal_id, decision, reason,
    ]).encode("utf-8")
    entry_hash = hmac.new(
        secret, prev_hash + body, hashlib.sha256
    ).digest()
    return entry_hash

Because entry_hash folds in prev_hash, the log is a chain: recomputing any earlier link fails if a single historical field was altered, which is what makes the trail defensible under inspection.

Rolling-Window Audit Aggregation

Boundary security is not only about single requests — it is about detecting drift and abuse over time. Privilege-escalation attempts, credential reuse, and reject storms only become visible when access events are aggregated across a rolling window. PostgreSQL window functions express this cleanly against the audit_event table, counting rejects per principal over a trailing interval so an alerting job can flag principals whose reject rate spikes.

SELECT
    principal_id,
    occurred_at,
    decision,
    count(*) FILTER (WHERE decision = 'reject')
        OVER (
            PARTITION BY principal_id
            ORDER BY occurred_at
            RANGE BETWEEN INTERVAL '1 hour' PRECEDING AND CURRENT ROW
        ) AS rejects_last_hour
FROM audit_event
ORDER BY principal_id, occurred_at;

A trailing RANGE window keyed on occurred_at gives a live rejects-per-hour figure per principal without a self-join; a downstream policy simply thresholds rejects_last_hour to quarantine a misbehaving service account. The same shape, widened to a 24-hour or 7-day window, surfaces slow-burn escalation attempts that a single-request check can never catch.

Integration Points

The boundary is a seam, so it is defined as much by its neighbors as by itself. Upstream, it fronts flight data ingestion: every ACARS, IATA, or crew-management payload crosses the boundary’s authentication and schema gates before it is admitted to the normalized store, and third-party rostering payloads are projected to their data contract on entry. Laterally, the boundary depends on the crew duty time taxonomy mapping to know what it is protecting — the taxonomy’s canonical field names are exactly the identifiers that populate each scope’s field_allow list, so a taxonomy change and a contract change are reviewed together.

Downstream, the boundary hands projected, read-only data to the rule engines that apply the FAA Part 117 rule schema design and its EASA counterpart, and it captures their verdicts in the signed audit trail. This is why the evaluation layer is stateless: it holds no credentials of its own and can only ever read what the boundary already vetted, which keeps the trust surface small and the verdicts reproducible.

Testing, Edge Cases, and Fail-Safe Degradation

Boundary logic is validated with property-based tests rather than a handful of examples, because the security guarantee is universal: no payload should ever reach the engine carrying a field outside its contract, and no principal should ever be admitted to a scope it lacks. A generative test asserts that project is idempotent and that its output is always a subset of field_allow, across randomly mutated payloads.

from hypothesis import given, strategies as st

_scope = Scope("read", "duty_record",
               frozenset({"crew_id", "duty_start", "duty_end"}))


@given(st.dictionaries(st.text(), st.integers()))
def test_projection_never_leaks_unlisted_fields(payload):
    try:
        result = project(payload, _scope)
    except BoundaryError:
        return  # rejecting an out-of-contract payload is correct
    assert set(result).issubset(_scope.field_allow)

Three edge cases dominate boundary correctness. First, degrade closed: when the primary compliance store is partitioned, the boundary must switch to a cached, cryptographically verified rule snapshot that enforces conservative limits — a request must never fail open. Second, idempotent retries: a crew swap replayed after a failover must not double-write; every write carries an idempotency key so a duplicate payload is a no-op, not a corrupted duty record. Third, credential rotation mid-request: a token that expires between the auth gate and the write must invalidate the whole crossing, not just the tail, which is why the boundary binds the decision to a single authenticated context and logs it atomically.

Explore This Topic in Depth

Back to Core Architecture & Regulatory Mapping.

Explore this section