Build, Deploy, and Secure a Mattermost Plugin: A Practical Guide
Learn how to build a slash‑command plugin for Mattermost, bundle and install it via the CLI, and apply best‑practice security measures like signing and least‑privilege access.
17 Feb 2026, 00:16 UTC

Concrete Problem: Extending Mattermost Without Touching the Core
Many teams want to add custom slash commands, message hooks, or integrations to Mattermost, but the core server is a monolithic Go binary. Recompiling the server for every tweak is risky and hard to maintain. The question becomes: how can we add new functionality safely, keep the core untouched, and still have full control over the plugin’s behavior?
Thesis: Use Mattermost’s Plugin API, a Separate Process, and Built‑in Signing
Mattermost exposes a well‑documented plugin API that runs in its own process. This design lets you ship code as a simple .tar.gz bundle, install it through the web UI or CLI, and secure it with a signing mechanism. The following sections walk through building a minimal "Hello World" slash command, deploying it, and hardening it against common attack vectors.
1. Build the Plugin
All plugins are written in Go and must implement the mattermost-server/plugin interface. Create a new module:
mkdir hello-plugin
cd hello-plugin
go mod init github.com/example/hello-plugin
Define the plugin struct and implement the slash command handler:
package main
import (
"github.com/mattermost/mattermost-server/v7/plugin"
"github.com/mattermost/mattermost-server/v7/model"
)
// HelloPlugin is the main type for the plugin.
type HelloPlugin struct {
*plugin.MattermostPlugin
}
// ExecuteCommand is called when a user types "/hello".
func (p *HelloPlugin) ExecuteCommand(c *plugin.CommandArgs) (*model.CommandResponse, *model.AppError) {
// Validate user input - in this simple example we just echo back.
return &model.CommandResponse{Text: "Hello, " + c.User.Username + "!"}, nil
}
func main() {
plugin.ClientInit(&HelloPlugin{})
}
Compile the binary:
go build -o hello_plugin
Bundle the binary into a plugin package. The bundle must contain a plugin.json manifest describing the plugin’s name, version, and the slash commands it registers.
cat > plugin.json <<'EOF'
{
"id": "com.example.hello",
"name": "Hello World",
"description": "A simple slash command.",
"min_server_version": "7.0.0",
"server": {
"main": "hello_plugin"
},
"client": {
"main": "client.js"
},
"commands": [
{
"trigger": "hello",
"display_name": "Hello",
"description": "Say hello to the user."
}
]
}
EOF
tar -czf hello_plugin.tar.gz hello_plugin plugin.json
2. Deploy with the Mattermost CLI
Deploying through the web UI is straightforward, but the CLI allows CI pipelines. Install the plugin:
mattermost plugin install hello_plugin.tar.gz
Enable it (the CLI will also enable it if the plugin is not already enabled):
mattermost plugin enable com.example.hello
Verify that the slash command appears in the channel:
/hello
Expected response: Hello, <username>! If the command does not appear, check the server logs for any compilation or runtime errors.
3. Harden the Plugin
Because the plugin runs with elevated privileges, follow these steps:
- Least Privilege: The plugin should request only the scopes it needs. In
plugin.json, add apermissionssection listing the API calls. - Input Validation: Always sanitize user input. In the example, the input is trivial, but more complex commands should check for length, prohibited characters, or injection patterns.
- Signing: Generate a private key and sign the bundle.
openssl genrsa -out private.key 2048 mattermost plugin sign hello_plugin.tar.gz -k private.key -o hello_plugin.tar.gz.sigUpload the signature in the admin console under "Plugins → Signing" and enable signing. The server will reject any unsigned or tampered bundle.
- Isolation: If the plugin performs long‑running tasks, run it in a separate container or use goroutine pools to avoid blocking the main server thread.
4. Trade‑Offs and Limitations
Plugins are stateless by default, which keeps performance high. However:
- Stateful Plugins: Storing data in the plugin’s own database or in a shared store can increase complexity and introduce race conditions.
- Version Compatibility: The plugin API changes between major releases. A plugin built for 6.x may not compile against 7.x without code adjustments.
- Security Risk: A compromised plugin can read or write any data the server can. Thorough code reviews and dependency audits are mandatory.
Actionable Next Steps
- Clone the Mattermost server repo and run the sample plugin against the latest 7.x release to confirm it works.
- Set up a CI job that compiles, bundles, signs, and deploys the plugin using the CLI.
- Enable plugin signing in the admin console and test that tampering the bundle fails.
- Document the plugin’s required permissions and audit the code for any elevated operations.
By following this workflow, you can extend Mattermost safely, keep the core binary untouched, and maintain full control over the plugin’s lifecycle.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.