Concepts
Security & compliance
Authentication modes, signing requirements, data-classification model, and the seven Ugandan regulatory frameworks the architecture addresses by design — not by retrofit.
Authentication
Every endpoint outside the public catalogue requires a Bearer token. Tokens are scoped per integrator, carry a documented set of permissions, and are revocable at any time without affecting other clients. Both Authorization: Bearer <key> and X-Sente-Authorization: Bearer <key> are accepted on every authenticated endpoint — use the standard Authorization header for compatibility with the bulk of client libraries; the custom header is retained for cases where another middleware on the network path may strip or rewrite Authorization.
Bearer tokens
Default for server-to-server integrations. The token is opaque, ~40 characters, prefixed with sk_sandbox_ in sandbox or sk_live_ in production. Send it on every request:
GET /v1/citizens HTTP/1.1
Host: sente-rails.space
X-Sente-Authorization: Bearer sk_sandbox_xxxxxxxxxxxxxxxxOAuth 2.0 client_credentials
For partner platforms (a city ERP, an enterprise integrator) the rail issues OAuth 2.0 client_credentials tokens with configurable scopes — for example citizens.read, assessments.write, oversight.read. The flow follows RFC 6749 §4.4 with no client-side deviations; any standard OAuth library works without modification.
mTLS for high-risk endpoints
Payment confirmation webhooks, oversight read endpoints under the OAG scope, and credential rotation are gated behind mutual TLS in addition to the Bearer token. The rail pins the client certificate against a registered per-integrator fingerprint set.
Request signing
Inbound webhooks from aggregators (MoMo, Airtel) and EFRIS-side callbacks are signed with HMAC-SHA256 over the raw request body using a shared secret. Verify before parsing:
import hmac, hashlib
def verify(secret: str, body: bytes, signature_header: str) -> bool:
expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
# Constant-time compare against the value carried in X-Sente-Signature
return hmac.compare_digest(expected, signature_header)Always verify against the raw body
JSON re-serialisation reorders keys and changes whitespace; the signature is computed against the bytes on the wire. Verify before you parse, or buffer the raw body separately from your parser.
Data classification
Three tiers govern how the rail handles each data class. Higher tiers carry stricter logging, retention, and access controls.
- Tier 1 — Personal. National Identification Numbers, phone numbers, addresses, consent metadata. Never appears in URLs or log lines; only in indexed body fields with role-gated read. Soft- archive on erasure request preserves referential integrity for paid receipts under PFMA retention rules.
- Tier 2 — Financial. Payment Intent references, aggregator transaction IDs, FDN values, assessment totals. Audit- logged on every read and write. Aggregator-side balances are never persisted by the rail.
- Tier 3 — Operational. MDA catalogue, service catalogue, public statistics, integration status. Catalogue endpoints are public-read; statistics aggregate only.
Regulatory frameworks
Seven Ugandan regulatory frameworks fall within scope. Each is addressed by design — the architecture does the regulatory work, not a retrofitted policy layer.
| Framework | Architectural posture | Evidence |
|---|---|---|
Personal Data and Privacy Act 2019 | Consent metadata is captured on every Citizen record. National Identification Numbers never appear in URLs or document names. Right-to-erasure is honoured via soft-archive (status flip + audit trail) so referential integrity on paid receipts stays intact. | Citizen.consent_data_sharing · ARCHITECTURE.md §5 · /v1/citizens schema |
Tax Procedures Code Act 2014 §73A–73B | Every Service with efris_taxable=true routes through the EFRIS fiscal adapter at assessment time. Each Assessment Line carries a per-line Fiscal Document Number. The full fiscal round-trip — generate PRN, post invoice, retrieve FDN — runs live end-to-end in the sandbox. | sente_rails/adapters/fiscal/uganda_efris.py · Service.efris_taxable · Assessment Line.efris_fdn |
Public Finance Management Act 2015 §43 | The rail never holds public money. Citizen payments flow directly from the citizen's wallet to a licensed aggregator on a per-MDA payable account. Cross-MDA assessments split at the aggregator, never at Sente Rails. There is no single rail wallet that accumulates revenue. | ARCHITECTURE.md §7 · MDA.treasury_account · Payment Intent Split per-MDA targets |
e-Government Interoperability Framework | API-first by construction. The complete /v1 surface is documented in OpenAPI 3.1 across twenty-two endpoints in seven modules. All payloads are JSON over REST. The UGHub gateway adapter is scaffolded for the standard NITA-U integration path. | sente_rails/api/v1/openapi.yaml · /docs/explorer · adapters/gateway/uganda_ughub.py |
Access to Information Act 2005 | Oversight bodies (OAG, MoFPED, UBOS, MoLG) operate as Mode C Read Consumers — scoped reads only, never collect on behalf of any MDA. Aggregate statistics are open by default; itemised reads require role-scoped credentials with full audit logging. | MDA(mode=C, oversight_scopes=...) · /agencies?status=Inquiry · ARCHITECTURE.md §3 |
Computer Misuse Act 2011, amended 2022 | Authentication logs are immutable. Rate limiting at the nginx edge plus per-endpoint application throttling. The underlying platform's administrative interface (/app, /desk) returns 404 at the edge — there is no public administrative surface. Intrusion detection and penetration testing are sequenced for pre-production hardening. | /etc/nginx/conf.d/sente-bench.conf · sente_rails/auth.py · platform Version doctype |
National Payment Systems Act 2020 | All payment processing is mediated by licensed aggregators — MTN MoMo (sandbox live), Airtel Money (sandbox pending), Pesapal (planned). Card and bank settlement routes are scaffolded for production. Sente Rails never registers as a PSP; by architectural posture it does not need to. | sente_rails/adapters/payment/*.py · site_config momo_sandbox block |
Audit trail
Every state-changing operation is captured in an immutable audit log: actor, timestamp, target document, before / after payload. Oversight Mode C consumers read against this log scoped to their statutory remit.
{
"doctype": "Assessment",
"name": "ASSESS-2026-000123",
"action": "submit",
"actor": { "user": "clerk@gulu.gov.ug", "role": "Sente Rails Clerk" },
"context": { "shift": "SHIFT-2026-000045", "mda": "GULU", "ip": "10.x.x.x" },
"before": null,
"after": { "status": "Assessed", "total_amount": 50000 },
"occurred_at": "2026-05-25T08:30:11Z"
}Production hardening
Two items remain queued for the pre-production pass before any live traffic moves through the rail:
- Intrusion detection. Edge-side request anomaly scoring with per-integrator baselines. Slated for the production hardening pass.
- Formal penetration test. Independent third-party test against the live URL, results published as an appendix to this documentation under the API explorer.
Source of truth
The standalone compliance matrix — the same content as the table above, on one page — lives in the repository at docs/COMPLIANCE_MATRIX.md. Every row above quotes it.