Skip to content

Under developmentMica is experimental and not ready for production. The API and the content format can still change, so read the release notes before you upgrade.Follow the releases.

Docs/Security

Operate

Security

What Mica protects, who it trusts, the risks it accepts, and how to harden a self-hosted deployment. How to report a vulnerability.

8 min read

This page states what Mica protects, who it trusts, and which risks it accepts. It also tells you how to harden a self-hosted deployment.

Mica is a hobby project of one maintainer and is not independently audited. Read this page before you put a self-hosted Mica on the internet.

Report a vulnerability

Do not open a public issue. Use GitHub private vulnerability reporting: open the repository's Security tab and select Report a vulnerability. The full policy is in SECURITY.md.

What Mica protects

  • The repository token. It can write to the content repository. The self-hosted Mica keeps it in its environment and never sends it to the browser. Mica gives it only to the Git host, and to the Git LFS host when you set MICA_REPOSITORY_LFS_URL. It never writes the token into the clone.
  • The session secret. MICA_SESSION_SECRET encrypts the session cookie (AES-256-GCM). A person who knows it can create a session for any editor.
  • The drafts. Saved, unpublished changes are files in the Git clone on the data volume. They are not in the repository until somebody publishes them.
  • The content repository. Only an admitted editor can publish, and only to the configured branch.

Who Mica trusts

  • The identity provider. It decides who a person is. Mica decides who may edit, from the rules in MICA_ADMIT_*. A provider that issues a false identity or false claims gets that person in.
  • An admitted editor. Every admitted editor can do everything, Publish included. There are no roles.
  • A contributor to the content repository. Anything in the repository reaches the editor: content, media and the content model. Give write access to the repository only to people you would also admit as editors.
  • The reverse proxy. It terminates TLS. With MICA_TRUST_PROXY=true, Mica believes the client address in X-Forwarded-For.
  • Processes on a developer's machine. The local editor has no sign-in. It listens only on loopback and answers only to localhost, 127.0.0.1 and [::1].

Risks Mica accepts

  • Removal at the provider applies at the next sign-in. Mica checks the provider's claims when a person signs in. A person you remove from a group at the provider keeps working until the session ends: at most 8 hours, or 4 hours without activity, by default. To stop a person sooner, change MICA_ADMIT_SUBJECTS or the other rules, and restart Mica: it checks the rules on every request.
  • No server-side session revocation. Sessions are encrypted cookies. Mica has no database. To end every session at once, change MICA_SESSION_SECRET and restart. Every editor must then sign in again.
  • The local editor is open to local processes. Any process on the developer's machine can call the local API. Such a process can already change the files in the working tree.
  • An agent's access cannot be ended one by one. On a self-hosted Mica with MICA_MCP=true, an agent's token is sealed like a session, and Mica stores no list of them. It ends after MICA_SESSION_TTL_HOURS, or at the next request after the rules change and Mica restarts. A copied refresh token works until then too. To end all of them, change MICA_SESSION_SECRET.
  • Content can give instructions to an agent. A text in the content can try to direct an agent that reads it. By default, an agent only saves drafts; MICA_MCP_PUBLISH lets it publish.
  • A publish is immediate. There is no review step. If you need one, point Mica at a staging branch and merge on your own schedule.
  • No roles. Every admitted editor can save, publish, discard and delete.
  • Rate limits are in the process. Each Mica counts on its own, and a restart resets the counts.
  • A contributor can already run code. A person who can change the website's package.json runs code in its build and on developers' computers. Mica adds no new way to do that.
  • The website's templates decide whether an editor can run script. Rich text cannot add script: Markdoc escapes HTML. A template can still do it. Do not put a script address in a field, for example for analytics: every editor could then load their own script on every page. Keep such addresses in code. When a template writes content into a <script> element, for example JSON-LD, replace each < with its JSON Unicode escape, so that a text cannot end the element.
  • A map loads tiles from another host. A coordinates field with a map makes the editor's browser load tiles from OpenFreeMap, or from the hosts of a style that the model names. The model comes from the repository, so only a contributor can change those hosts.

What the repository token can reach

Mica writes only below the content root. The token can do more: it can push to the whole repository, the website's code and its CI files included, such as .github/workflows/ or .gitlab-ci.yml. A CI file runs with the secrets of your CI. So a person who steals the token can change what your build does. The content root is not a limit for the token. When the content is in its own repository, the token reaches that repository only. This is a reason to use one: the website's code and CI files are in another repository.

  • Keep the token only in the container's environment, give it an expiry, and replace it when a person who saw it leaves.
  • Let the production deployment run only from a protected branch, with its own secrets, for example with a GitHub environment or a GitLab protected environment.
  • For the strongest separation, point Mica at a staging branch, and let a person merge it into the production branch.

