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 readThis 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_SECRETencrypts 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 inX-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.1and[::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_SUBJECTSor 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_SECRETand 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 afterMICA_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, changeMICA_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_PUBLISHlets 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.jsonruns 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
subclaim 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 (thehdclaim) inMICA_ADMIT_DOMAINS. When Google is behind a broker, such as Dex or Keycloak, the broker is the issuer, and Mica cannot checkhd: 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,
stateandnonce. - Session cookies are
HttpOnlyandSameSite=Lax. Over https they are alsoSecure, 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 anhttp://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/tokenand/oauth/registertake 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-Policythat allows the same origin only,X-Content-Type-Options: nosniff,X-Frame-Options: DENYandReferrer-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, andblob: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
.lfsconfigin the repository cannot send the files to another server. The self-hosted Mica never fetches submodules.
Hardening a self-hosted deployment
- Put Mica behind a reverse proxy with HTTPS.
- Set
MICA_PUBLIC_URLto thehttps://address. Mica refuses anything else, except an address on this machine for a trial. - Set
MICA_TRUST_PROXY=trueonly behind a proxy that you control. Otherwise, clients can spoof their address and avoid the rate limit. - 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_repositoryandwrite_repository. The token can push anything in that repository, CI files included: see "What the repository token can reach" above. - Generate
MICA_SESSION_SECRETwith at least 32 random characters, for exampleopenssl rand -base64 48, and keep it out of Git. - Admit as few people as you need. Prefer a group at your provider to a whole domain.
- Keep the image up to date. Only the latest release gets security fixes.
- Back up the data volume. It holds the clone and the drafts. Publish first or stop the container, then copy the volume.