- Linux (Ubuntu / Debian recommended)
- Python 3.8+
python3-venvpackage- systemd (for service management)
- Root privileges
sudo ./install.sh <install_dir> <config_dir>Example:
sudo ./install.sh /opt/vigrid-shell-api /etc/vigrid-shell-apiThis will:
- Copy application files to
<install_dir>/ - Copy documentation, test script, and uninstall script to
<install_dir>/ - Create a Python virtual environment at
<install_dir>/venv/ - Install Python dependencies (Flask, PyYAML)
- Copy the default configuration to
<config_dir>/vigrid-shell-api.conf - Install a systemd service (
vigrid-shell-api.service) - Create the log file at
/var/log/vigrid-shell-api.log
sudo ./uninstall.sh <install_dir> <config_dir>A copy of uninstall.sh is also placed in <install_dir>/ during installation.
This removes everything created by install.sh (service, venv, config, logs).
The source directory is never modified.
File: <config_dir>/vigrid-shell-api.conf (YAML format).
bind: "0.0.0.0" # 0.0.0.0 = all interfaces
port: 8443ssl:
enabled: true
certificate: "/path/to/server.crt"
private_key: "/path/to/server.key"When ssl.enabled is false or the files are missing, the API falls back to
plain HTTP.
run_as: "root" # Default user for command execution
dry_run: false # true = log commands but do not executecommand_path: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"Colon-separated list of directories searched to resolve command names
(equivalent to the shell $PATH).
forbidden_chars:
- '"'
- "'"
- ";"
- "\\"
- "|"
- "&"
- "("
- ")"
- "{"
- "}"
- "$"
- "`"Characters that are not allowed in command arguments. Using a YAML list avoids any escaping ambiguity. A plain string value is also accepted and will be split into individual characters.
log_dir: "/var/log"
log_format: "syslog" # "syslog" or "json"
log_level: "INFO" # DEBUG, INFO, WARNING, ERROR, CRITICALLog file: <log_dir>/vigrid-shell-api.log (rotating, 50 MB max, 5 backups).
acl:
deny:
- "192.168.1.100/32" # Deny this specific host
allow:
- "127.0.0.0/8" # Allow localhost
- "192.168.1.0/24" # Allow this subnetEvaluation order:
- Check deny list -- first CIDR match --> REJECT
- Check allow list -- first CIDR match --> ACCEPT
- Default --> REJECT
This allows denying a specific IP while allowing its surrounding subnet.
tokens:
admin:
secret: "your-secret-token-here"
user: "root"
allow_commands:
- ".*"
deny_commands:
- "^shutdown"
- "^reboot"
monitor:
secret: "monitor-token-here"
user: "nobody"
allow_commands:
- "^ls"
- "^ps"
deny_commands: []Each token defines:
| Field | Description |
|---|---|
secret |
The bearer token string |
user |
Unix user under which commands run (identity switching via sudo) |
allow_commands |
Regex patterns -- first match accepts |
deny_commands |
Regex patterns -- first match rejects |
Command authorization order:
- Check
deny_commands-- first regex match --> REJECT - Check
allow_commands-- first regex match --> ACCEPT - Default --> REJECT
Regex patterns are matched against the full command string
(<command> <arg1> <arg2> ...).
If the Unix user associated with a token does not exist on the system or has
a disabled shell (/usr/sbin/nologin, /bin/false), the request is rejected.
User identity switching:
When a token specifies a user that differs from the user running the API
process (typically root), the command is executed via
sudo -u <user> -- <command> <args...>. The system administrator must
configure /etc/sudoers accordingly (e.g., NOPASSWD entries).
sudo systemctl start vigrid-shell-api
sudo systemctl stop vigrid-shell-api
sudo systemctl restart vigrid-shell-api
sudo systemctl status vigrid-shell-api
sudo systemctl enable vigrid-shell-api # start on bootThe service restarts automatically on crash (Restart=always, RestartSec=3).
The API cannot be stopped via any API call -- only systemctl works.
# Foreground
<install_dir>/venv/bin/python3 <install_dir>/vigrid-shell-api.py \
-c <config_dir>/vigrid-shell-api.conf --foreground
# Debug (verbose logging to stderr + log file, implies --foreground)
<install_dir>/venv/bin/python3 <install_dir>/vigrid-shell-api.py \
-c <config_dir>/vigrid-shell-api.conf --debug
# Dry-run override from CLI
... --foreground --dry-run
# Override bind/port from CLI
... --foreground --bind 127.0.0.1 --port 9090| Option | Description |
|---|---|
-c, --config |
Path to configuration file (required) |
-f, --foreground |
Run in foreground |
-d, --debug |
Debug mode (implies --foreground) |
--dry-run |
Force dry-run mode |
--bind |
Override bind address |
--port |
Override listen port |
-v, --version |
Show version |
All endpoints except /api/v1/health require:
Authorization: Bearer <token>
All request bodies must be JSON. All responses are JSON.
Health check. No authentication. IP ACL still applies.
{"status": "ok", "service": "vigrid-shell-api", "version": "1.0.0", "timestamp": "..."}Execute a shell command.
Request:
{
"command": "ls",
"arguments": ["-la", "/tmp"],
"synchronous": true,
"timeout": 30
}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
command |
string | yes | -- | Command name |
arguments |
list | no | [] |
Command arguments |
synchronous |
bool | no | false |
Block until done |
timeout |
int | no | null |
Seconds before kill |
Response (synchronous, HTTP 200):
{
"status": "ok",
"order_id": "uuid",
"result": {
"stdout": "...",
"stderr": "...",
"return_code": 0,
"dry_run": false
}
}Response (asynchronous, HTTP 202):
{
"status": "ok",
"order_id": "uuid",
"message": "Command queued for execution"
}List pending and running orders.
{
"status": "ok",
"queue_size": 1,
"orders": [
{
"order_id": "...",
"command": "sleep",
"arguments": ["60"],
"status": "running",
"queued_at": "...",
"started_at": "...",
"token_name": "admin",
"client_ip": "127.0.0.1",
"synchronous": false
}
]
}Send a signal to the currently running command.
Request:
{"signal": "SIGTERM"}Supported signals: SIGHUP, SIGTERM, SIGKILL, SIGINT, SIGUSR1, SIGUSR2.
Reload the configuration from disk. This updates the config hash used for integrity checking.
{"status": "ok", "message": "Configuration reloaded successfully"}Service status and queue information.
{
"status": "ok",
"service": "vigrid-shell-api",
"version": "1.0.0",
"dry_run": false,
"queue_depth": 0,
"current_order": null,
"timestamp": "..."
}| Code | Meaning |
|---|---|
| 200 | Success |
| 202 | Accepted (async command queued) |
| 400 | Bad request (invalid JSON, missing fields, forbidden chars) |
| 401 | Authentication failed |
| 403 | Access denied (IP ACL, command not authorized, user invalid) |
| 404 | Command or endpoint not found |
| 405 | Method not allowed |
| 500 | Internal server error |
| 503 | Configuration integrity check failed |
# Execute synchronously
curl -s -X POST http://127.0.0.1:8443/api/v1/execute \
-H "Authorization: Bearer VigridAdmin-Secret-Token-Change-Me" \
-H "Content-Type: application/json" \
-d '{"command": "ls", "arguments": ["-la", "/tmp"], "synchronous": true}'
# Execute asynchronously
curl -s -X POST http://127.0.0.1:8443/api/v1/execute \
-H "Authorization: Bearer VigridAdmin-Secret-Token-Change-Me" \
-H "Content-Type: application/json" \
-d '{"command": "ps", "arguments": ["aux"]}'
# Health check
curl -s http://127.0.0.1:8443/api/v1/health
# Service status
curl -s http://127.0.0.1:8443/api/v1/status \
-H "Authorization: Bearer VigridAdmin-Secret-Token-Change-Me"
# Queue listing
curl -s http://127.0.0.1:8443/api/v1/queue \
-H "Authorization: Bearer VigridAdmin-Secret-Token-Change-Me"
# Kill running command
curl -s -X POST http://127.0.0.1:8443/api/v1/kill \
-H "Authorization: Bearer VigridAdmin-Secret-Token-Change-Me" \
-H "Content-Type: application/json" \
-d '{"signal": "SIGKILL"}'
# Reload configuration
curl -s -X POST http://127.0.0.1:8443/api/v1/reload \
-H "Authorization: Bearer VigridAdmin-Secret-Token-Change-Me" \
-H "Content-Type: application/json" \
-d '{}'- Change default tokens immediately after installation
- Restrict IP ACLs to trusted networks only
- Use HTTPS in production
- Least privilege: create tokens with minimal command access
- Config protection: the API detects disk modifications and rejects all requests until restarted (or reloaded via the reload endpoint)
- The API cannot be stopped via API calls -- only
systemctl - Forbidden characters in arguments prevent shell injection
- Commands are resolved via a fixed
command_path, not the system$PATH - User switching via sudo allows per-token isolation of privileges
A comprehensive test script is included:
# Full mode: install, HTTP tests, HTTPS tests, dry-run tests, uninstall
sudo ./test-api.sh
# Test-only mode: against an already running service
sudo ./test-api.sh -t 127.0.0.1 8443 http
sudo ./test-api.sh -t 127.0.0.1 8443 httpsThe full-mode test:
- Installs the API into a temporary directory
- Generates a test configuration with dedicated tokens
- Creates a temporary Unix user (
_vigridtest) for identity switching tests - Runs 80+ tests covering all endpoints, authentication, authorization, forbidden characters, error handling, queue management, kill, timeout, deny-overrides-allow priority, user identity switching, and log verification
- Tests HTTP, HTTPS (with auto-generated self-signed certificate), and dry-run
- Verifies each operation against the log file
- Uninstalls and verifies cleanup
Client --> [IP ACL] --> [Auth] --> [Command Auth] --> [Char Check] --> [Queue]
|
[FIFO Worker]
|
[sudo -u user]
|
[subprocess]
- All commands execute in a single FIFO queue (strictly ordered, one at a time)
- Synchronous requests block until their command completes
- Asynchronous requests return immediately with an order UUID
- The queue worker runs in a dedicated background thread
- systemd restarts the service automatically on crash
- Each token can specify a different Unix user for execution