Skip to content

Security

A town that can deploy to production is a control plane, so Myrtille is built to be safe first. This page says, in plain words, what it guarantees and what it does not.

  • At installation, actions are off in every district. The town shows everything and changes nothing.
  • An admin of a district turns its actions on or off in the panel of that district. The change is recorded in the audit log. It is never a line of the town plan.
  • A webhook never triggers an action. Webhooks are hints: the town hall checks what they say through the API of the tool before it believes them.
  • Myrtille never acts on its own. Every action comes from a person, or from an API key its owner created with the act scope.

Each person has a role per district: viewer, operator or admin. Someone can be admin of staging and viewer of production. The owner of the town, the first account, also manages the couriers, the tokens, the members and the invitations.

  • A viewer sees the town, the mail and the events.
  • An operator also opens the logs, decides the mail, runs safe actions (redeploy, restart) and gives missions.
  • An admin also runs destructive actions (stop a container, set an environment variable) and turns the actions of the district on or off.

Every route of the API that needs more than a viewer is tested against a viewer and a read-only API key.

A town is private: the town hall answers nobody without an account. Two exceptions exist, both chosen: a public link an owner makes, which shows the town read-only, anonymised and late, and the visitors of the demo town of the mock mode, who only look (Public view and visitors).

Before an action runs, the town hall checks, in this order: the citizen and its courier, that the courier declares the action, that the actions of the district are on, the role of the person on that district, and the confirmation.

  • A destructive action and a mission always ask for a confirmation.
  • In production, a destructive action also asks you to type the name of the citizen.
  • A safe action asks for one when its district says confirmation: always, the default for production.
  • A confirmation is single use and bound to that request.

Every step of an action is written to an append-only audit log, before and after it runs: who, with which role, what, on which citizen and district, with which token (a digest, never its value), and the result. Each entry is sealed with the one before it, in a chain keyed by the instance secret. The town hall checks the chain every day, and a break raises a critical item in the mail. An owner reads the log at GET /api/v1/audit and checks it at GET /api/v1/audit/verify.

  • No environment value, password, private key or compose content is ever displayed, not even to an admin or the owner. Those fields are write-only: you can set a value, not read it.
  • Tokens are stored encrypted (AES-256-GCM, with a key derived from the instance secret MYRTILLE_SECRET). They are never sent to the browser, never in an event, never in a log line, never in an error, and never in the context of a coding agent.
  • Every answer of an external API is filtered by a list of allowed fields before it goes further.
  • Logs shown in the town have their secrets masked first, and are never stored.
  • Myrtille uses three separate Coolify tokens (read, deploy, write), picks the least powerful one an action needs, and refuses a root token (Coolify).

Myrtille integrates through configuration only. It ships no SDK to import and writes no file into your repositories. The hooks of Claude Code go in the settings of the user, ~/.claude/settings.json, after you saw the change and said yes, never in a project (Claude Code). Removing Myrtille is removing its configuration.

  • It is never the only barrier for coding agents. When the town hall cannot be reached, the hooks of Claude Code fall open: Claude Code applies its own behaviour, its prompt on the workstation. Keep the permission rules and the sandbox of Claude Code on.
  • A mission runs with the settings of its repository. What .claude/settings.json of a repository says still applies to a headless session: allow missions only where you trust those settings.
  • The masking of secrets in summaries and logs is a safety net, not a guarantee: a secret in a shape it does not know can get through. Command lines, queries and search patterns of Claude Code never leave the workstation.
  • Sign-in with an e-mail and a password (argon2id), with an optional second step (above), or through an identity provider. Sessions live in an HttpOnly cookie, Secure over https, with a new token at every sign-in. The first sign-up needs the setup token; then sign-up closes.
  • API keys start with myr_, are stored hashed, have a scope (read, ingest, act), and are shown once.
  • Strict headers on every answer: a Content Security Policy without inline script, HSTS over https, X-Frame-Options: DENY. Rate limits on sign-in and on webhooks.
  • The container runs without root, with a read-only file system except its data volume, no capability and no-new-privileges.
  • Agents always connect out to the town hall, with a single-use enrolment token, then a long-term token the town hall renews. The town hall never dials an agent.
  • With the version check (the same consent as telemetry), a version with a security fix raises a high item in the mail.

Each member can add a second step to their sign-in: a code of six digits from an authenticator app on their phone (any app that reads an otpauth:// link: Google Authenticator, Microsoft Authenticator, 1Password, Aegis, and others).

  • Set it up from the Security page of the town, with your password. The town hall shows a link to open on the phone and the key to type by hand, once. You confirm with your password and the first code the app shows; the town hall then gives you ten recovery codes, also shown once. Keep them somewhere safe: each one signs you in once if you lose your phone. Setting it up ends your other sessions.
  • An open session is never enough: setting up, renewing the recovery codes, turning it off and linking an identity provider all ask for your password, and for a code once you have a factor. Someone who steals a session cookie cannot change your sign-in.
  • Signing in: after the password, the town asks for the code. A recovery code works there too. A code is accepted for its 30 seconds and one step on either side (a phone a little early or late), and never twice; the step of a sign-in serves once.
  • Too many wrong codes: five per account, then one every five minutes, whatever the address they come from. After ten wrong codes in a row, only a recovery code signs in, until one is used; the mail of the town says so to the owner. The count lives in the database, so restarts and several town halls change nothing. Every wrong code, every setup and every removal is written to the audit log (never the code itself).
  • The key is stored encrypted (AES-256-GCM, with its own key derived from the instance secret), bound to its account, and never sent back after the setup. Recovery codes are stored hashed.
  • Turning it off asks for your password and a code or a recovery code, and ends your other sessions. A member who lost both their phone and their codes asks the owner, who removes their second factor from the members of the town (DELETE /api/v1/members/<id>/totp); their sessions end, they sign in with their password again and set it up anew.
  • The policy of the town: the owner can require two-step sign-in of the owner and of every admin, at every sign-in, with a password or through an identity provider. Whoever has not set it up yet does so before the town opens, and cannot turn it off while the policy stands. Turning the policy on ends the sessions of those it catches without a factor.
  • The CLI asks for the code when the account has one (myrtille init, doctor, backup, export, import).
  • Sign-in through an identity provider (Google, GitHub, the provider of your company) is not asked for a code unless the policy of the town covers you: otherwise the provider owns its own factors. Turn on two-step verification there. API keys and agents are not people: they never sign in.

The owner locked out of a self-hosted town

Section titled “The owner locked out of a self-hosted town”

The owner who lost both their phone and their recovery codes has no one above them in the town. On the host of the town hall, stop it, then run, with its data directory and its instance secret (MYRTILLE_SECRET, or myrtille.secret in that directory):

Terminal window
myrtille totp remove --email owner@example.com --data-dir /path/to/data

The command refuses while the town hall runs, never works over the network, ends the sessions of that account, and writes the removal to the audit log of every town of the account. Start the town hall again and set up a new factor from the Security page.

Please do not open a public issue. Report privately, through the private vulnerability reporting of GitHub on the repository, or by e-mail to security@myrtille.eu. Every report is acknowledged within 72 hours, and fixed through a coordinated advisory. The details are in SECURITY.md at the root of the repository.