Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

warpgate-man

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.

How it works

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 the prune: section (see Prune scope).

Where the desired state comes from

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.

Installation

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.

Configuration

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@laptop

Secrets are referenced through environment variables (${VAR}) and interpolated at load time. Never write secrets in clear text.

Prune scope

--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.

Tokens

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-config needs.
  • An admin token is additionally allowed on the admin API, so diff and apply need one. Start Warpgate with --enable-admin-token and set WARPGATE_ADMIN_TOKEN on 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.

CLI usage

Administering a server

# 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 apply

Sourcing the desired state from Odoo

Add 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:

  • targetsmaintenance.equipment records matching targets.domain. The ssh_target field, formatted [user@]HOST[:PORT] (defaulting to root and 22), gives host, port and backend account; the record name gives the target name; auth is publickey. SSH only — Odoo describes machines, not HTTP/database endpoints.
  • usersres.users records matching each users[].domain entry, one Warpgate user per Odoo login. A user matching several entries accumulates the roles of all of them.
  • SSH public keys — the ssh.key records 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.roles or users[].roles is 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.

Previewing with fetch

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 fetch
targets:
- 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@laptop

Note 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.

Connection details on the command line

--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.com

There 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.

Connecting to your targets

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-config
Host web-01
    HostName ssh.warpgate.example.com
    Port     2222
    User     alice:web-01

Then 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/warpgate

Use --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.

Authenticating with SSO

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.

Library usage

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 / deleted

You 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))

License

MIT.

About

Declarative manager for Warpgate bastion configuration (targets, groups, users, roles) from a YAML file.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages