Plugins
Adding applications, storage backends and site-specific steps to a cluster without forking the installer — and what trusting a plugin really means.
What a plugin is
Someone running a community cloud should be able to add their own application, storage backend, or site-specific checks without forking the installer. A plugin is a set of packages and commands that can be run on their own or composed into bundles, carrying its own manifest templates.
mastodon/
plugin.yaml # metadata, packages, bundles, declarative commands
templates/ # rendered exactly like k8s/
Mastodon.values.yaml
Mastodon.database.yaml
The manifest describes what the plugin adds, in the same shape as the installer's built-in package catalogue:
apiVersion: community-cloud/v1
name: mastodon
description: Federated social, backed by the cluster's Postgres
type: package
packages:
- name: mastodon
chart: { repo: https://charts.example.invalid, name: mastodon }
version: 6.1.0
namespace: cc-social
requires: [cloudnative-pg]
valuesFile: plugin://mastodon/templates/Mastodon.values.yaml
manifests:
before: [plugin://mastodon/templates/Mastodon.database.yaml]
secrets:
- name: mastodon-db
description: the Mastodon database role
username: mastodon
labels: { cnpg.io/reload: "true" }
A plugin's own files are referred to with a plugin://<name>/<path> URL, anywhere a manifest path can appear — values files, manifests.before and after, and run-template. requires is resolved with the same dependency ordering as built-in packages, and secrets are generated during the install like any other.
Authentik is the worked example: a chart, a values template, a before-manifest, a CloudNative-PG database, a generated secret, and a dependency on another package.
Installing a plugin
Plugins live in one directory, ~/.community-cloud/plugins/, either unpacked or as a single file:
~/.community-cloud/
└── plugins/ # mode 0700
├── mastodon.ccp # a packaged plugin
├── forgejo.ccp
└── site-checks/ # or an unpacked one, while developing
├── plugin.yaml
└── templates/
A directory is the development form, so you can edit files without repackaging. A .ccp file is the shipping form — an ordinary zip under another name, read in memory without being unpacked. plugin.yaml has to be at the root of the archive, so zip the plugin's contents rather than its directory:
Being in the directory isn't consent
The directory says what's available; the configuration says what's enabled. Nothing loads because it was dropped in a folder:
"plugins": {
"mastodon": {
"enabled": true,
"sha256": "3f1c…"
}
}
Enable a plugin without a sha256 and the installer prints everything it would add — the charts, the manifests, the credentials, and the actual shell of every command — then refuses to run and gives you the line to paste. Once pinned, it won't ask again until the plugin changes. It's the SSH host key model: trust on first use.
Naming
A plugin's name comes from plugin.yaml, not its filename. Everything it registers is prefixed with that name, so a plugin called mastodon adding a media bundle contributes mastodon:media. A thing named after its own plugin isn't doubled: Authentik's package is authentik, not authentik:authentik.
Plugin types
Every plugin declares a type, and the type is a ceiling on what it may contain. A plugin declaring package that ships a command is refused when it's read, not when the command runs.
| Type | May contain | Status |
|---|---|---|
package |
Charts, manifests, secrets, values. No commands. | Supported — enforced by counting commands |
node |
Arbitrary shell, run as root on the nodes it's pointed at | Supported — not enforceable, and said so |
inspection |
Facts and probes from a fixed catalogue, with assertions. No shell. | Refused until the probe catalogue exists |
code |
TypeScript executed inside the installer | Not shipped |
Trust
Installing a plugin means "run this on every machine I own, as root." There's no sandbox, and there can't be one: you can't sandbox a tool whose job is running root commands on your machines. What can be done is to separate the ways a plugin can do harm, refuse capabilities that aren't needed, and make what will run visible before it runs.
| Path | What it reaches | What actually limits it |
|---|---|---|
| Node — root shell over SSH | Everything on every node the bundle runs against | The SSH user's real privileges, enforced by the node's OS |
| Installer — code plugins | The cluster token, registry passwords, Nebula API key, SSH keys | Not shipping code plugins |
| Cluster — charts and images | Whatever a pod can reach | Cluster hardening: admission policy, registry allowlist, network policy |
The third row is reachable by a plugin with no commands at all: a package plugin that installs a Helm chart can pull any image it likes. Restricting a plugin to manifests moves the attack surface; it doesn't remove it.
A few things that look like boundaries and aren't:
- A command's declared purpose is the author's claim. A malicious plugin can label anything
Inspect. It guards against carelessness, not malice. - Declarative isn't safer than code for node damage. The same shell runs either way. What declarative buys is reviewability — the exact shell can be printed before anything runs.
- Read what you pin. The approval prompt shows every command and manifest precisely so that "trust this stranger" becomes "read these forty lines."
The real controls are in the environment. Harden the cluster — Pod Security Admission, a registry allowlist with digest pinning, default-deny network policy with Cilium — and treat the machine holding cc.config.json as the most privileged machine you have.
What's still open
The plugin design is still evolving. Open questions include how plugin bundles order against each other, whether a plugin's checks should affect doctor's overall result, how compatibility across apiVersions is handled, and whether plugins can contribute sections to the interactive configure flow. For now, a plugin's settings are written into cc.config.json by hand.
The full design document, including measurements of the packaging formats considered, is docs/plugins.md in the repository.