Skip to content

About

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

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

13 Commits

Folders and files

Repository files navigation

warpgate-man

Keep a Warpgate bastion in sync with an Odoo database — and let its users get their ssh config from the same place.

Two people use this tool, and this document is written for both:

  • the manager who runs the bastion and wants Odoo, not hand-edited YAML, to be the source of truth for which machines exist and who may reach them — see For the manager;
  • the Odoo user who just wants ssh some-machine to work, without ever touching Warpgate — see For the Odoo user.

Odoo is optional: warpgate-man is at bottom a declarative manager that reconciles one or more Warpgate servers to a YAML file, and the Odoo source is one way of filling that file in. The Reference section covers the general form.

Installation

warpgate-man is a command-line tool — install it as one. It and its oerpc dependency are fetched from git, so git must be available.

uv tool install 'warpgate-man[odoo] @ git+https://github.com/0k/warpgate-man.git'

That puts warpgate-man in ~/.local/bin, in an isolated environment of its own: no virtualenv to create or activate. (uv pip install is the wrong verb here — it installs into the virtualenv you are currently in, and refuses to guess one.)

From nothing at all, on a bare Debian-based image:

apt-get update && apt-get install -y curl git
curl -LsSf https://astral.sh/uv/install.sh | sh
. "$HOME/.local/bin/env"          # puts uv, and ~/.local/bin, on PATH
uv tool install 'warpgate-man[odoo] @ git+https://github.com/0k/warpgate-man.git'

The env line matters: uv’s installer does not touch the PATH of the shell that ran it, so uv is otherwise not found until you open a new one.

To run it once without installing anything:

uvx --from 'warpgate-man[odoo] @ git+https://github.com/0k/warpgate-man.git' \
    warpgate-man --version

Without uv, pip takes the same requirement string:

pip install 'warpgate-man[odoo] @ git+https://github.com/0k/warpgate-man.git'

The odoo extra pulls in the =oerpc= JSON-RPC client. Both personas need it: the manager to read Odoo, the user to authenticate to it. Without the extra everything else works, and any Odoo command tells you to install it.

From a checkout:

uv sync --extra odoo          # add --extra dev for the test suite
uv run warpgate-man --version

For the manager: syncing Warpgate from Odoo

You run a Warpgate bastion in front of a fleet of machines, and Odoo already knows those machines and the people who work on them. Rather than repeating that knowledge in Warpgate by hand, warpgate-man reads it from Odoo and reconciles the bastion to match: creating what is missing, updating what differs, and — only when you ask — deleting what is gone.

What Odoo must hold

Three things are read. The first two come with the maintenance_server_data addon; the third is what makes access work at all, and it is the one your users have to supply themselves.

  • Machines are maintenance.equipment records whose ssh_target field is set. That field is the address the bastion dials, in the form [user@]HOST[:PORT] — root@ceres.example.com:22, defaulting to root and 22. The record’s name becomes the target name your users will type after ssh, so keep it short and stable.
  • People are res.users. A person’s Odoo login becomes their Warpgate username, verbatim. Which accounts become bastion users is up to your selection (below).
  • SSH public keys are ssh.key records attached to a user (user_id + key). Every user must have added their public key to their Odoo profile before they can log in through the bastion. Warpgate authenticates by public key: a user with no key in Odoo gets an account on the bastion but cannot open a session with it.

    Keys pasted with hard line-wraps are repaired at read time. Anything that is not an OpenSSH public key stops the run with a loud error naming the record, rather than being uploaded as a credential nobody can use.

What Warpgate must provide

An admin token to write with. Start Warpgate with --enable-admin-token and set WARPGATE_ADMIN_TOKEN on the server, or mint a personal API token for an admin user in the web UI. Either is passed as api-key below and never written in clear text — reference an environment variable.

The configuration

One file, say wgman.yaml:

servers:
  - name: prod
    url: https://warpgate.example.com:8888
    api-key: ${WG_PROD_TOKEN}
    ssh-host: warpgate.example.com     # what your users' ssh will dial
    ssh-port: 2222                     # optional, default 2222

odoo:
  url: https://odoo.example.com
  db: mydb                             # optional; see below
  user: sync@example.com
  password: ${ODOO_PASSWORD}           # omit to be prompted interactively

  # Which machines become targets, and which role reaches them.
  targets:
    domain: [["ssh_target", "!=", false]]   # the default
    roles: [sysadmin]

  # Which Odoo accounts become bastion users, and with which roles.
  users:
    - domain: [["groups_id", "in", [12]]]     # e.g. an Odoo "sysadmins" group
      roles: [sysadmin]
    - domain: [["login", "=", "alice@example.com"]]
      roles: [developers]

Access in Warpgate is a user and a target share a role. Naming the same role on a targets: entry and a users: entry is the entire wiring; every role mentioned is declared for you. Leave roles out everywhere and no access is granted — every user is created, every machine is created, and nobody reaches anything, which is rarely what you want.

ssh-host / ssh-port are not needed to sync. They exist so your users can build their ssh config from this same file without knowing the bastion’s address by heart.

Selections are raw Odoo domains — the [field, operator, value] triplets (with |, &, !) you would give search_read, forwarded verbatim. Anything Odoo can express is available.

The commands

Preview what Odoo yields, without touching anything:

warpgate-man --config wgman.yaml fetch

It prints the targets, users, keys and roles as YAML in exactly the shape of a config file, so you can read it or diff it against a previous run. Because it never contacts Warpgate, an unset ${WG_PROD_TOKEN} does not stop it.

See what would change on the bastion:

warpgate-man --config wgman.yaml diff

Apply it:

warpgate-man --config wgman.yaml apply

apply creates and updates. It deletes nothing unless you say so:

warpgate-man --config wgman.yaml apply --prune

which removes targets no longer in Odoo — and only targets, by default. Users, roles and groups stay put, because a selection that happens to match nobody must not wipe the admin account. Widen this deliberately in the prune section.

Keys are synchronised strictly, in one direction: a key removed from Odoo is removed from the bastion. A user with no key in Odoo is left untouched, so an incomplete Odoo does not lock anyone out.

There is deliberately no --odoo-password flag: a password on the command line leaks into ps and shell history. Use ${ODOO_PASSWORD} in the file, or leave the key out and type it at the prompt.

For the Odoo user: setting up your ssh config

You have an Odoo account, and someone has told you the machines you work on now sit behind a bastion. You need two things: your key on file, and an ssh config. You never need to log into Warpgate itself.

1. Put your public key in Odoo

In your Odoo profile, add your SSH public key (the contents of ~/.ssh/id_ed25519.pub or similar) as an ssh.key record. The manager’s next sync pushes it to the bastion. Without it, the bastion knows your name but has no way to let you in.

2. Generate your ssh config

Install warpgate-man (see Installation), then:

warpgate-man --odoo-url https://odoo.example.com --odoo-db mydb \
             --odoo-user you@example.com \
             ssh-config --from-odoo --bastion warpgate.example.com:2222 \
             -o ~/.ssh/config.d/warpgate

It asks for your Odoo password, and writes something like:

# warpgate targets for you@example.com via warpgate.example.com:2222
# generated from https://odoo.example.com (desired state) — run 'apply' first if the bastion is not up to date
Host ceres-prod
    HostName warpgate.example.com
    Port     2222
    User     you@example.com:ceres-prod

Host web-01
    HostName warpgate.example.com
    Port     2222
    User     you@example.com:web-01

Then, once, in ~/.ssh/config:

Include config.d/warpgate

and ssh ceres-prod works. Re-run the command whenever the fleet changes.

If your manager gave you a wgman.yaml with the odoo: and ssh-host details filled in, the whole thing shortens to:

warpgate-man --config wgman.yaml ssh-config --from-odoo -o ~/.ssh/config.d/warpgate

The file’s api-key is not read on this path, so you need no Warpgate token and no ${WG_PROD_TOKEN} in your environment — that is the point. The key may be left out of the file altogether: a server entry needs only name, url and ssh-host to serve this command.

If you only need it once

Generating an ssh config is a once-in-a-while errand, so you do not have to install anything for it. uvx fetches warpgate-man, runs it, and keeps nothing:

uvx --from 'warpgate-man[odoo] @ git+https://github.com/0k/warpgate-man.git' \
    warpgate-man --odoo-url https://odoo.example.com --odoo-db mydb \
                 --odoo-user you@example.com \
                 ssh-config --from-odoo --bastion warpgate.example.com:2222 \
                 -o ~/.ssh/config.d/warpgate

It prompts for your Odoo password and writes the same file as above. All you need on the machine is uv and git (see Installation) — no virtualenv, no config file, and no Warpgate token.

The three --odoo-* flags are what replace the config file here, so give all three; with only some of them warpgate-man looks for a wgman.yaml and stops when there is none.

The scripted way

odoo-get-ssh-config does all of the above on a machine that has nothing installed: it gets uv if it is missing, asks once for your Odoo server and your bastion, and prints the ssh config.

Installing it

curl -LsSfO https://raw.githubusercontent.com/0k/warpgate-man/master/scripts/odoo-get-ssh-config
chmod +x odoo-get-ssh-config
mv odoo-get-ssh-config ~/.local/bin/

Using it

odoo-get-ssh-config > ~/.ssh/config.d/warpgate

It asks what it needs on the first run and remembers it, so later runs ask only for your Odoo password. Only the ssh config goes to stdout, so that redirection yields exactly the file ssh wants. Run --help for the options.

What is going on

Your ssh client only ever talks to the bastion. The machine you want is named inside the User field, as <your-login>:<target>; the bastion reads it, checks that one of your roles grants that target, and dials the machine itself with its own credentials. So:

  • your Odoo login is your bastion identity — nothing to configure;
  • the machines’ real addresses never appear in your config, and you never need to know them;
  • known_hosts pins the bastion, not each machine, and nothing here weakens that check.

Which targets you get is the rule the bastion applies: those sharing a role with you. It is computed from what the manager’s configuration declares, so it describes what the bastion looks like after their last apply. A machine added to Odoo this morning appears in your config but does not answer until the sync has run. And if that configuration declares no roles at all, it is not describing access, so every machine is listed — whether each one answers is then up to how the bastion was set up.

Use --prefix wg- if a target name collides with a host you already have (ssh wg-web-01).

Reference

Everything above is one way to use warpgate-man. Underneath, it is a declarative manager for any Warpgate server, Odoo or not.

How it works

Warpgate keeps targets, users and roles in its database, reachable through its HTTP admin API (/@warpgate/admin/api). warpgate-man reads your desired state, reads the live state, and issues the creates, updates and (with --prune) deletes that make them match. Matching is by name; Warpgate’s own ids are never in your file.

The desired state has two sources, usable together: the YAML file’s own target-groups: / targets: / roles: / users: sections, and the Odoo server named in odoo:. A name defined on both sides is an error, not a silent override.

Full YAML form

See examples/wgman.yaml for a complete annotated file. In short:

servers:
  - name: prod
    url: https://warpgate.example.com:8888
    api-key: ${WG_PROD_TOKEN}
    verify-tls: true                # optional, default true
    ssh-host: warpgate.example.com  # optional, for ssh-config --from-odoo
    ssh-port: 2222                  # optional, default 2222

roles:
  - name: admin
  - name: developers
    default: true                   # auto-assigned to every user

target-groups:                      # purely organisational (UI folders)
  - name: databases
    color: info
    targets:
      - name: pg-main
        kind: ssh                   # ssh | http | mysql | postgres | kubernetes
        host: 10.0.0.5
        port: 22
        username: postgres
        auth: publickey             # publickey | password | iam_role
        roles: [admin]

targets:                            # ungrouped
  - name: app-frontend
    kind: http
    url: http://10.0.1.10:8080
    roles: [developers]

users:
  - name: alice
    roles: [admin]
    public-keys:
      - ssh-ed25519 AAAAC3Nz... alice@laptop

