- Python 52.3%
- Shell 44.4%
- Dockerfile 3.3%
|
|
||
|---|---|---|
| .forgejo/workflows | ||
| .env.example | ||
| .gitignore | ||
| config.example.json | ||
| config.json | ||
| docker-compose.yml | ||
| Dockerfile | ||
| install.sh | ||
| README.md | ||
| trigger_server.py | ||
| triggerserver.service | ||
| uninstall.sh | ||
trigger-server
A small HTTP server that runs whitelisted commands on the host when triggered from another device on the same network. You define the commands and their allowed parameters in config.json; trigger-server validates incoming requests and executes only what you've explicitly allowed.
Useful for: home-lab automation, remote service restarts, ops shortcuts from a phone, glue between things on your LAN that don't speak HTTP.
┌─────────────┐ curl ?param=…&token=… ┌──────────────────────┐
│ phone / │ ───────────────────────▶ │ trigger-server │
│ laptop / │ │ (port 65432) │
│ shortcut / │ ◀─────────────────────── │ → your command │
│ cron job │ JSON stdout/err │ (shell=False) │
└─────────────┘ └──────────────────────┘
Highlights
- Single Python file, stdlib only — no dependencies.
- Two deployment paths: Docker (
docker compose up) or native systemd (sudo ./install.sh). - Defines safety up front: fixed
argvper command, per-parameter regex validation,shell=False, shared-secret auth, constant-time token compare. - Hot-reload: edit
config.jsonand the next request picks it up — no restart. - Empty by default: ships with no preconfigured commands. You decide what's allowed.
Security model
This is a server that runs commands on the host. The whole design is about giving the network the ability to trigger commands without giving it shell access:
- Commands and their
argvarrays are defined server-side inconfig.json. The network can never supply program names or fixed flags. - The network can only supply named parameters, each gated by a per-parameter regex (fullmatch). Values that don't match are rejected with HTTP 400.
- All commands run with
shell=False— no shell metacharacter interpretation, ever. - Requests must include a shared secret (
Authorization: Bearer <token>or?token=<token>). Token comparison is constant-time.
Caveats you should understand before deploying:
- A leaked token + access to the listening port lets the holder run any configured command with any value that passes your regex. Tightness of regex = tightness of authorization.
- If you grant the server access to the Docker socket (mounted in the container, or
--with-dockeron systemd), commands that talk to Docker can do anything Docker can do — which is effectively root. - This server has no rate limiting and no HTTPS. Run it on a trusted LAN. Don't expose port 65432 to the internet without a TLS reverse proxy and stricter access controls.
Run with Docker
Requirements: Docker Engine + Compose plugin.
git clone <repo-url> trigger-server && cd trigger-server
cp .env.example .env
echo "TRIGGER_TOKEN=$(openssl rand -hex 4)" >> .env
docker compose up -d --build
If your commands need access to the host Docker socket, also set DOCKER_GID in .env (getent group docker | cut -d: -f3) and uncomment the socket bind mount in docker-compose.yml.
Read the token back any time:
grep ^TRIGGER_TOKEN .env
Pull pre-built image (Forgejo registry)
A multi-arch image (linux/amd64, linux/arm64) is built and pushed by the Forgejo Actions workflow at .forgejo/workflows/build.yml on every push to main, every v* tag, and on manual dispatch. To use it, comment out the build: block in docker-compose.yml and uncomment the image: line:
# build:
# context: .
# args:
# DOCKER_GID: "${DOCKER_GID:-999}"
image: git.xlith.net/xlith/trigger-server:latest
Then:
docker login git.xlith.net # if the registry requires auth
docker compose pull
docker compose up -d
Available tags: latest, sha-<short> per main commit, and <major> / <major>.<minor> / <major>.<minor>.<patch> for v* tags.
Run with systemd
Requirements: Debian/Ubuntu (or any systemd distro), Python 3, openssl.
git clone <repo-url> trigger-server && cd trigger-server
sudo ./install.sh # or: sudo ./install.sh --with-docker
install.sh creates a triggerserver system user, copies the app to /opt/triggerserver/, generates a random token at /etc/triggerserver/triggerserver.env, installs the systemd unit, and starts the service. Pass --with-docker only if your commands need the docker socket — it adds the service user to the host docker group.
Read the token:
sudo cat /etc/triggerserver/triggerserver.env
Open the port
Allow LAN access to port 65432:
sudo ufw allow from 192.168.0.0/16 to any port 65432 proto tcp
Use
From any device on the LAN:
# Discovery (no auth)
curl http://<host>:65432/
# Trigger (token via query string — easy from a browser or shortcut)
curl "http://<host>:65432/run/<command>?<params>&token=$TOKEN"
# Or via header
curl -H "Authorization: Bearer $TOKEN" \
"http://<host>:65432/run/<command>?<params>"
Response is JSON: {ok, returncode, stdout, stderr}.
Status codes: 200 on success, 400 invalid/missing params, 401 bad token, 404 unknown command, 500 subprocess error/timeout.
Configure commands
Edit config.json. Changes apply on the next request — no restart needed.
{
"commands": {
"<name>": {
"argv": ["program", "fixed", "args"],
"params": {
"<param>": {
"regex": "^…$", // fullmatch; reject anything that doesn't match
"required": true,
"flag": "--name" // optional: prepended before the value
}
}
}
}
}
Each declared param appends one (or two, if flag is set) element to argv in declared order. Unknown query-string keys are rejected with 400.
See config.example.json for runnable patterns: no-arg commands, single positional args, regex whitelists (^(nginx|redis|app)$), multi-param commands with optional flags, URL validation.
Operate
| Action | Docker | systemd |
|---|---|---|
| Status | docker compose ps |
systemctl status triggerserver |
| Logs | docker compose logs -f |
journalctl -u triggerserver -f |
| Restart | docker compose restart |
sudo systemctl restart triggerserver |
| Rotate token | edit TRIGGER_TOKEN in .env, then docker compose up -d |
edit /etc/triggerserver/triggerserver.env, then restart |
| Stop | docker compose down |
sudo systemctl stop triggerserver |
| Uninstall | docker compose down -v && docker rmi trigger-server:latest |
sudo ./uninstall.sh (--purge to also remove user/token) |
Project layout
trigger_server.py # the server (stdlib HTTP, no deps)
config.json # active command map (empty by default)
config.example.json # reference patterns
Dockerfile # python:3.12-alpine + docker CLI
docker-compose.yml # Docker deployment
.env.example
triggerserver.service # systemd unit
install.sh / uninstall.sh