Shipped controls
- Commands run with plain arguments, never through a shell.
- Raw commands are off unless you enable them, and every raw call waits for approval.
- Destructive actions are held for approval by default.
- Every agent gets its own key. One switch disables an agent and its keys.
- Commands run as a dedicated non-root user (run_as). Running commands as root is refused. The connector process must run as root to switch to that user. The published image runs as uid 10001 and does not switch users.
- The connector is outbound-only.
- The audit log is hash-chained, with offline verify.
- Optional rate limits, set per agent or caller.
Architecture
The bastn gateway is one Go binary with Postgres, self-hosted in your network. Agents connect to the gateway at its single MCP endpoint, /mcp, with their own virtual key or an OAuth sign-in. On each host, the bastn connector dials out to the gateway's connector hub over mutual TLS and runs the actions it is allowed to run. Calls, results, credentials and audit logs stay in your network.
Where limits are enforced
Every call passes these checks in the gateway, in this order: identity, tool policy, argument rules, rate limit, new and changed tool checks, approval, the call, result scan, then audit. On the host, the connector applies its own limits before anything runs.
The connector
- Outbound only. Mutual TLS to the gateway. No inbound port, no VPN. A revoked connector is refused at the TLS handshake. Certificates renew themselves.
- Allowlist on the host. Programs listed in the connector config can run, each at an absolute path, with a maximum effect (read, write or destructive) and blocked flags.
- Commands run as a dedicated non-root user (run_as). Running commands as root is refused. The connector process must run as root to switch to that user. The published image runs as uid 10001 and does not switch users.
- No shell by default. Raw commands are off unless you enable them, and every raw call waits for approval. Programs run with plain arguments, timeouts and output caps.
- Credentials stay on the connector. Upstream credentials are read from the connector's config, used per call and scrubbed from results.
- Reviewed scripts. Agent-written scripts run after human approval. Approval pins the hash and the connector checks it before each run.
Policy and approvals
- Rules per agent, per tool and per argument value, including which hosts by label.
- Destructive actions are held for approval by default. New tools can be denied until you review them (new_tools: deny).
- Set rate limits per agent or caller.
- Every agent gets its own key. One switch disables an agent and its keys.
- Policy is a file you validate before you apply it.
The record, captured
The action, as written
Excerpt from the read-only Linux pack, packs/linux/ip.yaml.
spec: bastn.command/v1
name: ip
binary: ip
tools:
- name: addr_show
effect: read
description: List the addresses of all interfaces, or of one, as JSON
fixed: ["-j"]
argv: ["{fixed}", "{family?}", "addr", "show", "{dev?}"]
args:
family: { type: string, enum: ["-4", "-6"], allow_dash: true }
dev: { type: string, pattern: '^[A-Za-z0-9_][A-Za-z0-9_.@-]{0,14}$' }Every argument has a type and a pattern. A value that does not match never reaches the host.
The host's own limits
Excerpt from a connector config.
capabilities:
- name: commands
kind: commands
run_as: bastn-cmd # never root
timeout_max: 60s
profiles: [linux-readonly]
binaries:
- { name: ip, path: /usr/sbin/ip, max_effect: destructive, deny_flags: ["netns", "-force", "-batch"] }
- { name: deployctl, path: /opt/acme/bin/deployctl, max_effect: write }The machine owner decides which programs may run, how much they may change and as which user. The connector enforces that locally.
A policy and its verdicts
captured from a bastn run
policy: bastn.policy/v1
teams:
ops:
servers:
commands:
commands: {}
allow_tools: [systemctl_status, journalctl_unit, svc_restart]
new_tools: deny
arg_rules:
- tool: svc_restart
args: [{ name: unit, enum: [nginx, php-fpm] }]
rate_limits:
- { tool: "*", limit: 30, window: 1m, per: caller }
approvals:
restart:
server: commands
tool_glob: svc_restart
target: { labels: { role: web } }
categories:
web:
target: { labels: { role: web } }
tools: [commands__systemctl_status, commands__journalctl_unit, commands__svc_restart]
agents:
remediation-agent: { category: web }deny: tool not permitted by gateway policy: write_file
hold: approval required: call is held for human approval (approval id 1)svc_restart is a reviewed custom spec; the shipped Linux pack is read-only.
Verifying the audit log
Every decision is sealed into a keyed hash chain (HMAC-SHA256 under a key derived from your BASTN_SECRET_KEY). The verify lines below are captured from a bastn run.
bastn audit export -config config.yaml > audit.jsonl
BASTN_SECRET_KEY=... bastn audit verify --file audit.jsonl
bastn audit verify --anchor <seq>:<tip>captured from a bastn run
ok: chain global intact, 29 rows checked, length 29, head 55597adc948f…
BROKEN: chain global, audit chain break at seq 5: hash_mismatch …- An edited, re-ordered or deleted row in the middle of the chain is caught.
- Deleting the newest rows leaves a valid, shorter chain. To catch that, ship the periodic audit.anchor log lines to storage the database's writers cannot rewrite, and verify against them.
- Without the key, verification checks structure and says so.
- Anyone holding the key can re-seal rows. The key is the trust root. Protect it like a signing key.
- A team-scoped export checks each row. It cannot prove that no rows are missing. A full admin export verifies the whole chain.
Report a vulnerability
To report a vulnerability, email comms@dflux.io. Please include what you tried and how to reproduce it. We will reply to confirm we received the report.