Beegait

Security controls

For the person who audits. Every control, its owner, its status — the same page the operator keeps honest at every release.

This is the list an auditor reads first. One row per control: what it is, its status (in place with its date and the card that brought it, or planned with the card that will), its owner as a role, and its evidence — a path in the platform’s repository or a page on the operator console. The source of this page is docs/security-controls.md in the platform’s repository; a release that changes a control updates it in the same release.

The roles: the platform operator (Beegait on the hosted platform; on your own server, your operator — the same controls apply, with the bundle’s files), the account owner, the workspace manager, the runner’s owner (the person or team whose machine runs the helper program) and the vendor (Beegait, for the offline signing keys). The plan behind the planned rows is the security workstream of the product’s own project record; each row names its card.

Host

Control What it is Status Owner Evidence
TLS front Caddy is the only public-facing service: automatic HTTPS (Let’s Encrypt, auto-renew) for the domain, everything else proxied to the app on the compose network. in place — 2026-08-06, card MV2-LAUNCH-VPS (34) platform operator deploy/Caddyfile (self-hosted: deploy/selfhosted/Caddyfile)
The forwarded client address The app believes X-Forwarded-For only from Caddy’s fixed address on the edge network; any other peer is seen at its own address. The app port is loopback only, checked by the deploy preflight. in place — 2026-10-22, card MV2-SEC-FORWARDED-IP-TRUST (ships in the Phase S release) platform operator compose.prod.yml, cloud/access.py (client_ip), tools/compose_validate.py (app_port_problem, edge_problem, binding_problem)
Outbound connections Every ssh the cloud’s git starts goes through one egress rule: no private, local or metadata address, resolved once (DNS-rebinding proof); git follows no https redirect; postgres and minio sit on an internal network with no route out. in place — 2026-10-22, card MV2-SEC-APP-EGRESS (ships in the Phase S release) platform operator cloud/egress.py, Dockerfile (ssh_config.d/beegait-egress.conf), compose.prod.yml (networks.data)
Browser security headers Every page of the app carries a Content Security Policy (same-origin sources, no plugins, no form to another site, no framing by another site; the operator console is never framed), nosniff and a same-origin referrer policy; the TLS front adds HSTS. The two boxed paths of the hosted docs site and the public links keep their own stricter policy. planned — built 2026-10-21 (card MV2-SEC-CLOUD-NO-INLINE-DATA) platform operator cloud/secheaders.py, deploy/Caddyfile
No exposed database or object-store port Postgres and MinIO have no host port mapping at all; in production the app’s own port binds loopback (COMPOSE_APP_PORT=127.0.0.1:8100) — only Caddy reaches it. in place — 2026-07-25, the ransom-bot lesson (card 34) platform operator docker-compose.yml (the NEVER lines under postgres and minio), compose.prod.yml
PID 1 init tini reaps git’s detached grandchildren; without it zombies filled the container’s task limit in ~7 h (the 2026-09-07 outage). in place — 2026-09-07 platform operator docker-compose.yml (init: true)
Firewall ufw: SSH + 80/443 and nothing else, default deny incoming. in place — 2026-08-06, card MV2-LAUNCH-VPS (34) platform operator tools/launch_vps.md §1
sshd keys only PermitRootLogin no, PasswordAuthentication no; the deploy user holds the provisioning key. in place — 2026-08-06, card MV2-LAUNCH-VPS (34) platform operator tools/launch_vps.md §1
Unattended security patches unattended-upgrades applies the OS security updates by itself. in place — 2026-08-06, card MV2-LAUNCH-VPS (34) platform operator tools/launch_vps.md §1
Disk doors Every publish carries the disk-usage gate lines; a full disk is refused before the deploy, not discovered after. in place — 2026-09-13, card MV2-PHX-DISK-GATE (291) platform operator tools/launch_vps.md §7b, tools/phx_publish.sh
The deploy preflight compose_validate.py --prod refuses a deploy whose served artifacts lack their hash or signature, and — since card 364 — a malformed admin IP allowlist, the app port binding and the edge network. in place — 2026-09-03 (cards 172 · 177), 2026-09-27 (338), 2026-09-28 (364), 2026-10-22 (MV2-SEC-FORWARDED-IP-TRUST) platform operator tools/compose_validate.py, tools/phx_publish.sh

Access

