https://github.com/nickadam/rikugan
A remote management and monitoring tool for device management. A single binary that can run as either a server or an agent (client), with websocket communication, scheduled command execution, file synchronization, and comprehensive logging.
A docker image is available at https://hub.docker.com/r/nickadam/rikugan
# Ensure Go 1.21+ is installed
go mod tidy
go build -o rikugan .
# Cross-compile for different platforms
GOOS=linux GOARCH=amd64 go build -o rikugan-linux-amd64 .
GOOS=windows GOARCH=amd64 go build -o rikugan-windows-amd64.exe .
GOOS=darwin GOARCH=amd64 go build -o rikugan-darwin-amd64 .
GOOS=darwin GOARCH=arm64 go build -o rikugan-darwin-arm64 .
Running without parameters starts the server with auto-generated tokens:
./rikugan
Or explicitly:
./rikugan -server \
-port 8080 \
-data-dir ./data \
-admin-token "your-admin-token" \
-agent-token "your-agent-token"
Server Options:
| Flag | Default | Description |
|---|---|---|
-server | (default mode) | Run in server mode |
-port | 8080 | HTTP/WebSocket server port |
-data-dir | ./data | Directory for persistent data |
-admin-token | (auto-generated) | Token for admin API authentication |
-agent-token | (auto-generated) | Token for agent authentication |
Server Environment Variables:
The server supports configuration via environment variables. Command-line flags take precedence over environment variables.
| Variable | Description |
|---|---|
DATA_DIR | Directory for persistent data (equivalent to -data-dir) |
ADMIN_TOKEN | Admin authentication token (equivalent to -admin-token) |
ADMIN_TOKEN_FILE | Path to file containing admin token (for secrets management) |
AGENT_TOKEN | Agent authentication token (equivalent to -agent-token) |
AGENT_TOKEN_FILE | Path to file containing agent token (for secrets management) |
The _FILE variants are checked first, allowing integration with Docker secrets, Kubernetes secrets, or other secrets management systems. Example:
# Using environment variables directly
export ADMIN_TOKEN="my-admin-token"
export AGENT_TOKEN="my-agent-token"
export DATA_DIR="/var/lib/rikugan"
./rikugan
# Using file-based secrets (e.g., Docker/Kubernetes secrets)
export ADMIN_TOKEN_FILE="/run/secrets/admin_token"
export AGENT_TOKEN_FILE="/run/secrets/agent_token"
./rikugan
Token Persistence:
When tokens are not provided via flags or environment variables, the server will:
<data-dir>/.admin_token and <data-dir>/.agent_tokenThis ensures tokens remain stable across server restarts without requiring explicit configuration.
Log Rotation Options:
| Flag | Default | Description |
|---|---|---|
-log-rotate | false | Enable log rotation |
-log-rotate-daily | true | Rotate logs at midnight UTC |
-log-max-size-mb | 0 | Rotate when log exceeds this size (0 = no limit) |
-log-max-age-days | 0 | Delete logs older than this (0 = keep forever) |
-log-max-files | 0 | Max rotated files to keep (0 = unlimited) |
Example with log rotation:
./rikugan -server \
-log-rotate \
-log-rotate-daily \
-log-max-size-mb 50 \
-log-max-age-days 7 \
-log-max-files 10
./rikugan -agent \
-server-url "http://server:8080" \
-token "agent-token-from-server"
Agent Options:
| Flag | Default | Description |
|---|---|---|
-agent | Run in agent mode | |
-server-url | (required) | Server URL (http:// or https://) |
-token | (required) | Agent authentication token |
-agent-id | (auto-generated) | Unique agent identifier (see below) |
-agent-data-dir | ./agent_data | Base directory for agent data |
Agent Directory Structure:
agent_data/
├── state/
│ └── .agent-id # Persistent unique agent ID
└── sync/
└── (synced files from server, OS-specific)
Environment Variables:
The agent sets the following environment variable on startup, which is inherited by all child processes (scheduled and ad-hoc commands):
| Variable | Description |
|---|---|
RIKUGAN_SYNC_DIR | Absolute path to the agent's sync directory |
This allows commands to reference synced files without knowing the exact path:
$RIKUGAN_SYNC_DIR/myscript.sh%RIKUGAN_SYNC_DIR%\myscript.batAgent ID Generation:
-agent-id is not specified, a unique ID is auto-generated as hostname-xxxxxxxx<agent-data-dir>/state/.agent-id and reused on restartAll admin API endpoints require authentication via:
Authorization: Bearer <admin_token>?admin_token=<admin_token>GET /api/commands
Response:
[
{
"id": "abc123",
"command": "df -h",
"interval_sec": 300,
"os": "linux",
"created_at": "2024-01-15T10:30:00Z"
}
]
POST /api/commands
Content-Type: application/json
{
"command": "df -h",
"interval_sec": 300,
"os": "linux"
}
Command Fields:
| Field | Required | Description |
|---|---|---|
command | Yes | Shell command to execute |
interval_sec | Yes | Minimum seconds between executions |
os | No | Target OS: linux, windows, or all (default: all) |
id | No | Custom ID (auto-generated if omitted) |
Response:
{
"id": "abc123def456",
"command": "df -h",
"interval_sec": 300,
"os": "linux",
"created_at": "2024-01-15T10:30:00Z"
}
DELETE /api/commands?id=abc123
Response:
{
"deleted": "abc123"
}
GET /api/agents
Response:
[
{
"id": "workstation-001",
"os": "linux",
"connected_at": "2024-01-15T09:00:00Z",
"last_seen": "2024-01-15T10:30:00Z",
"connected": true
}
]
Execute a one-time command on a specific agent.
POST /api/exec
Content-Type: application/json
{
"agent_id": "workstation-001",
"command": "whoami",
"timeout_sec": 30,
"wait": true
}
Request Fields:
| Field | Required | Default | Description |
|---|---|---|---|
agent_id | Yes | Target agent ID | |
command | Yes | Shell command to execute | |
timeout_sec | No | 60 | Command timeout in seconds |
wait | No | false | If true, wait for result before responding |
Response (wait=true):
{
"id": "adhoc-abc123def456",
"agent_id": "workstation-001",
"command": "whoami",
"status": "completed",
"result": {
"agent_id": "workstation-001",
"command_id": "adhoc-abc123def456",
"command": "whoami",
"stdout": "root\n",
"stderr": "",
"return_code": 0,
"start_time": "2024-01-15T10:30:00Z",
"execution_time_sec": 0.015
}
}
Response (wait=false):
{
"id": "adhoc-abc123def456",
"agent_id": "workstation-001",
"command": "whoami",
"status": "sent"
}
Status Values:
| Status | Description |
|---|---|
sent | Command sent to agent (fire-and-forget mode) |
pending | Waiting for agent response |
completed | Command executed, result available |
timeout | Command timed out |
error | Error sending command or agent not connected |
Note on Windows Paths:
Backslashes in JSON must be escaped. To send dir c:\Windows:
"command": "dir c:\\Windows"\\\\ (shell escapes to \\, JSON decodes to \)"command": "dir c:/Windows"Files are stored in OS-specific directories on the server. All file operations require the os parameter (linux or windows).
GET /api/files?os=linux
GET /api/files?os=windows
Response:
[
{
"name": "install-agent.sh",
"size": 1024,
"mod_time": "2024-01-15T08:00:00Z"
}
]
POST /api/files?os=linux
Content-Type: multipart/form-data
[email protected]
Response:
{
"uploaded": "install-agent.sh",
"os": "linux"
}
DELETE /api/files?os=linux&filename=install-agent.sh
Response:
{
"deleted": "install-agent.sh",
"os": "linux"
}
GET /api/files/download?os=linux&filename=install-agent.sh
# Set your admin token
ADMIN_TOKEN="your-admin-token"
SERVER="http://localhost:8080"
# Add a command to check disk space every 5 minutes (Linux only)
curl -X POST "$SERVER/api/commands" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"command": "df -h",
"interval_sec": 300,
"os": "linux"
}'
# Add a command to get system info every hour (Windows only)
curl -X POST "$SERVER/api/commands" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"command": "systeminfo",
"interval_sec": 3600,
"os": "windows"
}'
# Add a command for all OSes
curl -X POST "$SERVER/api/commands" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"command": "hostname",
"interval_sec": 60,
"os": "all"
}'
# List all commands
curl "$SERVER/api/commands" \
-H "Authorization: Bearer $ADMIN_TOKEN"
# Delete a command
curl -X DELETE "$SERVER/api/commands?id=abc123" \
-H "Authorization: Bearer $ADMIN_TOKEN"
# List connected agents
curl "$SERVER/api/agents" \
-H "Authorization: Bearer $ADMIN_TOKEN"
# Execute ad-hoc command (fire and forget)
curl -X POST "$SERVER/api/exec" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "workstation-001",
"command": "whoami"
}'
# Execute ad-hoc command and wait for result
curl -X POST "$SERVER/api/exec" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "workstation-001",
"command": "df -h",
"timeout_sec": 30,
"wait": true
}'
# Execute command with longer timeout
curl -X POST "$SERVER/api/exec" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "workstation-001",
"command": "apt-get update",
"timeout_sec": 300,
"wait": true
}'
# Windows commands with paths - NOTE: backslashes must be escaped in JSON
# Use \\\\ in shell (becomes \\ in JSON, which decodes to single \)
curl -X POST "$SERVER/api/exec" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "workstation-001",
"command": "dir c:\\\\Windows\\\\System32",
"wait": true
}'
# Alternative: use forward slashes (Windows cmd.exe accepts these)
curl -X POST "$SERVER/api/exec" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "workstation-001",
"command": "dir c:/Windows/System32",
"wait": true
}'
# Upload a script for Linux agents
curl -X POST "$SERVER/api/files?os=linux" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-F "file=@./scripts/install-software.sh"
# Upload a script for Windows agents
curl -X POST "$SERVER/api/files?os=windows" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-F "file=@./scripts/install-software.bat"
# List synced files for Linux
curl "$SERVER/api/files?os=linux" \
-H "Authorization: Bearer $ADMIN_TOKEN"
# List synced files for Windows
curl "$SERVER/api/files?os=windows" \
-H "Authorization: Bearer $ADMIN_TOKEN"
# Delete a synced file from Linux
curl -X DELETE "$SERVER/api/files?os=linux&filename=old-script.sh" \
-H "Authorization: Bearer $ADMIN_TOKEN"
# Execute a synced script using RIKUGAN_SYNC_DIR environment variable
curl -X POST "$SERVER/api/exec" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "workstation-001",
"command": "$RIKUGAN_SYNC_DIR/install-software.sh",
"wait": true
}'
/etc/systemd/system/rikugan-server.service:
[Unit]
Description=Rikugan Server
After=network.target
[Service]
Type=simple
User=rikugan
ExecStart=/usr/local/bin/rikugan -server -port 8080 -data-dir /var/lib/rikugan
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
/etc/systemd/system/rikugan-agent.service:
[Unit]
Description=Rikugan Agent
After=network.target
[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/rikugan -agent -server-url http://manager.example.com:8080 -token YOUR_AGENT_TOKEN
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
data/
├── commands.json # Persisted command definitions
├── results.json # Current command execution logs
├── results-2024-01-15T00-00-00.json.gz # Rotated log (when rotation enabled)
├── results-2024-01-14T00-00-00.json.gz # Older rotated log
└── sync/ # Files for agent synchronization
├── linux/ # Files synced to Linux agents
│ ├── install.sh
│ └── monitor.sh
└── windows/ # Files synced to Windows agents
├── install.bat
└── setup.msi
Results are stored in results.json.gz as newline-delimited JSON:
{"agent_id":"workstation-001","command_id":"abc123","command":"df -h","stdout":"Filesystem Size Used Avail Use% Mounted on\n/dev/sda1 50G 20G 28G 42% /\n","stderr":"","return_code":0,"start_time":"2024-01-15T10:30:00Z","execution_time_sec":0.125}
{"agent_id":"workstation-002","command_id":"abc123","command":"df -h","stdout":"Filesystem Size Used Avail Use% Mounted on\n/dev/sda1 100G 45G 50G 48% /\n","stderr":"","return_code":0,"start_time":"2024-01-15T10:30:01Z","execution_time_sec":0.098}
Read results with:
# Current log
zcat data/results.json.gz | jq .
# All logs (including rotated)
zcat data/results*.json.gz | jq .
# Search across all logs
zcat data/results*.json.gz | jq 'select(.agent_id == "workstation-001")'
When log rotation is enabled (-log-rotate), the server will:
-log-rotate-daily)-log-max-size-mb > 0)-log-max-age-days > 0)-log-max-files > 0)Rotated files are named with ISO 8601 timestamps: results-YYYY-MM-DDTHH-MM-SS.json.gz
┌──────────────────────────────────────────────────────────────┐
│ SERVER │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Admin API │ │ Commands DB │ │ Results Log (gzip) │ │
│ │ (HTTP/REST) │ │ (JSON file) │ │ (JSONL file) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
│ │ │ │ │
│ ▼ ▼ ▲ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ WebSocket Handler │ │
│ │ - Broadcasts command updates to agents │ │
│ │ - Receives execution results │ │
│ │ - Handles file sync requests │ │
│ └──────────────────────┬───────────────────────────────┘ │
│ │ │
│ ┌──────────────────────┴───────────────────────────────┐ │
│ │ Sync Files │ │
│ │ (scripts, installers) │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────┬────────────────────────────────────┘
│ WebSocket
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Agent 1 │ │ Agent 2 │ │ Agent N │
│ (Linux) │ │ (Windows)│ │ (...) │
└──────────┘ └──────────┘ └──────────┘
Server → Agent:
commands: List of commands to executefiles: List of available sync filesfile_data: Binary file content (base64 encoded)Agent → Server:
result: Command execution resultfile_request: Request for a specific fileMIT License
Content type
Image
Digest
sha256:ac553a524…
Size
5 MB
Last updated
9 months ago
docker pull nickadam/rikugan