Secrets go through ${VAR} and are interpolated from the environment at load time. Never write one in clear text.

Odoo mapping, in full

OdooSelectionBecomes
maintenance.equipmenttargets.domainSSH target; ssh_target gives host/port/account, name the target name
res.userseach users[].domainone user per login; roles accumulate across entries
ssh.keythose users’ keysthe user’s public-key credentials
roles named above—declared automatically

Only SSH targets come from Odoo: it describes machines, not HTTP or database endpoints. Add those in the YAML.

--odoo-url, --odoo-db and --odoo-user override the odoo: section. --odoo-url and --odoo-user are enough to run fetch or ssh-config --from-odoo with no config file at all.

The database is optional

db may be left out, in the config and on the command line alike. Odoo publishes its database list on the same unauthenticated endpoint its own login page uses, so warpgate-man asks before logging in:

  • exactly one database — it is used, and nothing is asked;
  • several — an error listing them, because picking one would silently work against the wrong data;
  • listing disabled (list_db = False, common hardening) — an error saying to name it explicitly.

Declaring db skips the lookup entirely, so it also serves as the answer to either error.

Prune scope

--prune is the master switch; the optional prune: section says what it may delete. Without it, only targets are pruned.

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 it is:

  • an admin token (--enable-admin-token + WARPGATE_ADMIN_TOKEN on the server, or a personal token of an admin user) is needed by diff and apply;
  • a personal API token (web UI → your profile → API tokens) identifies you as a user, and is what the plain ssh-config needs;
  • ssh-config --from-odoo needs no Warpgate token at all.

Accordingly api-key is optional in the file: it is what authenticates toward the bastion, not what identifies a server, so a config written for an end user may declare a server by name, url and ssh-host alone. A command that does authenticate then refuses with a message naming the server and how to fix it, rather than sending an empty token.

A personal token used with apply fails with a message saying what it lacks, not a bare HTTP 403. The server’s global admin token is bound to no user: plain ssh-config refuses it rather than emitting an empty file.

ssh-config without Odoo

If you hold a personal Warpgate token, the bastion can answer directly which targets you reach:

warpgate-man --config wgman.yaml ssh-config -o ~/.ssh/config.d/warpgate

This reads the live state — exactly what Warpgate grants right now — and takes the bastion address from the server itself, honouring reverse-proxy and NAT settings. When several servers are configured, aliases are prefixed with the server name, since target names are only unique within one server.

The two forms emit the same stanzas; they differ only in who they ask and what they need to ask it.

Warpgate’s SSO is a browser-redirect flow with no headless variant, so warpgate-man cannot log in through SSO itself. SSO users either mint a personal token in the web UI, or — simpler — use --from-odoo, which never touches Warpgate.

Library usage

Everything the CLI does is available from Python, synchronous and dependency-light, for calling from another program (Odoo included).

from wgman import WarpgateManager, Target, Role, User

mgr = WarpgateManager(url="https://warpgate.example.com:8888",
                      api_key="...")

plan = mgr.reconcile(
    roles=[Role(name="admin")],
    targets=[Target(name="pg-main", kind="ssh", host="10.0.0.5",
                    username="postgres", auth="publickey",
                    roles=["admin"])],
    users=[User(name="alice", roles=["admin"])],
    prune=False,
)
print(plan)  # what was created / updated / deleted

Or from a YAML file:

from wgman import load_config, WarpgateManager

config = load_config("wgman.yaml")
mgr = WarpgateManager.from_server(config.server("prod"))
mgr.reconcile_config(config, prune=False)

The user side too. From the live bastion, with a personal token:

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

Or from the desired state, with no bastion credential — the same rule --from-odoo applies, as wgman.access:

from wgman import access, sshconfig
from wgman.sshconfig import BastionInfo

me = next(u for u in config.users if u.name == "alice@example.com")
mine = access.reachable_targets(
    me, access.ssh_targets(config.all_targets()), config.roles)
info = BastionInfo.declared(username=me.name,
                            host="warpgate.example.com", port=2222)
print(sshconfig.render([access.to_render_payload(t) for t in mine], 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