Control What it is Status Owner Evidence
A separate admin realm admin_users / admin_sessions share nothing with the account tables: an account session can never reach /admin, an admin session never impersonates a user. in place — 2026-07-27, card 28 platform operator cloud/admin.py (the module docstring), migrations/007_admin.sql
MFA on every admin, mandatory Every console sign-in is an argon2id password AND a valid TOTP code (RFC 6238, stdlib) — no admin account exists without the second factor. in place — 2026-07-27, card 28 platform operator cloud/admin.py, cloud/mfa.py
The operator grant (least privilege) An admin account is a GRANT on a Beegait account: no password of its own, the person’s own password and authenticator open the console, disabled_at is the grant’s kill switch; no open signup, the CLI bootstrap is the rescue door only. in place — card 299, 2026-09-14 platform operator cloud/admin.py (the card 299 block), /admin/admins
12-hour admin sessions The operator realm’s sessions expire after 12 hours (account sessions: 14 days); sessions are listed with IP and last sign-in and revocable one by one. in place — 2026-07-27 (card 28), 2026-08-31 (MV2-ADMIN-CONTROL, 118) platform operator cloud/admin.py (ADMIN_SESSION_HOURS), /admin/admins
The admin IP allowlist With MC_ADMIN_IP_ALLOWLIST set, /admin/* answers 404 to every client outside the list (invisible, in front of the MFA login). A malformed entry fails closed: the console answers 404 to everyone, one log line names the entry, and the deploy preflight refuses the value before the box sees it. in place — 2026-08-31, card MV2-ADMIN-CONTROL; fail-closed 2026-09-28, card 364 (it failed open before) platform operator cloud/access.py (_admin_ip_allowed), tools/compose_validate.py (allowlist_problem), .env.compose.example
A second factor for every user TOTP offered to every account, “remember this PC” for 5 days, ten one-time recovery codes at enrolment, a forced re-enrolment door. in place — 2026-09-09 (cards 238-239), 2026-09-14 (310-313) account owner (each person enrols) cloud/mfa.py, migrations/042_user_mfa.sql
Sign-in brute-force window + captcha Failed sign-ins are remembered 15 minutes per IP and per email; the picture captcha is asked after a failure, and platform-wide while the spike rule trips (login_spike_failures on the platform row, the console shows a banner). in place — 2026-09-09 (card 239), 2026-09-27 (MV2-CAPTCHA-WHEN, MV2-SIGNUP-SAFETY) platform operator migrations/043_login_failures.sql, cloud/captcha.py, cloud/admin.py (spike_banner)
argon2id passwords + the password policy Passwords hashed with argon2id; tokens stored only as sha256; ONE ordered password rule list every set-password door enforces. in place — 2026-07-21 (card 07, MV2-CLOUD-TENANCY-AUTH), 2026-09-27 (MV2-SIGNUP-RULES) platform operator cloud/security.py
Secure session cookies The session cookie carries the Secure flag behind TLS (MC_COOKIE_SECURE=true in production). in place — 2026-08-06, card MV2-LAUNCH-VPS (34) platform operator cloud/config.py, compose.prod.yml
The private-preview curtain MC_ACCESS_CODE gates every page behind one shared code during a test phase; rotating the value re-prompts every device. in place — 2026-08-06, card 35 (MV2-ACCESS-CURTAIN) platform operator cloud/access.py (access_gate)
Workspace roles Owner · manager · member · viewer — the doors that change money or data (upgrade, audit export, public links, the agent rules) are owner/manager only. in place — 2026-08-31 (card 116), 2026-09-14 (302), 2026-09-25 (MV2-AUDIT-EXPORT) account owner cloud/auth.py, cloud/upgrade.py, cloud/share.py
No name runs as code A name a person, a card or a machine chooses never lands inside an inline handler of a page: buttons carry data in data-attributes and every page escaper escapes quotes; a check refuses the pattern. planned — built 2026-10-21 (card MV2-SEC-CLOUD-NO-INLINE-DATA) platform operator tests/noinlinedata_test.py
Cross-site writes refused A change sent to the app from a page of another website, or from a sibling address of the same site, is refused before it runs: every request that is not a read must come from a page the app served itself (the browser’s Sec-Fetch-Site, else Origin against the app’s own address); scripts and machines that send neither header keep working. planned — built 2026-10-22 (card MV2-SEC-CSRF-ORIGIN-CHECK) platform operator cloud/csrf.py, tests/csrf_test.py

Secrets

Control What it is Status Owner Evidence
The vault and its master key Per-row AES-256-GCM sealing (a per-workspace DEK wrapped by ONE deployment master key from the environment) for the repo deploy keys and the git-host OAuth secrets; a secret that reaches a log line anyway is masked by the row below. in place — 2026-07-25, card MV2-KEY-VAULT (25) platform operator cloud/vault.py
A secret never reaches a log line Every log handler of the app and of the site-builder process masks the known secret shapes — provider and git-host tokens, runner tokens, bearer values, secret headers, credentials inside a URL, the token · code · state · ticket query values, share links, private keys — in the message, in its arguments and in the traceback; a print() cannot write one either (both processes’ stdout and stderr go through the same table). A password-reset click now reads token=***redacted*** in the access log. planned — built 2026-10-22 (card MV2-SEC-LOG-REDACTION, audit SEC-8) platform operator cloud/logsafe.py, tests/logsafe_test.py
No AI provider key stored for an account The provider-key lanes were retired: agents run on the credentials the runner machines hold; the platform holds no provider key of its own, and the free trial runs on the person’s own runner too (the trial’s platform lane retired by card MV2-TRIAL-RUNNER-FIRST, 2026-09-24). in place — card 283, 2026-09-12 platform operator cloud/vault.py (the docstring), the site’s /security/
The master key in a KMS or HSM The deployment master key leaves the environment file for a managed KMS; every DEK re-wrapped; every unseal audited. planned — cards 346-347 (S1) platform operator Story MV2-SEC-MASTER-KEY-KMS
No code-reading credential on the platform The GitHub App drops to Metadata: Read (348-349, S2); deploy keys move to the runner (350-351, S3); broad personal tokens refused at connect (352, S4); runner-only repositories as a documented option (353, S5); Bitbucket listed from the runner (361-362, S13). planned — cards 348-353, 361-362; 362 built 2026-09-30 (migration 081: no Bitbucket credential on the platform), in place with the release that carries it platform operator Stories S2-S5, S13
Signed releases Every engine wheel and runner package is signed OFFLINE by the vendor’s release key; the self-update offer carries the signature beside the sha256; the publish refuses an unsigned artifact. in place, the cloud half — card 338, 2026-09-27; the runner’s verifier (339) and the OS package signing (340) planned the vendor cloud/relsig.py, tools/release_sign.py, tools/compose_validate.py
Signed licences A self-hosted licence is an Ed25519-signed file; the image verifies it with the public key it carries. in place — 2026-08-31 (card 124, MV2-SH-LICENSE), 2026-09-27 (MV2-SH-KEY-BAKED) the vendor cloud/license.py, deploy/license_pubkey.txt, tools/license_issue.py
Secrets never in the repository .env.compose is gitignored; ${VAR:?} fails loud when one is missing; the offline keys (licence, release, backup) live outside every checkout. in place — 2026-08-06 (card 34), 2026-09-27 (338) platform operator docker-compose.yml, .env.compose.example

Audit

Control What it is Status Owner Evidence
admin_audit Every mutating console action (who / when / old → new), keyed by the account it touched; readable at /admin/audit with filters. in place — 2026-07-27 (card 28); the viewer 2026-08-31 (MV2-ADMIN-CONTROL, 118) platform operator cloud/admin.py (the audit viewer), /admin/audit
org_audit The workspace’s own changes (run limits, public links, upgrade requests, the grace and dormant flips the platform writes itself); the account reads it through the audit export (CSV / JSON, a Premium feature) together with the console’s rows about the account. in place — 2026-09-25, card MV2-AUDIT-EXPORT account owner / workspace manager cloud/auditexport.py, GET /api/org/audit.csv
git_host_audit · repo_audit · hub_mirror_audit · hub_lifecycle_audit Written on every git-host connect or refusal, repository change, mirror push and hub lifecycle step. Writers only — no viewer yet. in place — the writers since 2026-07-22 (card 11) through 2026-09-13 (card 257); a viewer planned — a follow-up card under Story MV2-SEC-MIRROR-KEY-HOST platform operator cloud/githost.py, cloud/repos.py, cloud/mirror.py, cloud/hubs.py
vault_audit Every unseal of a sealed secret written down. planned — card 346 (S1) platform operator Story MV2-SEC-MASTER-KEY-KMS
The runner’s local audit log What every job sent back, on the machine that ran it. planned — card 354 (S9) runner’s owner Story MV2-SEC-CAP-AND-LOG

Data

Control What it is Status Owner Evidence
Nightly backups The files (hub bare repositories + blobs) and pg_dump -Fc of the database, 14 dated folders, the restore drill run once then quarterly. They land PLAIN on the host until card 365 is live there (the next row). in place — 2026-08-06 (MV2-LAUNCH-VPS), 2026-09-13 (card 258); encryption planned — built 2026-09-29 (card 365) platform operator tools/backup_storage.sh, tools/launch_vps.md §8
Encrypted backups Every backup artifact encrypted on its way to disk (age) to a recipient key whose private half stays offline with the other vendor keys (a hosted backup without a recipient is refused); the restore script decrypts and deletes the copied key; a self-hosted box’s pre-update dump takes the same door as an option. planned — built 2026-09-29 (card MV2-SEC-BACKUP-ENCRYPT, 365); in place once phx holds the recipient and the restore drill (TC5) has run platform operator tools/backup_storage.sh, tools/restore_storage.sh, cloud/updater.py (_write_dump), tests/backup_test.py, tools/launch_vps.md §8
Retention limits on trails and chat Job trails and chat purged after the plan’s window. planned — card 358 (S11) platform operator Story MV2-SEC-RETENTION
A chat is its asker’s A chat job (its question, reasoning trail and answer) is served to the person who asked it and nobody else — a manager included: another person’s chat is left out by the SQL of every person reader (queue.CHAT_PRIVATE_SQL, passed as viewer_id), its id answers 404, its thread never resumes for someone else; the hub lock still counts it (“another person’s chat”, no words) and a manager may still stop it. A new person-facing read of jobs passes the viewer (_job_rows(viewer_id=…) / queue.chat_visible) — viewer_id=None is for machine readers only. in place — 2026-10-06, card MV2-CHAT-PRIVATE platform operator cloud/queue.py (chat_visible), tests/chatprivate_test.py
Per-workspace encryption of hub data The hub files and the database text sealed per client. planned — cards 356-357 (S10) platform operator Story MV2-SEC-PER-CLIENT-ENCRYPTION
The record is git Every hub is a git repository: its history is the audit of the record, and a one-way mirror to the account’s own GitHub or Bitbucket keeps a full copy in their hands. in place — 2026-07-22 (card 11), 2026-08-25 (MV2-HUB-HOSTING, 76) workspace manager cloud/hubs.py, cloud/mirror.py
The hub mirror write key The cloud’s own write key into the client’s repository (the mirror): a “Rotate key” door and a 90-day schedule register a new key at the host, push once with it and de-register the old one by its id (a pasted key rotates in two steps, the person confirming); every push that opens the key writes a key_used row. planned — built 2026-09-29 (card MV2-SEC-MIRROR-KEY-ROTATE, 363); in place with the release that carries migration 079 platform operator cloud/mirror.py (rotate, rotate_due), tests/mirrorkey_test.py
Trash before delete A deleted hub sits in the Trash and is purged after its TTL by the site builder’s hourly tick — never gone on one click. in place — card 257, 2026-09-13 workspace manager cloud/hubs.py (purge_tick), cloud/sitebuilder.py
Dormant trials An ended trial nobody signed into is warned by mail, then removed by the sweep. in place — 2026-09-25, card MV2-TRIAL-DORMANT-CLEANUP platform operator cloud/dormant.py
Data-protection statement What is stored, where, for how long — the public statement. in place — 2026-09-05, the site’s page platform operator the site’s /data-protection/

Code protection

Control What it is Status Owner Evidence
The image carries the control plane compiled The cloud package is compiled per module in a build stage (Nuitka); the image — ONE for the hosted platform and every self-hosted box — holds extension modules, the package’s __init__.py and the launch stubs of its python -m doors: no module source and no bytecode cache, in no layer. The engine inside the image is still the pure wheel until its Linux wheel is compiled (card MV2-SEC-ENGINE-WHEEL-COMPILED-MAC-LINUX). planned — built 2026-10-02 (card MV2-SEC-SELFHOSTED-IMAGE-COMPILED); in place with the release that ships the compiled image the vendor Dockerfile, /healthz build, tools/compose_validate.py (image_problem)
The Windows runner ships compiled The engine wheel inside the MSI is compiled per module (every module a native one; what stays source: the package’s __init__.py files, __main__.py and two launch stubs); the update lane is unchanged. Self-update serves each runner the signed wheel of its own platform: the runner announces the Python it runs on, the cloud picks the compiled wheel of that tag from a per-platform table and falls back to the pure wheel; every served wheel carries its own sha256 and release signature. planned — built 2026-10-02 (card MV2-SEC-ENGINE-WHEEL-COMPILED-WIN, card MV2-SEC-PLATFORM-WHEEL-OFFER); in place with the release whose MSI carries the compiled engine and whose offer serves the compiled wheel the vendor tools/compile_build.py --wheel (engine), tools/build_runner_msi.ps1, cloud/runners.py (engine_wheel_table), tools/launch_vps.md §6
The macOS and Linux runners ship compiled One compiled wheel per platform, offered to the runner that matches: the pkg’s engine is compiled with its bundled CPython for arm64 and Intel (every native module codesigned inside the signed, notarized app), the deb carries a manylinux wheel per CPython minor (3.10 · 3.11 · 3.12) and installs the one the box’s Python fits; the pure wheel stays the fallback for a Python outside that range. planned — built 2026-10-03 (card MV2-SEC-ENGINE-WHEEL-COMPILED-MAC-LINUX; the workflow’s compiled lane); in place with the release that ships the compiled pkg and deb the vendor Story MV2-SEC-COMPILED-DISTRIBUTION · .github/workflows/runner-packages.yml
Constants and data files hidden in the binaries The commercial edition of the compiler: constants encrypted, data files embedded, tracebacks stripped. planned — card MV2-SEC-NUITKA-COMMERCIAL-KEY (S15) the vendor Story MV2-SEC-COMPILED-DISTRIBUTION
The shipped wheel stripped, the pages minified No docstrings, tests or docs in the wheel; the board’s and the runner window’s JavaScript and CSS minified. planned — card MV2-SEC-WHEEL-STRIP-MINIFY (S15) the vendor Story MV2-SEC-COMPILED-DISTRIBUTION

Runner

Control What it is Status Owner Evidence
Outbound only The runner long-polls the platform; it opens no inbound port and accepts no incoming connection. in place — 2026-07-22, card MV2-RUNNER-CONNECT (13) runner’s owner maistro/runner.py (the engine), the site’s /security/
Typed jobs A runner runs only the job types the engine declares; the claim hands it typed parameters, never a shell line. in place — 2026-07-22, card MV2-JOB-QUEUE (12) platform operator cloud/jobtypes.py, cloud/queue.py
Pairing with consent, revoke in one click A runner is paired once with a short code, serves what its owner consented to (the account, its workspaces) and the console revokes it in one click. in place — 2026-07-22 (MV2-RUNNER-CONNECT), 2026-08-11 (MV2-RUNNER-CONSENT, 49), 2026-07-27 (card 28) runner’s owner / platform operator cloud/runners.py, /admin/runners
The version floor MC_MIN_RUNNER_VERSION refuses an outdated runner with the one-liner that updates it. in place — 2026-08-22, card MV2-RUNNER-VERSION-FLOOR (63) platform operator cloud/config.py, compose.prod.yml
The self-update verified The runner verifies the offer’s signature against the release key it pins by value; an unsigned or badly signed offer is refused with one sentence. planned — card 339 (S6) the vendor Story MV2-SEC-SIGNED-RELEASES
A real person behind every job + the runner allowlist Every job carries the requesting person’s proof; a workspace lists the runners allowed to serve it. planned — cards 341-343 (S8) workspace manager Story MV2-SEC-JOB-PROOF-ALLOWLIST
Client-controlled updates The machine’s own update policy decides when a runner updates; the console only shows it. planned — cards 344-345 (S7) runner’s owner Story MV2-SEC-UPDATE-POLICY
Cap and log what comes back An outbound cap on what a job may send, and the local audit log of what it sent. planned — cards 354-355 (S9) runner’s owner Story MV2-SEC-CAP-AND-LOG
No code quotes A workspace setting that forbids code quotes in what its agents send, enforced on the runner. planned — cards 359-360 (S12) workspace manager Story MV2-SEC-NO-CODE-QUOTES

Try it on your own project.

Download the app, sign in, and see your first project on the board in minutes. Prefer a guided look? A 20-minute walk-through with the person who built it.