Website visitors

Mica is not on a visitor's path: your website is the output of your build. Editors decide what visitors see, so a stolen editor session can change the website until you notice. Every publication is a Git commit: revert it to undo it. Whether an editor can run script on your website depends on your templates (see the accepted risks above).

Availability

When Mica is down, your website stays up: the website does not call Mica. Mica limits sign-in attempts and the size of requests: an upload to MICA_MAX_UPLOAD_BYTES, a JSON request to 2 MB, a request to /oauth/* to 64 kB, and a request to /mcp to the upload limit with its base64 encoding. The content model comes from the repository, so only a person who can push to it can change the model, for example a slow regular expression in text({ pattern }).

The supply chain

  • The image is signed without a key, with GitHub's OIDC identity, and carries an SBOM and build provenance. Self-hosting has the command that verifies it.
  • The npm packages carry provenance from the release workflow when the repository is public.
  • CI audits the dependencies that Mica ships, the libraries in the editor's bundle included, on every change and every week.

Which claims of the identity provider Mica trusts

  • Subject: the sub claim with the issuer. It never changes for a person. The subject rule is the strictest.
  • Groups: the claim in MICA_OIDC_GROUPS_CLAIM, as the provider sends it. Whoever can change a group at the provider can admit a person. Make sure that only your administrators can create a group with an admitted name. For example, the GitHub connector of Dex puts GitHub organisation names into the groups claim, and each GitHub user can create an organisation: see Other sign-in providers.
  • Email and domain: only with email_verified. The provider vouches for the address, not for the person who owns the domain. Google reports a personal Google account with an address at your domain as verified. So with Google, a domain rule also needs the Workspace domain of the account (the hd claim) in MICA_ADMIT_DOMAINS. When Google is behind a broker, such as Dex or Keycloak, the broker is the issuer, and Mica cannot check hd: limit the broker to your Workspace domain (in Dex, hostedDomains), or use a group or subjects. With another public provider, use a group or subjects.

What Mica does for you

  • It signs in with the OIDC authorization code flow, with PKCE, state and nonce.
  • Session cookies are HttpOnly and SameSite=Lax. Over https they are also Secure, with a __Host- name, so that another subdomain cannot set or replace them.
  • Over https, every response carries Strict-Transport-Security. The self-hosted Mica refuses to start with an http:// public address, except on this machine.
  • The editor's API and the consent page for agents refuse a write from another origin. Each write of the editor but the sign-out needs a matching CSRF token. The sign-out needs none, so that it works also with an expired session. The agent endpoints /mcp, /oauth/token and /oauth/register take a bearer token or the OAuth flow instead, as OAuth requires.
  • On the consent page for an agent, Allow works only one second after the page opens, and Mica refuses a sooner one. A click that a page on another site hides behind a pop-up window cannot allow an agent.
  • Sign-in attempts are rate limited.
  • Responses carry a Content-Security-Policy that allows the same origin only, X-Content-Type-Options: nosniff, X-Frame-Options: DENY and Referrer-Policy: same-origin. Only when the model has a coordinates field with a map does the policy also allow the hosts of that map for images and connections, and blob: workers for MapLibre.
  • The editor loads nothing from another origin. Its fonts are served by Mica itself. The one exception is a map, as above.
  • Mica refuses SVG files, because they can contain scripts: it does not accept them as uploads and does not show them in the editor. Mica checks the type of each upload from its bytes, and stores only the types of its table: images, PDF, GPX, KML, GeoJSON, ZIP and CSV. Media files are served in a sandbox, and files that are not images are sent as downloads.
  • The self-hosted Mica refuses to start when no admission rule is set.
  • Git runs with its hooks off, and with Git LFS as the only filter. A .lfsconfig in the repository cannot send the files to another server. The self-hosted Mica never fetches submodules.

Hardening a self-hosted deployment

  1. Put Mica behind a reverse proxy with HTTPS.
  2. Set MICA_PUBLIC_URL to the https:// address. Mica refuses anything else, except an address on this machine for a trial.
  3. Set MICA_TRUST_PROXY=true only behind a proxy that you control. Otherwise, clients can spoof their address and avoid the rate limit.
  4. Give the repository token access to that one repository only: on GitHub, a fine-grained token with Contents read and write; on GitLab, a project access token with read_repository and write_repository. The token can push anything in that repository, CI files included: see "What the repository token can reach" above.
  5. Generate MICA_SESSION_SECRET with at least 32 random characters, for example openssl rand -base64 48, and keep it out of Git.
  6. Admit as few people as you need. Prefer a group at your provider to a whole domain.
  7. Keep the image up to date. Only the latest release gets security fixes.
  8. Back up the data volume. It holds the clone and the drafts. Publish first or stop the container, then copy the volume.