Security Policy
Reporting a vulnerability
If you find a security issue in madtea — credential leak, authentication bypass, code execution via crafted input, etc. — please report it privately rather than opening a public issue.
How to report: open a confidential issue on the project tracker (Codeberg’s “confidential issues” feature). Include a brief description, reproduction steps if you have them, and the madtea version (madtea --version).
Pre-release: madtea’s public Codeberg repo is not live yet. Until it is, report security issues privately by email to
security@sixfold.spaceinstead of via the confidential-issue tracker.
We’ll acknowledge receipt within 72 hours and aim to ship a fix within 30 days, followed by public disclosure.
Supported versions
Only the latest minor release receives security fixes. Older versions may receive critical patches at maintainer discretion.
| Version | Supported |
|---|---|
| Latest minor release | ✅ |
| Older minor releases | best-effort |
Scope
In scope:
- Token or credential exposure (argv, environment,
/proc, error messages, shell history bypasses, log injection) - Authentication bypass against the configured Forgejo/Gitea instance
- Remote code execution via crafted inputs (commit messages, API responses, file contents, branch names, etc.)
- Privilege escalation across users on a shared host beyond what mode
0600and process isolation protect against - Vulnerable dependencies surfaced by
govulncheckand not yet patched
Out of scope:
- Issues in upstream Gitea or Forgejo themselves — please report those to gitea.com or codeberg.org/forgejo directly
- Server-side configuration concerns (e.g. Forgejo/Gitea logging full URLs at
infolevel) - Social engineering against project maintainers
- Theoretical attacks that require
rootor the same UID as the target user
Verifying release artifacts
Every release publishes a checksums.txt (SHA-256 of each archive) alongside checksums.txt.asc, an armored detached GPG signature over checksums.txt. The signing key is:
- Fingerprint:
4CF168E8EF9E40DC9173505F07D6FBD4B0D2A065 - UID:
madtea release signing (sixfold.space) <releases@sixfold.space>
The armored public key is committed at internal/cmd/update/madtea-release-pubkey.asc and embedded in the binary. madtea update verifies the checksums.txt signature against that embedded key, then verifies the per-archive SHA-256, automatically — you don’t need to do anything.
To verify a download by hand:
gpg --import internal/cmd/update/madtea-release-pubkey.asc # or the .asc from the release
# confirm the fingerprint is 4CF168E8EF9E40DC9173505F07D6FBD4B0D2A065
gpg --verify checksums.txt.asc checksums.txt
sha256sum --ignore-missing -c checksums.txt
Caveats — what this does and does not protect:
- Signature verification protects subsequent updates. The first install (
go install, or downloading a release binary) is trust-on-first-use: you’re trusting the TLS connection to Codeberg the first time, the same as anygo installorcurl | sh. The embedded key only starts protecting you once you have a verified binary - and because that key is compiled straight into the binary with//go:embed(frominternal/cmd/update/madtea-release-pubkey.asc) rather than read from a file at run time, nothing on disk can silently swap the trust anchor out from under an update. - Key rotation requires shipping a new release carrying a new embedded key. If the signing key is ever rotated, the running binary still trusts the key it shipped with, so you must update (over TLS) to a release built with the new key before signatures from that new key will verify.
- Verification happens before extraction, and it is fail-closed. madtea first checks the GPG signature on
checksums.txtagainst the embedded key, then checks the downloaded archive’s SHA-256 against that signed manifest (a constant-time comparison), and only extracts and installs the binary once both pass. A bad signature, an unknown signing key, or a single wrong byte in the archive aborts the update before anything is unpacked, so nothing untrusted is ever written to disk. - Updates only move forward. madtea compares the running version against the release tag with semver and refuses to install a tag that is older than or equal to what you already run. A compromised or rolled-back release host that serves an older - but still validly signed - release as the
latestone is treated as already up to date and ignored, never applied. This is what bounds the trust-on-first-use property above: with no per-release revocation, any release the key ever signed will still verify, but the version gate means such a release can only ever carry you forward, never roll you back to an old, known-vulnerable build.
Updating a root-owned install (privilege split)
The common Linux install places the binary in a root-owned location (e.g. /usr/local/bin) while your madtea credentials belong to your own user. Neither naive path works on its own: a plain in-place replace cannot write a root-owned binary, and running the whole update as sudo madtea update would do the network fetch as root, whose context has no madtea credentials — so the request to the release host goes out anonymous and is rejected. madtea therefore keeps the update as a privilege split: the token stays with your user and does all the network work, and root only ever performs the final, offline atomic swap.
A plain madtea update restores the one-command experience by doing that split automatically: it stages and verifies as you, then auto-elevates only the atomic swap by re-invoking sudo madtea update --apply (you may be prompted for your sudo password). When sudo is unavailable it falls back to printing the two manual steps below; on Windows it tells you to re-run as Administrator. Only the staged path and its SHA-256 ever cross the sudo boundary — never a credential. The two steps are also always available to run by hand:
madtea update --stage— unprivileged, run as your own user. Does all network work — fetch the release, download the archive, GPG-verifychecksums.txtagainst the embedded key, then verify the archive’s SHA-256 (the TOFU chain above) — using only your own resolved credentials. It never falls back to an anonymous request when credentials exist, and the token is never printed or logged. It stages the verified binary to a temp file, records that binary’s SHA-256, and prints the staged path, the SHA-256, and the exact finishing command.sudo madtea update --apply <staged-path> --sha256 <hex>— elevated. Does no network work and uses no credentials. It re-computes the staged file’s SHA-256, fails closed if it does not match the--sha256value recorded in step 1, and only then atomically replaces the binary.
Because verification happens under your own user in step 1 — whether you run it or the auto-elevation does it for you — and is re-checked in step 2, root never fetches, never authenticates, and never sees a token; a tampered staged file is caught by the SHA-256 mismatch before any replacement. An ordinary madtea update on a writable install still verifies and replaces in one step with no elevation at all — and, once it confirms an update is actually available, it preflights that the target is writable before downloading the release archive, auto-elevating only the swap (or, when even that is impossible, pointing you at --stage) rather than failing at the end. A root-owned install that is already up to date, or a madtea update --check, needs no write and succeeds regardless of who owns the binary.
One hardening caveat: do not give madtea a NOPASSWD sudoers carve-out. The split already prompts for your sudo password interactively whenever it needs root for the swap, so a passwordless rule is never required. It is also actively dangerous: because the staged path and SHA-256 differ on every run, the only rule shape that can make the auto-elevation passwordless is a broad one (the bare binary path, or a madtea update * wildcard) - a rule pinned to the literal arguments madtea update matches neither the auto-elevation nor the finishing step - and any rule that broad also covers sudo madtea update --apply <path> --sha256 <hex> with no prompt. Under such a rule an attacker who can run madtea gains two root primitives from it. Passing a deliberately wrong --sha256 makes the fail-closed mismatch error (archive checksum mismatch: got <hash>, want <hash>) print the SHA-256 of any root-readable file named as <path> - an arbitrary-file-read oracle. Passing a --sha256 that matches an attacker-controlled file makes --apply overwrite the installed binary as root. Keep the interactive prompt: it is the boundary that makes both of those cost a password.
Agent / MCP threat model
madtea is a CLI tool that also exposes its commands over MCP for use with LLM agents. It runs the operations you ask for: the madt_orchestrate MCP tool executes the verify command you pass, the attachment-upload tools upload the local file path you pass, the api_call tool hits the endpoint you pass, and so on.
madtea is not an agent firewall. If you drive it via an autonomous LLM agent and you are worried about indirect prompt injection — a hostile issue body, README, or webpage that tricks the agent into uploading ~/.ssh/id_ed25519, running rm -rf ~, or pushing to the wrong branch — that defense belongs at the agent or harness layer: sandbox the agent, restrict its tool access, require human confirmation for sensitive operations, or run the whole thing in a container. grep, curl, and git don’t have prompt-injection firewalls; madtea won’t grow one either.
The credential-safety and code-execution guarantees in the “In scope” list above are the things madtea does defend against, and we hold ourselves to them. Sandboxing an autonomous agent that drives the tool is not in that list.
Within that narrow scope, madtea does provide two concrete agent-facing defenses. First, it sanitizes terminal control characters (ANSI/OSC escape sequences and C0/C1 control bytes) out of forge-sourced text — issue/PR titles and bodies, branch/tag/label/user/org names — on both the success and error output paths before that text reaches an agent’s terminal, so a malicious issue or PR title or body cannot drive the terminal. Second, on a detectable wrong-repo MCP call it fails closed: when the caller’s intended repo resolves and differs from the one the server resolved from its own working directory, madtea refuses the call rather than silently writing to the workspace-root repo.
Implementation details
For the mechanics behind madtea’s credential-safety guarantees (storage backends, GIT_ASKPASS flow, the four no-token-on-argv / no-token-in-errors guarantees), see docs/safety/credential-safety.md.
Canonical source: SECURITY.md in the madtea repo.