Go binary installed on servers that use grantssh. It authenticates with the grantssh app, reports version and last-seen, enforces SSH access, reports SSH login events, and monitors sudo command usage.
Supported platforms: Linux amd64/arm64 and FreeBSD amd64/arm64. Tested on Debian, Ubuntu, Fedora, CentOS/RHEL, Rocky Linux, AlmaLinux, Amazon Linux, openSUSE, Gentoo, Arch, and Alpine. See docs/INSTALL.md for installation as a systemd service.
Prerequisites: Go 1.21+ and golangci-lint (e.g. brew install golangci-lint). For make fmt you also need goimports: go install golang.org/x/tools/cmd/goimports@latest.
- Test config:
./bin/grantssh-agent --test(validates config, tests connectivity, checks event source, exits) - Build:
make buildorgo build -o bin/grantssh-agent ./cmd/grantssh-agent - Build all platforms:
make build-all(Linux + FreeBSD, amd64 + arm64) - Build dist:
make dist(builds all + checksums indist/) - Run:
make runor./bin/grantssh-agent - Test:
make testorgo test ./... - Lint:
make lint - Format:
make fmt(apply),make fmt-check(check only),make check(fmt-check + lint + test; run before committing)
See AGENTS.md for agent-focused setup, commands, and code style. See docs/INSTALL.md for install script, systemd unit, and configuration.
GitHub Actions workflows in .github/workflows/:
- CI (
ci.yml): Runs on every push to main and on PRs. Runsmake fmt-check,make lint,go test -race ./..., then builds all 4 platform/arch binaries and uploads them as artifacts. - Release (
release.yml): Triggers on tag push (v*). Runs the full check suite, builds all binaries with the tag as the version, and creates a GitHub Release with the binaries and achecksums.txt.
git tag v1.0.0
git push origin v1.0.0This triggers the release workflow which builds, tests, and publishes binaries to a GitHub Release automatically. The release includes:
| Binary | OS | Arch |
|---|---|---|
grantssh-agent-linux-amd64 |
Linux | x86_64 |
grantssh-agent-linux-arm64 |
Linux | ARM64/aarch64 |
grantssh-agent-freebsd-amd64 |
FreeBSD | x86_64 |
grantssh-agent-freebsd-arm64 |
FreeBSD | ARM64 |
grantssh-agent-amd64 |
Linux | x86_64 (legacy name for install script) |
grantssh-agent-arm64 |
Linux | ARM64 (legacy name for install script) |
checksums.txt |
SHA256 checksums |
All binaries are statically linked and work across all supported distros for their OS — no distro-specific builds needed.
The POST /api/agent/events request body includes a schema_version field so the app can handle payloads from old and new agents gracefully. The current version is 2.
{
"schema_version": 2,
"events": [...]
}| Version | Changes |
|---|---|
| 1 | Initial: ssh_login events with server_account, fingerprint_sha256, remote_ip, remote_port, auth_method |
| 2 | Added event_type field. New event types: ssh_disconnect, sudo, failed_login_summary. New fields: target_user, command, working_dir, tty, attempt_count, first_seen, last_seen |
When adding new fields or event types, bump EventSchemaVersion in internal/client/client.go.
The agent exposes a local HTTP endpoint for monitoring tools (Nagios, Prometheus, Datadog, etc.).
| Endpoint | Response |
|---|---|
GET /healthz |
200 ok if the agent is running |
GET /status |
JSON with version, uptime, last heartbeat/sync times, event counters |
Configuration:
| Setting | Default | Description |
|---|---|---|
health_listen (config) |
127.0.0.1:19099 |
Listen address. Set to "" to disable. |
GRANTSSH_HEALTH_LISTEN (env) |
127.0.0.1:19099 |
Same. Set to off to disable. |
The default binds to localhost only — monitoring tools on the same machine can reach it, but it's not accessible from the network. To expose it (e.g. for a remote Prometheus scrape), set health_listen to 0.0.0.0:19099 or a specific interface.
To disable entirely: set "health_listen": "" in config.json or GRANTSSH_HEALTH_LISTEN=off in the environment.
Periodic stats logging (optional, off by default): Set "stats_log_interval_sec": 300 in config or GRANTSSH_STATS_LOG_INTERVAL_SEC=300 to log a stats summary every 5 minutes. SIGHUP-reloadable — you can enable/disable without restarting. Example output:
level=INFO msg="agent stats" uptime=4h32m events_sent=142 events_queued=0 failed_logins_flushed=3 last_heartbeat_ago=58s last_sync_ago=12s
Example /status response:
{
"status": "running",
"version": "v1.2.0",
"uptime": "4h32m10s",
"uptime_seconds": 16330,
"last_heartbeat": "2026-03-07T14:30:00Z",
"last_sync": "2026-03-07T14:30:05Z",
"events_sent": 142,
"events_queued": 0,
"failed_logins_flushed": 3
}The project has three layers of testing: unit tests, a mock API server for local development, and VM-based integration tests for real end-to-end verification.
make test # or: go test ./...
make check # fmt-check + lint + test (CI parity — run before committing)Tests live alongside the code in *_test.go files. Packages with tests:
| Package | What it covers |
|---|---|
internal/authlog |
SSH login line parsing (publickey, password, keyboard-interactive/pam), syslog prefix stripping, journal detection |
internal/sudolog |
Sudo command parsing across all distro log formats, session tracker for fingerprint linking |
internal/distro |
OS release detection (Ubuntu, Fedora, openSUSE, SLES, Gentoo, Calculate Linux) |
internal/authkeys |
authorized_keys file parsing, managed block read/write, key validation |
internal/client |
API client request/response handling |
internal/config |
Config loading from file and environment |
internal/creds |
Credential persistence |
internal/events |
Event batching, queue persistence, retry |
internal/schedule |
Time-based grant schedule evaluation |
internal/sync |
Sync loop desired-state building |
internal/updater |
Self-update version comparison |
internal/version |
Version string handling |
The mock API (test/mockapi) is a standalone Go program that mimics the grantssh app API. It accepts heartbeat, sync, and event requests, logs everything to stdout, and writes event payloads to a JSON-lines file for inspection.
# Build
go build -o bin/mockapi ./test/mockapi/
# Run (serves empty sync by default)
./bin/mockapi -port 8199 -events-log /tmp/events.jsonl
# Run with a custom sync response (e.g. to push SSH keys to the agent)
./bin/mockapi -port 8199 -events-log /tmp/events.jsonl -sync-response sync.jsonThen point the agent at it:
GRANTSSH_APP_URL=http://localhost:8199 \
GRANTSSH_AGENT_ID=test \
GRANTSSH_AGENT_TOKEN=test \
./bin/grantssh-agentThe -events-log file is JSON-lines — one line per event with metadata (agent_id, received_at, event). You can inspect it to verify exactly what the agent sent:
# Show all events
cat /tmp/events.jsonl | python3 -m json.tool
# Show only sudo events
grep '"event_type":"sudo"' /tmp/events.jsonlFor full end-to-end testing on real Linux distributions, the project includes a QEMU-based integration test runner. This catches issues that unit tests cannot — distro-specific journal behaviour, auth log paths, SSH authentication method differences, and sudo log format variations.
Prerequisites: qemu-system-x86_64, qemu-img, cloud-localds (or genisoimage), sshpass, curl. On Ubuntu/Debian: sudo apt install qemu-system-x86 qemu-utils cloud-image-utils sshpass.
# Build agent and mock API first
make build
go build -o bin/mockapi ./test/mockapi/
# Run against a specific distro
./test/integration/run.sh --distro ubuntu
./test/integration/run.sh --distro opensuse
./test/integration/run.sh --distro debianCloud images are downloaded on first run and cached in test/integration/images/. Without KVM, QEMU uses software emulation (~2 min boot); with KVM it is nearly instant.
The test runner:
- Boots a QEMU VM from a cloud image with cloud-init (sshd + test user)
- Starts the mock API on the host
- Copies the agent binary into the VM and starts it
- Triggers a real SSH login and sudo commands
- Verifies the mock API received correct event payloads (login + sudo with fingerprint)
See test/integration/README.md for full details, environment variables, and supported distros.
| Distro | SSH auth method | Sudo detection | Journal/file | Auth log path |
|---|---|---|---|---|
| Ubuntu 24.04 | password |
✅ | journalctl | /var/log/auth.log |
| Debian 12 | password |
✅ | journalctl | /var/log/auth.log |
| Fedora | password |
✅ | journalctl | /var/log/secure |
| Rocky Linux 9 | publickey |
✅ | journalctl | /var/log/secure |
| AlmaLinux 9 | publickey |
✅ | journalctl | /var/log/secure |
| Amazon Linux 2023 | publickey |
✅ | journalctl | /var/log/secure |
| openSUSE Leap 15.6 | keyboard-interactive/pam |
✅ | journalctl | /var/log/messages |
| Arch Linux | publickey |
✅ | journalctl | (journal only) |
| Alpine 3.20 | publickey |
✅ | file tail | /var/log/messages |
| Gentoo | publickey |
✅ | file tail | /var/log/messages |
| FreeBSD 14 | publickey |
✅ | file tail | /var/log/auth.log |
Notes:
- Alpine uses musl libc; the agent must be built with
CGO_ENABLED=0(the defaultmake builddoes this). - Alpine and Gentoo with OpenRC have no journald; the agent automatically falls back to file-based log tailing.
- Arch Linux uses journald exclusively by default (no auth log file); the agent auto-detects journal.
- FreeBSD builds are cross-compiled with
GOOS=freebsd. FreeBSD 13+ has/etc/os-releasefor distro detection.