Skip to documentation

Security Audit Operations

Last Updated: 2026-10-09 | For: Production administrators and security reviewers | Level: Advanced

The configured workflow audits confidentiality and account-compromise risks at https://mofacts.optimallearning.org. Its configuration does not establish that a scheduled run completed successfully; review the stored report and its freshness. A red report is evidence for remediation. The audit does not stop services, change the reverse proxy or UFW, block traffic, deploy code, or affect real accounts. Authentication probes may change only their dedicated synthetic fixtures.

The scanners are separate from application deployment, but their sanitized reports are delivered to MoFaCTS through one authenticated ingestion endpoint. Administrators review them at /admin/security-audits. The audit still has no authority to deploy, reconfigure, block, or remediate production.

What runs

  • Daily at 06:00 UTC: external network and internal host exposure controls.
  • Monday at 06:00 UTC: all four sections, adding authentication and repository/image controls.
  • Manual Actions runs: exposure or full.

The external runner resolves every A/AAAA address, scans public TCP and selected UDP ports, validates redirects, certificates, TLS protocols/ciphers, WebSockets, and security headers. An exact UDP open result fails. Silent open|filtered results pass only when the protected host evidence simultaneously confirms zero unexpected UDP listeners, zero Docker UDP publications, active default-deny inbound filtering, and zero unapproved UDP allow rules; unsafe or missing host evidence becomes FAIL or ERROR. TLS review evaluates enumerated cipher entries rather than matching normal compressors: NULL output. The forced host command checks sockets, Docker publications, UFW, the active Apache HTTPS virtual host and upstreams, MongoDB, Redis, the running image digest, and Sidecar bindings, reporting stable sub-probe outcomes and unexpected listener endpoints. Its socket policy permits the tailscaled process on wildcard UDP 41641 and its TCP peer listeners on Tailscale's reserved address ranges. It also recognizes a concrete-interface DHCP client on UDP 68 owned by systemd-networkd. Other unexpected listeners remain findings. Full runs add deterministic Playwright/DDP tests with synthetic accounts and separate runtime/development dependency checks. Development-only npm advisories remain visible as maintenance evidence but fail production posture only when an exact package and advisory ID has a reviewed build/CI exposure in the checked-in policy; runtime dependency and image findings remain independent. Authorization failures use stable semantic probe IDs, and passwordless containment reports each sealed-target boundary with impact-specific severity; anonymous resume-token issuance is expected to pass.

The report's source revision identifies the audit workflow checkout. It does not claim that production was deployed from a clean working tree.

The Sidecar is not part of scheduled execution. It remains available only for manual investigation with synthetic credentials, with ports 8931 and 8932 bound to 127.0.0.1.

Reports

Every run creates a bounded, sanitized canonical JSON report. The runner stores it in MoFaCTS and also encrypts it with RSA-OAEP-SHA256 and AES-256-GCM as a 90-day recovery artifact. Plaintext and raw scanner output are deleted from the runner.

  • A FAIL is a security finding. It does not make production unavailable and does not fail the workflow.
  • An ERROR means the result is not authoritative. Missing tools, malformed output, and unavailable fixtures enter executionErrors and fail the workflow after report storage. A completed but inherently inconclusive probe stays visible as ERROR without being counted as a vulnerability or workflow execution failure.
  • Failure to store the report makes the workflow fail.
  • Workflow logs contain only generic messages and encrypted-artifact metadata.

The admin page receives only report summaries. Full findings are available through five-minute, single-use JSON and escaped HTML downloads. Reports expire automatically after 90 days. Exposure reports show a stale warning after 36 hours; full reports show one after eight days.

Download the artifact from its Actions run. From C:\dev\MoFaCTS\mofacts, decrypt it into a new file with the operator-held private key:

node scripts/security-audit/decrypt-report.mjs C:\path\report.encrypted.json C:\path\report.json C:\secure\mofacts-security-audit-private.pem

The decryptor authenticates the ciphertext and verifies the report's canonical SHA-256. Delete plaintext copies after review.

Protected setup

The protected GitHub environment production-security-audit contains the production Tailscale MagicDNS host, dedicated audit SSH user/key and known-hosts line, limited Tailscale OAuth client ID/secret for tag:ci, synthetic-account fixture JSON, dedicated IMAPS password, and AUDIT_REPORT_INGEST_SECRET. The production application receives the same value as MOFACTS_SECURITY_AUDIT_INGEST_SECRET. AUDIT_REPORT_ENCRYPTION_PUBLIC_KEY is a protected environment variable containing the RSA public key; it is not secret.

The matching private key stays only with authorized operators. It must not be stored in GitHub, MoFaCTS settings, the production host, or source control. Generate a 3072-bit pair with:

openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out mofacts-security-audit-private.pem
openssl pkey -in mofacts-security-audit-private.pem -pubout -out mofacts-security-audit-public.pem
openssl rand -base64 48

