What is encrypted, what is not, and what that is worth
A gateway config names your endpoints, your tools, and the identity that gates them. This page states plainly what the service can see and what it cannot, including the parts that are uncomfortable.
The construction
When you publish, this page generates a 256-bit secret and a 16-byte salt. From the secret it derives three separate values with HKDF-SHA256: the AES key that encrypts the config, an edit token that proves the right to change it, and a burn receipt that proves the holder could decrypt it. The service receives the salt and a SHA-256 digest of the last two. It never receives the secret or the key.
The config is sealed with AES-256-GCM: a 256-bit key, a fresh 12-byte nonce generated immediately before use, and a 128-bit authentication tag. The associated data is mcpg-config-v1|{address}|{version}|{expires}|{one-time}|{salt}, so the tag covers every term of the config as well as its contents. A blob moved into another record, replayed under a different version, given a longer life, or quietly turned from one-time into permanent fails authentication rather than decrypting into something you did not publish. Changing any of those requires the secret, which means it produces a new version.
A reader rebuilds that string from the address it asked for and the parameters it was served, and never from the service’s own copy of it — taking the service’s copy would hand the binding back to the party it defends against. It also re-checks the expiry after decrypting, against its own clock rather than against the column the service reports.
What the service can see
Encryption hides the config; it does not hide the config’s shadow. A database dump reveals the ciphertext’s length, and therefore roughly how large your config is; when it was published, updated, and expires; how many times it has been fetched, and when; whether it is one-time; and its version count.
It reveals nothing else. Not the config’s name — that lives inside the ciphertext, because a label like acme-prod-stripe-gateway leaks plenty on its own. Not a hostname, a tool name, a plugin, an environment variable reference, or any credential.
What the fragment buys, and what it does not
The share link puts the secret after a #. A URL fragment is dereferenced by the browser and is never placed in the request: it is absent from the origin’s access log, from any CDN or load balancer in front of it, from the Referer header on outbound navigation, and from this service’s database — which has no field that could hold it.
That defends against the server. It does not defend against the channel. The share link is one string, and it goes into chat transcripts, browser history, bookmark sync, and password manager URL fields. Anyone holding the whole link holds the plaintext. Send the address and the secret separately, or send the address alone.
The limit worth stating outright
The page that reads your secret is served by this origin. A compromised or malicious build of this app could ship JavaScript that sends the secret somewhere else, and no schema fixes that — it is the irreducible limit of browser-delivered end-to-end encryption.
What actually helps, and it is worth being precise rather than reassuring. Every page here is served with script-src 'self' 'nonce-…' 'strict-dynamic' and no 'unsafe-inline', so an injected inline script does not execute at all — that is the control that stops the attack rather than narrowing it. This app loads no third-party script, font, or image on any route, and connect-src names this origin alone. There are no redirects on /c/, because a redirect re-attaches your fragment to a destination you never inspected.
What that does not close: no CSP directive restricts top-level navigation. Script that does get to run can still leave with location = 'https://…', and no header available to us prevents it. connect-src, form-action and base-uri narrow the ways out; they do not seal them.
If you will not extend that trust — and it is a reasonable thing not to extend — never open the config in a browser at all. Give the address and the secret to the gateway and let it decrypt: it fetches the ciphertext itself, derives the key itself, and runs no code from this origin. That is the path below, and it is the one the product is actually for.
Running a gateway against a stored config
Give the gateway the config source and put the secret in its environment. The secret must not appear as a command-line argument: an argument is visible in ps to every user on the host, and it lands in shell history.
export MCPG_CONFIG_SECRET_FILE=/run/secrets/mcpg-config-secret
mcpg --config mcpg+enc:https://mcpg.cloud/configs/c/<address>A file is the preferred form everywhere, not just in Kubernetes: an environment variable is readable through /proc/<pid>/environ for the life of the process and lands in core dumps, and the gateway does not try to unset it afterwards — removing a variable while other threads may be reading the environment is undefined behaviour, and a documented exposure is better than one papered over with a data race. In Kubernetes a Secret mounts as exactly this file.
MCPG_CONFIG_SECRET takes the value inline when a file is impractical, and MCPG_CONFIG_SECRETS_FILE takes a 0600 JSON map of address to secret when one gateway boots from several encrypted layers. Setting the file form and the inline form to different values is a boot error rather than a precedence question.
To boot only a version you have seen, pin it: mcpg+enc:https://mcpg.cloud/configs/c/<address>/v/7. Every version is authentic for its own version number, so an older one verifies perfectly and nothing in the cipher can flag it — pinning is the only complete defence against a service that answers “latest” with yesterday’s config. MCPG_CONFIG_MIN_VERSION is the looser form: a floor rather than an exact match.
Validation happens here, or not at all
The service cannot check your config, because it cannot read it. That is a property, not a gap — but it means the shape checks in the review step are the only ones that run before publishing, and they are a fast pre-flight rather than the gateway’s own validator.
mcpg config check is the authoritative one. It resolves plugins, validates every backend spec against the plugin that owns it, and catches things a browser cannot.
Losing the secret, and rotating it
A lost secret cannot be recovered. There is no reset, because there is nothing on this side to reset against. Publish a new config.
Rotation is forward-only. Re-encrypting under a fresh secret stops the old one working from that version onward, but it does not un-leak a blob somebody already fetched. Treat a leaked share link as a config that has been read, and change what it exposes rather than only its secret.