Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,54 @@ For `spice` and `proxmox` entries, the `spice_*` and `proxmox_*` fields listed u

### `PUT /api/addressbook/folders/:scope/:folder/entries/:entry` (admin)

Update a connection entry. Uses read-modify-write: reads existing entry from Vault, merges incoming fields on top. Credentials (`password`, `private_key`) that are omitted from the request are preserved from the existing entry. Jump host credentials are merged per-hop by index.
Update a connection entry, or create it if it does not exist. Uses read-modify-write: reads existing entry from Vault, merges incoming fields on top. Credentials (`password`, `private_key`) that are omitted from the request are preserved from the existing entry. Jump host credentials are merged per-hop by index.

**`?move=true`** makes this folder the entry's only home in the scope: after the write, same-named entries in every other folder of the scope (subfolders included) are deleted, each with its own `delete_entry` audit record. For upserts keyed on a stable name that can change folder, such as a NetBox webhook writing to the device's current tenant folder: when the tenant changes, the entry follows instead of being doubled. The response lists what was removed:

```json
{ "ok": true, "removed_from": ["mrgelato"] }
```

### `GET /api/addressbook/folders/:scope/:folder/defaults` (admin)

The folder's default credentials, secrets stripped. These are lent to entries in the folder (and in subfolders without their own, per session type) that have no password or private key of their own, so an entry created without secrets by a webhook or an import is connectable the moment it exists. An entry's own credentials always win, and an entry set to `prompt_credentials` still prompts.

```json
{
"defaults": {
"ssh": { "username": "root", "domain": null, "has_password": false, "has_private_key": true },
"web": { "username": null, "domain": null, "has_password": true, "has_private_key": false }
}
}
```

Entries covered by folder defaults list with `has_credentials: true` and `inherited_credentials: true`.

### `PUT /api/addressbook/folders/:scope/:folder/defaults` (admin)

Set default credentials per session type. Types not mentioned keep what is stored; `null` removes a type. Within a type an omitted field keeps the stored value and `""` clears it, the same rule entry PUTs use for secrets.

```json
{
"ssh": { "username": "root", "private_key": "-----BEGIN OPENSSH PRIVATE KEY-----\n..." },
"web": { "password": "router-password" },
"rdp": null
}
```

Returns the same summary as `GET`. A type left with nothing in it is dropped; a folder left with no types loses its defaults entirely. Audited as `update_folder_defaults`.

### `DELETE /api/addressbook/folders/:scope/:folder/defaults` (admin)

Remove the folder's default credentials. Returns `204`.

### `GET /api/addressbook/version`

A counter rustguac bumps after every successful change made through its API to the address book (entries, folders, folder config and defaults, moves, copies, bulk operations) or to the caller's credential variables. Requires **operator**. It costs no Vault read, so clients can poll it often and reload only when it moves; the Connections page does so every 3 seconds. `changed_at` is unix milliseconds, `0` before the first change since start. The counter is per process: with several rustguac instances on one Vault, each sees only its own changes, so the page also refreshes unconditionally every two minutes.

```json
{ "version": 17, "changed_at": 1760155200000 }
```

### `DELETE /api/addressbook/folders/:scope/:folder/entries/:entry` (admin)

Expand Down
19 changes: 17 additions & 2 deletions docs/netbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,8 @@ Use Event Rule **conditions** to sync only the devices you want. You can filter

2. **Create the Webhook**:
- Name: `rustguac-sync-create`
- URL: `https://console.example.com/api/addressbook/folders/shared/netbox-sync/entries`
- HTTP method: POST
- URL: `https://console.example.com/api/addressbook/folders/shared/netbox-sync/entries/{{ data.name | lower }}?move=true`
- HTTP method: PUT
- HTTP content type: `application/json`
- Additional headers:
```
Expand All @@ -161,6 +161,21 @@ Use Event Rule **conditions** to sync only the devices you want. You can filter

**Important:** The entry field is `type`, not `session_type` (it matches the Vault storage format). The hostname uses `.split('/')[0]` to strip the CIDR prefix from NetBox IP addresses (e.g. `10.0.0.1/24` → `10.0.0.1`). Avoid `regex_replace` and `cut` filters — they are not available in NetBox's Jinja2 environment.

`PUT` creates the entry on first sight and updates it afterwards, keeping any password or key already stored in Vault. `?move=true` matters when the folder is computed from the device, for example one folder per tenant (`.../folders/shared/{{ data.tenant.slug }}/entries/{{ data.name | lower }}?move=true`): when a device changes tenant, the entry follows it and the copy in the old folder is removed, instead of both existing. See [`PUT .../entries/:entry`](api.md#put-apiaddressbookfoldersscopefolderentriesentry-admin).

### Credentials for synced entries

NetBox never carries secrets, so a webhook creates entries without a password or key. Rather than a second pass that fills them in later, give the folder **default credentials**: a per-session-type username, password or private key that entries without their own borrow at connect time. Set them once on the folder (Connections, edit folder, Default credentials; or [`PUT .../defaults`](api.md#put-apiaddressbookfoldersscopefolderdefaults-admin)):

```bash
curl -X PUT https://console.example.com/api/addressbook/folders/shared/netbox-sync/defaults \
-H "Authorization: Bearer <admin-api-key>" \
-H "Content-Type: application/json" \
-d '{"ssh": {"username": "root", "private_key": "'"$(sed ':a;N;$!ba;s/\n/\\n/g' fleet-key)"'"}}'
```

A synced entry then shows **Connect**, not **Login...**, from the moment the webhook fires. An entry that is given credentials of its own keeps them; an entry with `"prompt_credentials": true` still prompts. The Connections page picks all of this up on its own within a few seconds (it polls [`GET /api/addressbook/version`](api.md#get-apiaddressbookversion)), so nobody has to reload.

### Create webhook: device deleted

1. **Create an Event Rule**:
Expand Down
33 changes: 33 additions & 0 deletions docs/reverse-proxies.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,36 @@ trusted_proxies = ["127.0.0.1/32"]

Without this setting, all requests from behind the proxy appear to come
from the proxy's own IP.

## Content-Security-Policy

rustguac sends its own `Content-Security-Policy` on every response. Leave
it alone: the Guacamole client needs `img-src 'self' data: blob:` because
browsers without WebCodecs `ImageDecoder` (Safari, Firefox) draw every
image tile from a `data:` URI. If your proxy sets a CSP of its own, keep
that directive, or SSH/RDP sessions render black in those browsers while
Chrome looks fine.

Releases up to v1.10.5 shipped a policy with no `img-src`, which is
exactly that failure (#249). If you can't upgrade, have the proxy replace
the header. The whole policy has to be repeated because the header is
replaced, not merged:

HAProxy (backend section):

```
http-response set-header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' wss: ws:; img-src 'self' data: blob:"
```

nginx:

```nginx
proxy_hide_header Content-Security-Policy;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' wss: ws:; img-src 'self' data: blob:" always;
```

Caddy:

```
header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' wss: ws:; img-src 'self' data: blob:"
```
Loading