On the host, install deploy/security-audit/host-exposure-audit.sh as a root-owned forced command, install host-listener-policy.awk and host-firewall-policy.awk under /usr/local/libexec/mofacts-security-audit/ with root:root ownership and mode 0644, and install security-audit.conf.example as /etc/mofacts/security-audit.conf with root:root ownership and mode 0600. Reinstall the host command and both policy files after scanner updates. Configure tailscale0 as the SSH management interface, the IPv4 and IPv6 tailnet ranges as the management CIDRs, and current service/container identities. The firewall policy requires active UFW, default-deny inbound, SSH only on that private interface from those ranges, and public TCP 80/443 only. Before enabling UFW, retain a public maintenance session and prove both operator SSH and the forced audit key through the production MagicDNS name. The production overlay sets HTTP_FORWARDED_COUNT=1 for the single trusted Apache proxy so Meteor's IP throttles use the originating client rather than Docker's private proxy address. Production disables Meteor inline runtime scripts and enforces the tracked CSP; only reviewed dynamic style attributes retain the narrow style-src-attr 'unsafe-inline' exception, and new external instructional image, media, or frame origins require explicit CSP review. MOFACTS_REDIS_PASSWORD must be the same URL-safe private value in the Compose environment and the root-only audit configuration; the audit verifies authenticated success, unauthenticated denial, and no Redis host publication without printing the value. The SSH key must permit no shell, PTY, forwarding, agent forwarding, X11 forwarding, or arguments.

The production CSP permits WebAssembly compilation for Anki import through the wasm-unsafe-eval source in script-src; the separate unsafe-eval permission remains prohibited. Voice previews use the direct https://docs.cloud.google.com/text-to-speech/docs/audio/<voice>.wav documentation sample for the ten offered voices. Permit https://docs.cloud.google.com in media-src; the preview no longer depends on a redirect through cloud.google.com. Existing additional media allowances require their own review before removal.

On October 9, 2026, staging2 candidate a3155584 passed synthetic upload ownership/transport probes, normal package/media upload, Chrome sample playback and inline failure display, visible theme confirmation, learner reload/resume, Anki analysis, hosted fonts and representative YouTube checkpoint playback. CSP continued to deny ordinary evaluation and inline scripts; login/reload/reconnect did not recreate x_mtok. The upload/voice/theme repair batch is accepted for this scope. This does not close Phase 0, qualify production or establish complete provider/surface coverage. See the repository durable audit status for image identity, verification limits and remaining work.

The canonical policy is C:\dev\mofacts_config\production\apache\000-default-le-ssl.conf. YouTube lessons use Plyr and require https://www.youtube.com/iframe_api plus https://www.youtube.com/s/player/ in script-src, https://i.ytimg.com/vi/ in img-src, and https://noembed.com/embed plus the control-icon file https://cdn.plyr.io/3.8.4/plyr.svg in connect-src. Review the versioned icon URL when upgrading Plyr. Keep playback frames on the existing https://www.youtube-nocookie.com allowance. These are explicit HTTPS path permissions; inline scripts, general JavaScript evaluation, and wildcard sources remain prohibited. After an authorized deployment, check the served CSP and confirm that video lessons initialize, play, present timestamp questions, and enforce their completion gate.

Before valid host JSON exists, transport failures are reported with one bounded category: tailnet-connection-failed, ssh-identity-configuration-failed, ssh-transport-failed, forced-command-rejected, or host-output-invalid. They produce complete internal ERROR controls without retaining raw SSH diagnostics. For tailnet-connection-failed, verify the protected environment contains TS_OAUTH_CLIENT_ID and TS_OAUTH_SECRET for an OAuth client allowed to create ephemeral tag:ci devices.

Production builds carry the exact Git commit in the OCI org.opencontainers.image.revision label. The authoritative production deployment verifies that label before recording the release, while the host audit reports the independently observed running-image SHA-256 identity.

The opt-in build-source capture prototype has a recorded real source capture, Windows-to-Linux round trip and structural image smoke. Full application smoke, source-proof binding and qualification gates remain open. It is not installed in the production command sheet, does not issue release assurance, and ordinary builds do not require its metadata.

The separately authorized collect-tests action verifies the captured source and creates an owned derivative containing reviewed supplemental test inputs. Missing inputs, unsafe paths, links, conflicting build bytes or modes, and acquisition failures stop collection; an existing derivative is never replaced. Success is test-inputs-captured-unqualified. Collection starts no services and runs no tests or image build. The October 9 source checkpoint reports synthetic tests; real collection on the supported Linux lane, complete runner/evidence checks, stronger sealing and final image source proof remain open.

The complete setup and configuration inventory are in the repository's periodic audit deployment guide.

Synthetic fixtures

Normal account sessions have a configured 30-day maximum lifetime and use session storage. Authentication errors deliberately avoid confirming whether an account exists. The audit checks session expiry, revocation and account isolation with synthetic identities; a successful probe does not justify disclosing account details in logs.

Use a dedicated empty tenant containing two learners, two teachers, a synthetic audit administrator, reset, expiry, and lockout identities plus an existing incomplete passwordless-study participant. Provide only minimum synthetic course, history, dashboard, experiment, settings, route, and export state. Add recognizable canaries to synthetic records so cross-user leakage can be detected without retaining raw payloads.

Use a dedicated IMAPS mailbox for reset links. Retain messages older than the configured one-hour lifetime so expiration can be tested. The scanner never treats a merely prior-run message as expired without checking its timestamp. Failed authorization probes appear under deterministic semantic IDs; reset-token and throttle failures use fixed subprobe IDs. Evidence is bounded and never includes fixture arguments, identities, tokens, or response bodies. Brute-force probes run last and may affect only the lockout canary and unique synthetic throttle identifiers.

First run

Run the first manual full audit only after the forced host command, UFW management CIDRs, encryption public key, matching ingestion key, mailbox, and fixtures are installed. Passwordless experiment sessions are expected to receive anonymous resume tokens; the audit tests that they remain contained to the sealed experiment target. Other initial findings may include unauthenticated Redis or missing CSP.

Do not change those systems while interpreting the report. Open separately scoped remediation work, verify it, and let the next scheduled report record the result.