No description
  • Python 52.3%
  • Shell 44.4%
  • Dockerfile 3.3%
Find a file
Civan Yavuzsen f7fc16e214
All checks were successful
build-image / build (push) Successful in 7m21s
update Docker login credentials in build workflow
2026-05-30 21:42:47 +02:00
.forgejo/workflows update Docker login credentials in build workflow 2026-05-30 21:42:47 +02:00
.env.example improve with docker 2026-05-30 19:43:33 +02:00
.gitignore improve with docker 2026-05-30 19:43:33 +02:00
config.example.json improve with docker 2026-05-30 19:43:33 +02:00
config.json improve with docker 2026-05-30 19:43:33 +02:00
docker-compose.yml add build actions 2026-05-30 19:56:18 +02:00
Dockerfile improve with docker 2026-05-30 19:43:33 +02:00
install.sh improve with docker 2026-05-30 19:43:33 +02:00
README.md add build actions 2026-05-30 19:56:18 +02:00
trigger_server.py fix some issues 2026-05-30 00:02:16 +02:00
triggerserver.service improve with docker 2026-05-30 19:43:33 +02:00
uninstall.sh first commit 2026-05-29 23:52:13 +02:00

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 argv per command, per-parameter regex validation, shell=False, shared-secret auth, constant-time token compare.
  • Hot-reload: edit config.json and 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 argv arrays are defined server-side in config.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-docker on 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