Declarative manager for Warpgate
bastion configuration. Describe the desired state of your Warpgate servers —
targets, target-groups, users and roles — in a single YAML file, and let
warpgate-man reconcile each server to match.
It ships both as:
- a command-line tool (
warpgate-man), and - a Python library (
wgman) with a clean API, synchronous and dependency-light, suitable for calling from another program.
The desired state does not have to be hand-written: warpgate-man can also read it from an Odoo server, turning your inventory of machines and users into Warpgate targets, users and roles. See the Odoo section below.
Warpgate stores its targets, users and roles in a database, exposed through its
HTTP admin API (/@warpgate/admin/api). warpgate-man talks to that API
using an admin token and reconciles the live state with your desired state:
- create what is missing,
- update what differs,
- delete what is not declared — only when you pass
--prune, and even then only targets unless you widen the scope in theprune:section (see Prune scope).
Two sources, usable together:
- the YAML file itself (
target-groups:,targets:,roles:,users:), - an Odoo server, when the file has an
odoo:section (or you pass--odoo-url/--odoo-db/--odoo-user).
Odoo-sourced entities are merged into the file-defined ones. A name defined on both sides is an error, not a silent override.
uv pip install -e .To use Odoo as a source of desired state, install the odoo extra, which
pulls in the oerpc JSON-RPC client:
uv pip install -e '.[odoo]'Without it everything else still works; only the Odoo source raises, with a message telling you to install the extra.
See examples/wgman.yaml for a complete, annotated example. In short:
servers:
- name: prod
url: https://warpgate.example.com:8888
api-key: ${WG_PROD_TOKEN}
roles:
- name: admin
target-groups:
- name: databases
targets:
- name: pg-main
kind: ssh
host: 10.0.0.5
port: 22
username: postgres
auth: publickey
roles: [admin]
users:
- name: alice
roles: [admin]
public-keys:
- ssh-ed25519 AAAAC3Nz... alice@laptopSecrets are referenced through environment variables (${VAR}) and
interpolated at load time. Never write secrets in clear text.
--prune is the master switch; the optional prune: section says what it
may delete. Without that section, --prune deletes targets only — the safe
default, since a naive full prune would happily remove the admin user.
prune:
targets: true # default true
target-groups: false # default false
roles: false # default false
users: false # default false
keep-users: [admin] # never deleted, even when users are pruned
keep-roles: [admin]Each flag turns deletion on or off for one kind; each keep-* list protects
individual names even when that kind is pruned.
api-key is your Warpgate token, and what you may run follows from what
that token is:
- A personal API token (Warpgate web UI → your profile → API tokens)
identifies you as a user. It is what
ssh-configneeds. - An admin token is additionally allowed on the admin API, so
diffandapplyneed one. Start Warpgate with--enable-admin-tokenand setWARPGATE_ADMIN_TOKENon the server, or give an admin user a personal token.
Using a personal token with apply fails with a clear message rather than a
bare HTTP 403.
Note the server’s global admin token (--enable-admin-token) is a machine
credential bound to no user: Warpgate reports no username for it and grants
it no target, so ssh-config refuses it.
# Show the delta between the YAML and the live server (no writes)
warpgate-man --config wgman.yaml diff
# Apply: create + update (no deletions)
warpgate-man --config wgman.yaml apply
# Apply and delete server-side targets not present in the desired state
# (widen to other kinds with the 'prune:' section)
warpgate-man --config wgman.yaml apply --prune
# Target a single server from a multi-server config
warpgate-man --config wgman.yaml --server prod applyAdd an odoo: section and diff / apply will query that server and merge
what it finds into the desired state, alongside whatever the file declares.
odoo:
url: https://odoo.example.com
db: mydb
user: sync@example.com
password: ${ODOO_PASSWORD} # omit to be prompted interactively
verify-tls: true # optional, default true
# Which machines become SSH targets, and who may reach them.
targets:
domain: [["ssh_target", "!=", false]] # default
roles: [sysadmin]
# Which Odoo accounts become Warpgate users, and with which roles.
users:
- domain: [["groups_id", "in", [12]]]
roles: [sysadmin]
- domain: [["login", "=", "alice@example.com"]]
roles: [developers]What is read, and how it maps:
- targets —
maintenance.equipmentrecords matchingtargets.domain. Thessh_targetfield, formatted[user@]HOST[:PORT](defaulting torootand22), gives host, port and backend account; the recordnamegives the target name; auth ispublickey. SSH only — Odoo describes machines, not HTTP/database endpoints. - users —
res.usersrecords matching eachusers[].domainentry, one Warpgate user per Odoologin. A user matching several entries accumulates the roles of all of them. - SSH public keys — the
ssh.keyrecords belonging to those users, so their keys are pushed to the bastion. Keys pasted with hard line wraps are glued back together; anything that is not an OpenSSH public key is a loud error, never silently uploaded. - roles — every role named in
targets.rolesorusers[].rolesis auto-declared. Since access in Warpgate is “user and target share a role”, naming a role on both sides is all the wiring there is.
Selection is expressed as raw Odoo domains — the same lists of
[field, operator, value] triplets (with |, &, ! operators) you would
pass to search_read. They are forwarded verbatim, so any query Odoo can
express is available.
fetch queries Odoo and prints the result as YAML, in exactly the shape a
config file uses — so you can read it, diff it, or paste it into a file:
warpgate-man --config wgman.yaml fetchtargets:
- name: web-01
kind: ssh
host: 10.0.0.11
port: 22
username: root
auth: publickey
roles:
- sysadmin
- name: db-main
kind: ssh
host: 10.0.0.12
port: 2222
username: dba
auth: publickey
roles:
- sysadmin
roles:
- name: sysadmin
- name: developers
users:
- name: bob@example.com
roles:
- sysadmin
- name: alice@example.com
roles:
- developers
public-keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB1kWyQ2 alice@laptopNote db-main above: its ssh_target was dba@10.0.0.12:2222, which is
where the non-default username and port come from. Note also developers,
declared because a users: entry names it, though no target uses it yet.
It writes nothing, anywhere — neither to Warpgate nor to Odoo.
--odoo-url, --odoo-db and --odoo-user override the odoo: section, and
together are enough to run fetch with no config file at all:
warpgate-man fetch --odoo-url https://odoo.example.com \
--odoo-db mydb --odoo-user sync@example.comThere is deliberately no --odoo-password flag: a password on the command
line leaks into ps output and shell history. Put it in the config as
${ODOO_PASSWORD}, or leave it out and be prompted on the terminal.
Because fetch never contacts Warpgate, it tolerates unset ${ENV}
references elsewhere in the file — an unexported WG_PROD_TOKEN will not
stop you from listing what Odoo holds.
ssh-config asks Warpgate which targets your token can reach and prints
matching ssh_config(5) stanzas. It only reads, and needs no admin rights —
admins are users too, and get their own reachable targets like anyone else.
warpgate-man --config wgman.yaml ssh-configHost web-01
HostName ssh.warpgate.example.com
Port 2222
User alice:web-01Then ssh web-01 just works. The usual pattern is a dedicated file included
from your ssh config:
warpgate-man --config wgman.yaml ssh-config -o ~/.ssh/config.d/warpgate
# and once, in ~/.ssh/config: Include config.d/warpgateUse --prefix to keep Warpgate aliases apart from your own hosts (--prefix
wg- gives ssh wg-web-01). When several servers are queried, aliases are
prefixed with the server name automatically, since target names are only
unique within one server.
What the stanzas mean: the SSH client connects to the bastion, and the
target is selected through the username, encoded <user>:<target>. The
backend’s own account never appears — Warpgate dials it with its stored
credentials. Likewise known_hosts pins the bastion, not each backend, so
nothing here weakens host-key checking.
Only SSH targets are emitted; HTTP, MySQL, Postgres and Kubernetes targets are not reachable through an ssh client.
Warpgate’s SSO is a browser redirect flow with no headless (device-code or
CLI callback) variant, so warpgate-man cannot perform an SSO login itself.
If your Warpgate uses SSO, log in through the web UI as usual, mint a
personal API token there, and use it as api-key.
from wgman import WarpgateManager, Target, Role, User
mgr = WarpgateManager(url="https://warpgate.example.com:8888",
api_key="...")
plan = mgr.reconcile(
roles=[Role(name="admin"), Role(name="dba")],
targets=[
Target(name="pg-main", kind="ssh", host="10.0.0.5",
port=22, username="postgres", auth="publickey",
roles=["admin", "dba"]),
],
users=[User(name="alice", roles=["admin", "dba"])],
prune=False,
)
print(plan) # what was created / updated / deletedYou can also load a YAML file directly:
from wgman import load_config, WarpgateManager
config = load_config("wgman.yaml")
server = config.server("prod")
mgr = WarpgateManager.from_server(server)
mgr.reconcile_config(config, prune=False)The user side is available too — WarpgateUserClient reads what one user may
reach, and wgman.sshconfig renders it:
from wgman import WarpgateUserClient, sshconfig
from wgman.sshconfig import BastionInfo
with WarpgateUserClient("https://warpgate.example.com:8888", "...") as client:
info = BastionInfo.from_info(client.get_info(),
url="https://warpgate.example.com:8888")
print(sshconfig.render(client.list_targets(), info))MIT.