Afunana is an on-premise legacy-system intelligence platform. It reads the source that runs the business — COBOL, RPG, CL, DDS, SQL, and PL/SQL across IBM i, Oracle, and (roadmap) z/OS — and reconstructs the knowledge inside it. Because that source is often a bank's, insurer's, or government's most sensitive asset, security is a first-class design constraint, not a bolt-on.
The whole platform runs inside the customer's own network. Nothing about the analyzed systems leaves the customer boundary unless the customer explicitly configures an outbound integration (an LLM provider, a SIEM, or the source system itself). For fully air-gapped estates, Afunana runs offline against a local model with no vendor access at all — see On-Premise and Air-Gapped Operation.
This document describes the controls. Compliance & Standards maps those controls to OWASP, ISO/IEC 27001, and SOC 2.
Authentication
JWT Tokens
- Algorithm: HS256 with a mandatory signing key of at least 32 characters (256 bits).
- The key (
JWT_SECRET) is a bootstrap key — resolved from a Docker secret first, then an environment variable, and never from the database (see Secret Management). - Enforcement is at startup: the process refuses to boot if
JWT_SECRETis unset or shorter than 32 characters. There is no insecure default and no fallback key. - Tokens carry the subject (username), role, issued-at (
iat), expiry (exp), a unique token ID (jti), and asession_startclaim that fixes the absolute session origin across refreshes.
Password Security
- Passwords hashed with bcrypt (per-password salt; input truncated to bcrypt's 72-byte limit).
- Complexity is enforced on signup, password change, and password reset: minimum 8 characters, with at least one uppercase letter, one lowercase letter, one digit, and one special character.
- Passwords are never logged or stored in plaintext. Password-reset tokens are stored only as SHA-256 hashes with a short expiry.
- Account-enumeration defense. The forgot-password flow returns a generic response regardless of whether the address exists, and the login path equalizes response timing between an unknown-user outcome and a wrong-password outcome, so neither the response body nor its timing reveals whether a username is valid.
- SSO users authenticate against their identity provider; a random unusable password is set at provision time so no password path exists for them (see Single Sign-On).
Account Lockout
Repeated failed logins lock an account for a cooldown period. The lockout is persisted to the database, so it survives an application restart — an attacker cannot reset it by forcing a container bounce — and an in-memory tracker backs it up if the database write is unavailable.
- The failed-attempt counter clears on successful login. Lockout events are written to the security audit log at
alertseverity. - The same lockout check guards the SSO callback path, so federated logins cannot bypass it.
Session Management
| Parameter | Value |
|---|---|
| Idle timeout | Configurable; expires a session after a period of inactivity |
| Absolute session lifetime | Configurable; ships at 30 days, enforced on every refresh and not extendable |
| Refresh mechanism | a token-refresh call issues a fresh idle window, preserving the original session origin |
| Token revocation (per-token) | On logout, the token is revoked and rejected thereafter |
| Token revocation (per-user) | Admin can invalidate all of a user's sessions, or every session system-wide |
| Cross-tab sync | Logout propagates across open browser tabs |
Two independent clocks bound every session. The idle timeout expires a token after a period of inactivity; the frontend issues a refresh on user activity to extend it. The absolute lifetime (the fixed session origin plus the configured maximum age) is enforced on every refresh and cannot be extended — once the maximum session age is reached, the user must re-authenticate regardless of activity.
Revocation operates at two granularities. Per-token: logout records the token's unique identifier in the revocation store, and every authenticated request checks the presented token against it. Per-user: an administrator can revoke all active sessions for a named user (deactivation, credential compromise) or, in an emergency, force every user to re-authenticate. Both are exposed through the compliance surface and are fully audited. Expired revocation entries are cleaned up by a daily maintenance task.
Authorization
Role-Based Access Control (RBAC)
Four roles govern access throughout the system.
| Role | Permissions |
|---|---|
admin |
Full access — user management, configuration, builds, chat, and all data. |
qa |
Read access to analysis results, documentation, and project-management views. Cannot manage users or configuration. |
user |
Read and chat access to assigned collections. |
viewer |
Read-only access to analysis results and documentation for assigned collections. |
Authorization is enforced in two layers:
- Backend (authoritative): dependency decorators run on every protected endpoint.
require_admingates administrative operations;require_qa_or_abovegates elevated read access (admin or qa). Requests without the required role return 403. The backend is the source of truth — it does not trust the client. - Frontend (usability): route guards prevent navigation to pages a role cannot use, so users are not shown dead ends. These guards are a convenience, not a security boundary; the backend re-checks every call.
Finer-grained access is enforced by per-collection access control lists. A user sees and queries only the collections assigned to them; the API enforces the collection boundary on data access, chat, and export. Role and collection ACL are independent — role sets what kind of operation is allowed, the ACL sets which collections it may touch.
Single Sign-On
Afunana supports OpenID Connect (OIDC) single sign-on. It has been used with Azure AD (Microsoft Entra ID), Okta, Google Workspace, and generic OIDC providers; provider discovery URLs for Azure/Okta/Google are built in, and any compliant provider works via an explicit discovery URL.
The authorization-code flow includes the following protections:
- Strict ID-token validation. The ID token is verified against the provider's published JWKS signing keys, with issuer and audience checks and an expiry check (with a small clock-skew leeway). If the signature cannot be verified against a matching JWKS key, the token is rejected — there is no fall-through to an unverified decode.
- SSRF-protected discovery. Before any OIDC discovery or JWKS document is fetched, the target URL is validated: it must be HTTPS, and its resolved address must be a public IP. Requests that resolve to private, loopback, link-local, or reserved ranges are refused, so a malicious or misconfigured discovery URL cannot be used to reach internal services. Discovery documents are cached (1 hour) and checked for the required OIDC endpoints.
- CSRF state + nonce replay protection. Each login mints a cryptographically random
state(CSRF binding) andnonce. Thestateis single-use and time-boxed (10 minutes) and consumed on callback; thenonceis checked against the returned ID token to defeat token replay. - Open-redirect protection. The post-login redirect target must start with the configured
FRONTEND_URL; anything else is rejected.
Operational controls:
- Auto-provisioning (optional). When enabled, a first-time SSO user is created automatically with the configured default role (
user,qa, orviewer— neveradmin). When disabled, the user must already exist, and an unmatched login is denied and recorded. - Domain allow-list. Administrators can restrict SSO to specific email domains; a login from a disallowed domain is denied and audited.
- Full audit trail. Every SSO step — initiation, callback, token exchange, provisioning, domain rejection, lockout — is written to the security audit log.
The client secret is stored in configuration and masked when the SSO settings are read back through the admin API, and redacted in audit detail.
CSRF Protection
For the local (non-SSO) session, CSRF defense uses Origin validation. Every mutating request is checked against:
- The configured CORS origins.
- The
FRONTEND_URLvalue from the database. - The
Hostheader of the incoming request.
Requests with a mismatched or missing Origin are rejected. Origins are resolved from multiple sources so that CORS keeps working across the supported deployment topologies (bundled Caddy, external proxy, separate proxy host) rather than depending on one static list.
Rate Limiting
Rate limiting is applied per client (keyed by remote address); exceeding a limit returns HTTP 429. Authentication endpoints (login, sign-up, forgot-password) carry the strictest per-endpoint limits, and a broader per-client request limit also applies across the API. Rate limiting works alongside account lockout: the rate limiter throttles request volume from an address, while lockout stops credential-guessing against a specific account.
Security Headers
The application itself sets the security headers (X-Frame-Options, HSTS, Content-Security-Policy, and more) in middleware — not the proxy — so they apply even behind a customer's own reverse proxy.
| Header | Value | Purpose |
|---|---|---|
Content-Security-Policy |
Restrictive policy | Limits script/style/connect sources; mitigates XSS |
Strict-Transport-Security |
max-age=31536000 |
Forces HTTPS for one year |
X-Frame-Options |
DENY |
Prevents clickjacking |
X-Content-Type-Options |
nosniff |
Prevents MIME-type sniffing |
Referrer-Policy |
strict-origin-when-cross-origin |
Limits referrer leakage |
Audit Logging
Audit logging is the backbone of Afunana's accountability story. Every security-relevant action funnels through a single logging entry point and lands in a dedicated audit table that is INSERT-only and tamper-evident.
Tamper-Evident Hash Chain
The audit log is a tamper-evident SHA-256 hash chain. Each record carries an integrity hash computed over the previous record's hash plus the current record's identifying fields (id, event id, timestamp, event type, actor, outcome). Because every record's hash links to the previous one, altering or deleting any historical record breaks the chain from that point forward, and the break is detectable. An optional external hash seed can anchor the chain when configured.
Immutability is enforced at the database layer, not just in application code: a database trigger blocks any UPDATE or DELETE of logged events and rolls the statement back. The only writer that may bypass it is the scheduled retention purge, which briefly suspends that protection, deletes only records past the retention window (and, when SIEM forwarding is on, only records already confirmed forwarded), then restores it — all under audit.
An integrity-verification routine walks the stored chain and reports whether it is intact and, if not, the id of the first mismatch. It is run on a schedule (below) and on demand through the compliance surface.
Event Categories
Seven categories are logged, each event attributed to an actor, IP, user agent, target, and outcome:
| Category | Examples |
|---|---|
auth |
Login success/failure, logout, lockout, token refresh, password change/reset |
authz |
Permission denied, role changes |
data |
Collection/source access, build triggers, data and audit exports |
chat |
Chat queries and errors (query text is logged only when chat-content auditing is explicitly enabled — off by default, for privacy) |
admin |
User management, configuration changes, SSO changes, session revocation |
system |
Startup/shutdown, deploy, errors, log purge |
integrity |
Hash-chain verification results, checkpoint exports |
Categories, minimum severity, and retention are all configurable in app_config.
SIEM Forwarding
Events can be forwarded to a customer SIEM in real time. Forwarding happens before the local insert (and the delivery timestamp is written into that same insert row), precisely because the immutability trigger forbids a later UPDATE.
- Syslog transport (RFC 5424):
tcp+tls(default, port 6514),tcp, orudp. Fortcp+tls, the SIEM's CA certificate is loaded from a mounted secret and the server certificate is validated. - HTTPS webhook transport: POST to an HTTPS collector (for example Microsoft Sentinel, Splunk HEC, Datadog) with an optional bearer token, validated against a mounted CA when present.
- Formats: CEF or JSON.
Endpoint, port, protocol, and format are set in the admin UI or app_config.
Integrity & Retention Scheduler
A background scheduler runs automated compliance maintenance:
| Interval | Task |
|---|---|
| Hourly | Hash-chain integrity check (recent records) |
| Daily | Full hash-chain verification |
| Daily | Checkpoint export (audit-state snapshot) |
| Daily | Log purge (retention window; default 365 days, configurable) |
| Daily | Expired-token-revocation cleanup |
Integrity failures raise a critical event that itself forwards to the SIEM.
Secret Management
Secret resolution follows two deliberately different precedence chains, because bootstrap secrets are needed before the database is reachable.
Bootstrap keys — DB_*, JWT_SECRET, SERVER_ROLE:
| Priority | Source |
|---|---|
| 1 | Docker secret |
| 2 | Environment variable |
| — | Never the database |
These identify and unlock the database itself, so they cannot depend on it. The application fails loud at startup if a required bootstrap key is missing or invalid.
General secrets — API keys, SMTP credentials, SIEM tokens, and the like — resolve through a layered hierarchy. When an external secret vault is configured it is consulted first, above the on-box layers, with the environment as a final fallback:
| Priority | Source | Notes |
|---|---|---|
| 1 (highest) | External secret vault (CyberArk CCP or HashiCorp Vault) | When configured, secrets are resolved from the enterprise vault first |
| 2 | Writable on-box secrets layer | Admin-UI-set values win, and take effect with no restart |
| 3 | Mounted container secret | Read-only |
| 4 | Database configuration | Cold-start fallback |
| 5 (lowest) | Environment variable | Final fallback |
A configured external vault (CyberArk CCP or HashiCorp Vault) is consulted first; otherwise the runtime path is the configuration cache with the on-box writable layer, mounted secrets, and finally the environment as fallbacks. The writable on-box layer lets an operator set or rotate a secret through the admin UI and have it apply immediately. Secrets are never logged, and sensitive keys (DB password, JWT_SECRET, deploy secret, API keys) are masked in audit detail.
Encryption
In Transit
- TLS everywhere. The bundled Caddy proxy terminates TLS with automatic certificates and enforces HSTS (
max-age=31536000). In external-proxy deployments the customer's proxy terminates TLS to the same standard. - Database connections use
Encrypt=yes. - Outbound integrations — OIDC discovery/JWKS, SIEM forwarding, and LLM calls — run over TLS.
- Internal service-to-service traffic stays on the container network and is not exposed to the wider network.
At Rest
- SQL Server Transparent Data Encryption (TDE), AES-256. In bundled-DB mode (Afunana provisions the database), TDE is automatically enabled — database files and backups are encrypted at rest. In external-DB mode, the database is owned and operated by the customer, so TDE (or an equivalent at-rest control) is the customer's responsibility to configure on their instance; Afunana does not, and cannot, enable it for them. This distinction matters for audit evidence: only bundled-DB deployments can point to Afunana as the party that enabled at-rest encryption.
- Passwords are stored only as bcrypt hashes; reset tokens only as SHA-256 hashes.
Container Security
- Non-root execution. The entrypoint fixes file ownership and then drops privileges to an unprivileged
appuser; the application does not run as root. - Minimal production image. A multi-stage build keeps compilers and build tooling out of the final image.
- Internal-only data services. The database and the DB admin tool are not published to the public network; they are reachable only on the internal container network.
- Runtime portability. Runs under Docker or Podman on RHEL, AlmaLinux, Rocky, Ubuntu, and Debian. In bundled mode the application connects with a least-privilege database login rather than a superuser account.
On-Premise and Air-Gapped Operation
Afunana is designed to run entirely inside the customer's perimeter with zero vendor access to the analyzed systems or data.
- Nothing leaves the network by default. Source, documentation, and chat all stay local. The only outbound connections that exist are ones the customer explicitly configures: an LLM provider, a SIEM, and the source system (IBM i / Oracle) being analyzed.
- Fully air-gapped mode. The One AI Hub can route every LLM call to a local model via Ollama, and embeddings are generated by a local model (
intfloat/multilingual-e5-large) — no external embedding API is ever called. Afunana can be installed from an offline image bundle against an internal registry mirror. In this configuration no data — and no LLM prompt — leaves the box. - No single-vendor dependency. The multi-provider AI Hub (Anthropic, OpenAI, Azure OpenAI, Ollama) means a provider outage or a policy decision to stop using a cloud model is a configuration change, not a rebuild.
Vulnerability Scanning
Every deploy runs three scanners:
| Tool | Scope | Purpose |
|---|---|---|
| pip-audit | Python dependencies | Known-CVE detection |
| npm audit | JavaScript dependencies | Known-CVE detection |
| Bandit | Python source | Static analysis (SAST) |
Scans are non-fatal — results are recorded but do not block a deploy — so awareness is continuous without a low-severity finding halting a production release. The decision to act on a finding stays with the operator.
Configurable Quality Checks
The code-quality check catalog is configurable per check (exposed via admin check-settings endpoints). Each check can be set to error, note, or off, controlling whether and how it surfaces as a finding.
Because the catalog and its per-check severities live in app_config, changes are governed by the same admin authorization and audit logging as all other configuration — making the organization's active quality posture an auditable control rather than a hidden default.
MCP Authentication
The Model Context Protocol server (over SSE) is protected by OAuth 2.0 Authorization Code with PKCE, so external assistants (Claude, Cursor, Copilot, claude.ai) connect under the same identity model as the web UI.
- Standards: RFC 8414 (Authorization Server Metadata), RFC 9728 (Protected Resource Metadata), RFC 7591 (Dynamic Client Registration).
- PKCE (S256) is required on every authorization flow.
- Dynamic client registration lets MCP clients register automatically.
- Bearer-JWT validation on every MCP request uses the same JWT infrastructure as the rest of the platform.