Warning
This project is experimental and intended for testing only. It is not recommended for production use.
REST API for remote ipmitool commands, built with Go, Gin, Viper, Cobra, and Zap.
Every command receives BMC credentials in its JSON body. Access requires a fixed
Bearer token, with optional HTTPS and an IP allowlist.
See BMC testing and WIP for hardware test status and planned features.
Requires Go 1.26+ and network access to the BMCs.
cp -n config.example.yaml config.yaml
chmod 600 config.yaml
openssl rand -hex 32Set the generated value as auth.token in config.yaml, then start the API:
go run . serve --config config.yamlFor live reload, install Air, goimports, and golangci-lint, then run
"$(go env GOPATH)/bin/air" -c .air.toml. Each rebuild formats, lints, builds,
and starts serve --config config.yaml. Go files, module files, and local config
changes trigger reloads; failed checks stop the API.
Startup finds ipmitool in PATH or standard system directories and verifies it
with ipmitool -V. If missing on Linux, it runs apt update and
apt install --yes --no-remove ipmitool, using noninteractive sudo when needed.
Installation requires root or suitable sudo permissions and has a 5-minute limit.
A custom ipmitool.binary path must already exist.
config.example.yaml contains all settings:
server:
address: "127.0.0.1:8080"
allowed_ips: []
tls:
enabled: false
cert_file: ""
key_file: ""
auth:
token: "YOUR_GENERATED_TOKEN"
ipmitool:
binary: "ipmitool"
timeout: "30s"
max_output_bytes: 1048576
max_concurrent: 4- TLS: set
enabled: trueand readable PEM certificate/key paths to serve HTTPS (TLS 1.2+). Withfalse, the listener serves HTTP. Restart after changes. - Allowlist: accepts IPv4/IPv6 addresses and CIDRs, such as
["127.0.0.1", "192.0.2.0/24"]. Empty allows all peers, still requiring a token. It checks the TCP peer IP and ignores forwarded headers; behind a proxy, it checks the proxy's IP. - Limits: timeout up to
24h, output from 1 to 67108864 bytes per stream, concurrency from 1 to 1024. Defaults are shown above. - Environment: override any field with
IPMI_API_plus its uppercase path, replacing dots with underscores, for exampleIPMI_API_AUTH_TOKENorIPMI_API_SERVER_TLS_ENABLED=true.IPMI_API_SERVER_ALLOWED_IPStakes a comma-separated list. The Go service does not load.envfiles.
Keep tokens and private keys outside Git. BMC credentials belong in each request. Use HTTPS for network access. Zap writes JSON lifecycle and request logs to stderr without logging credentials, request bodies, or command output.
These examples run inside a Laravel application using the
HTTP Facade. Set the API URL and token
in the Laravel application's .env:
IPMITOOL_API_URL=http://127.0.0.1:8080/api/v1
IPMITOOL_API_TOKEN=YOUR_GENERATED_TOKENAdd this entry to the array returned by config/services.php:
'ipmitool' => [
'base_url' => env('IPMITOOL_API_URL', 'http://127.0.0.1:8080/api/v1'),
'token' => env('IPMITOOL_API_TOKEN'),
],Create a reusable client factory and the connection for the requested BMC:
use Illuminate\Support\Facades\Http;
$api = fn () => Http::baseUrl(config('services.ipmitool.base_url'))
->withToken(config('services.ipmitool.token'))
->acceptJson()
->asJson()
->connectTimeout(5)
->timeout(40);
$connection = [
'host' => '192.0.2.10',
'username' => 'ADMIN',
'password' => 'bmc-password',
];
$payload = ['connection' => $connection];All paths below are relative to the base URL, which includes /api/v1. The
factory creates a fresh request for each call. Set the client timeout above the
server's command timeout. For a private CA, add
->withOptions(['verify' => '/path/to/ca.pem']) to the factory and use an HTTPS URL.
$health = $api()->get('health')->throw()->json();
$power = $api()->post('power/status', $payload)->throw()->json();
$chassis = $api()->post('chassis/status', $payload)->throw()->json();
$sensors = $api()->post('sensors', $payload)->throw()->json();
$sdr = $api()->post('sdr', $payload)->throw()->json();
$fru = $api()->post('fru', $payload)->throw()->json();
$events = $api()->post('sel/entries', $payload)->throw()->json();
$controller = $api()->post('mc/info', $payload)->throw()->json();
$lan = $api()->post('lan/1', $payload)->throw()->json();
$users = $api()->post('users/1', $payload)->throw()->json();Only health uses GET and needs no connection body. Every route requires the API
token. List responses preserve native text in stdout; they are not parsed arrays.
The following are independent operations. Run only the action you intend:
$powerOn = $api()->put('power', $payload + ['state' => 'on'])->throw()->json();
$restart = $api()->post('power/actions', $payload + [
'action' => 'cycle',
])->throw()->json();
$boot = $api()->put('boot-device', $payload + [
'device' => 'pxe',
'persistent' => true,
])->throw()->json();
$reset = $api()->post('mc/resets', $payload + ['mode' => 'warm'])->throw()->json();
$clearEvents = $api()->post('sel/clear', $payload)->throw()->json();Use commands for operations without a dedicated route, including OEM commands:
$result = $api()->post('commands', $payload + [
'args' => ['sensor', 'get', 'CPU Temp'],
])->throw()->json();
$deviceId = $api()->post('commands', $payload + [
'args' => ['raw', '0x06', '0x01'],
])->throw()->json();Pass the command and each argument separately, without ipmitool or global
connection flags. Arguments are passed literally, without a shell. Optional
stdin accepts a string. Command availability depends on the installed ipmitool
and BMC; see the ipmitool manual.
Native file paths refer to the API host. Interactive consoles requiring a TTY are
not supported by this synchronous API.
All command routes require connection in addition to the fields below.
| Method | Relative path | Additional fields |
|---|---|---|
| GET | health |
None; no connection required |
| POST | power/status, chassis/status |
None |
| POST | sensors, sdr, fru, sel/entries, mc/info |
None |
| POST | lan/{channel}, users/{channel} |
Channel from 0 to 15 |
| PUT | power |
state: on, off |
| POST | power/actions |
action: cycle, reset, diag, soft |
| PUT | boot-device |
device: none, pxe, disk, safe, diag, cdrom, bios, floppy; optional boolean persistent |
| POST | mc/resets |
mode: warm, cold |
| POST | sel/clear |
None; deletes event history |
| POST | commands |
args: 1–256 strings; optional stdin |
| Field | Rule |
|---|---|
host |
Required IP or hostname, without protocol or port |
username |
Required; up to 16 bytes, no control characters |
password |
Required; up to 20 bytes for lanplus or 16 for lan; explicit empty string allowed |
interface |
Optional: lanplus (default) or lan |
port |
Optional integer from 1 to 65535; default 623 |
privilege |
Optional: CALLBACK, USER, OPERATOR, ADMINISTRATOR, OEM, NO ACCESS; native + suffix accepted |
auth_type |
Optional: NONE, MD2, MD5, PASSWORD, OEM, RMCP+ |
cipher_suite |
Optional integer from 0 to 255 |
privilege and auth_type are case-insensitive. Unknown fields, invalid types,
and null values are rejected. Maximum JSON body: 128 KiB. Arguments total at most
64 KiB; stdin has a separate 64 KiB limit. Connection strings and arguments
cannot contain NUL bytes.
Successful command responses contain stdout, stderr, exit_code, and
truncated. Read the output through Laravel:
$response = $api()->post('sensors', $payload);
if ($response->failed()) {
$code = $response->json('error.code');
$message = $response->json('error.message');
$diagnostic = $response->json('result.stderr');
// Handle the error without exposing BMC diagnostics to untrusted users.
}
$output = $response->throw()->json('stdout');throw() raises RequestException for HTTP errors; connection failures/timeouts
raise ConnectionException. Common statuses: 400 invalid input, 401 invalid
token, 403 blocked IP, 502 ipmitool failure, 503 concurrency limit, 504
command timeout. Execution failures include partial output in result.
API error messages use lowercase except for acronyms such as IP and JSON.
Native stdout and stderr keep their original case.
Do not automatically retry operations that change BMC state.
The Dockerfile includes ipmitool and runs as UID/GID 10001.
Prepare a configured copy of config.yaml readable by that group:
docker build -t ipmitool-microservice .
sudo install -d -m 0750 /etc/ipmitool-microservice/docker
sudo install -m 0640 -o root -g 10001 config.yaml /etc/ipmitool-microservice/docker/config.yaml
docker run --detach --name ipmitool-api \
--publish 127.0.0.1:8080:8080 \
--mount type=bind,src=/etc/ipmitool-microservice/docker/config.yaml,dst=/etc/ipmitool-microservice/config.yaml,readonly \
ipmitool-microservice
docker logs --follow ipmitool-apiThe image listens on 0.0.0.0:8080 inside the container. If TLS is enabled, mount
the certificate/key read-only at their configured paths, readable by UID/GID
10001. Mount /var/lib/ipmitool-microservice for persistent native command files.
Preinstall ipmitool, then install the binary, configuration, and service unit:
sudo apt install ipmitool
go build -o ipmitool-microservice .
id -u ipmitool-api >/dev/null 2>&1 || \
sudo useradd --system --user-group --no-create-home \
--home-dir /var/lib/ipmitool-microservice --shell /usr/sbin/nologin ipmitool-api
sudo install -m 0755 ipmitool-microservice /usr/local/bin/ipmitool-microservice
sudo install -d -m 0750 -o root -g ipmitool-api /etc/ipmitool-microservice
sudo install -m 0640 -o root -g ipmitool-api config.yaml /etc/ipmitool-microservice/config.yaml
sudo install -m 0644 deploy/ipmitool-microservice.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now ipmitool-microserviceThe unit runs as ipmitool-api, writes state under /var/lib/ipmitool-microservice,
and prevents privilege elevation, including automatic apt installation. TLS files
must be readable by the service user. Use journalctl -u ipmitool-microservice -f
for logs and restart the service after configuration changes.
goimports -local ipmitool-microservice -w main.go internal
golangci-lint run ./...
go test -race ./...
go vet ./...
go build ./...Tests use simulated commands and temporary HTTP/HTTPS listeners. They cover validation, authentication, IP filtering, runner responses, logging, installation, and process lifecycle without requiring a real BMC.