Releases: sol1/rustguac
v1.7.3
A small UX-focused release. The v1.7.2 floating fullscreen button was unobtrusively in the way for everyone who wasn't actively using it; v1.7.3 moves it somewhere more sensible.
Headline changes
Fullscreen toggle moves into the Ctrl+Alt+Shift session menu (#156, @vk2amv)
In v1.7.2 the manual fullscreen control was a floating "⛶ Fullscreen" button in the top-right corner at 0.45 opacity. It worked, but it was 80px of permanent UI in the corner of every session for the majority of users who never clicked it. @vk2amv pushed back: the button covers part of the remote display (notably the xfce4 / Windows panel area) and never gets out of the way.
In v1.7.3, the floating button is gone. The manual fullscreen action lives in the existing Ctrl+Alt+Shift session menu (next to Home), so it is one extra keystroke away when you actually need it and zero pixels otherwise. The per-entry fullscreen_on_connect flag, the in-fullscreen top bar (entry name + Exit + Disconnect), and the Esc-key forwarding via navigator.keyboard.lock are all unchanged.
Documented in-session keyboard shortcuts
docs/web-sessions.md gains a new In-session keyboard shortcuts section covering:
Ctrl+Alt+Shiftto toggle the session menu / clipboard panel.Ctrl+V(orCmd+Von macOS) to sync local browser clipboard text into the remote session before forwarding the paste key event.Escbehaviour in fullscreen (browser-native exit;navigator.keyboard.lockkeeps it reaching the remote on Chromium-based browsers; one-time on-screen notice when lock is unavailable).- How
disable_copy/disable_pasteper-entry flags interact with these shortcuts.
Dependencies
guacd pin unchanged at 2980cf0.
Thanks
v1.7.2
A small focused release. One new feature, one dependency batch, one documentation gap closed.
Headline changes
Fullscreen on connect, with Escape key forwarding (#154)
Sessions can now be configured to open in browser fullscreen automatically. The motivation, from @Napsty's request: prevent users from accidentally closing the rustguac browser tab when they meant to close a window inside the remote session.
New per-entry checkbox Open in fullscreen on connect in the entry editor. When set, the client enters fullscreen on the first user gesture after the WebSocket connects, with a thin top bar showing the entry name plus an Exit Fullscreen button and a Disconnect button. The browser tab chrome (including the close X) is hidden in this mode.
A floating Fullscreen toggle in the top-right corner lets any user enter fullscreen at any time once connected, independent of the per-entry flag.
The interesting subtlety: in a remote-desktop session you need the Escape key to reach the remote (vim, dialogs, menus), but browsers reserve Esc to exit fullscreen by default. v1.7.2 uses the Keyboard Lock API (navigator.keyboard.lock(['Escape'])) on Chromium-based browsers so Esc passes through to the session; users exit fullscreen by holding Esc for two seconds (Chrome's built-in timer) or clicking the Exit button in the top bar. Firefox and Safari don't implement Keyboard Lock; on those browsers Esc still exits fullscreen and a one-time notice tells the user so.
The feature applies to all session types (SSH, RDP, VNC, Web, VDI).
Dependency batch
Four dependabot bumps landed together:
- russh 0.60.3 -> 0.61.1. Carries two security fixes: GHSA-wwx6-x28x-8259 (compression ZIP bomb bypassing max-packet size checks) and GHSA-hpv4-5h6f-wqr3 (server-side state reset on username change). This reverses the v1.7.0 decision in #144 to hold at the 0.60.x patch line. The 0.61 API churn hasn't changed since then, but the security fixes are reason enough to absorb it.
- rusqlite 0.39 -> 0.40. Bundled SQLite to 3.53.1. Closes #149.
- socket2 0.6.3 -> 0.6.4. Closes #151.
- aes 0.9.0 -> 0.9.1. Closes #152.
Microsoft Entra ID setup guide
docs/integrations.md gains a full Entra ID OIDC setup section. Previously we only had the Authentik guide, and the canonical example used extra_scopes = ["groups"] which works for Authentik / Keycloak / JumpCloud but always fails against Entra with AADSTS650053. The new section walks through the Token Configuration claim setup and the troubleshooting steps. Prompted by #153.
Dependencies
russh 0.61.1, rusqlite 0.40.0, socket2 0.6.4, aes 0.9.1. fuzz/Cargo.lock russh kept in sync (#155). guacd pin unchanged at 2980cf0.
Thanks
v1.7.1
Themes are now data, not code. A small follow-up release focused on getting one paper cut out of the way and laying observability groundwork for the next round of bug hunting.
Headline changes
Themes load from <static_path>/themes/*.toml at runtime
Previously, adding a new colour preset meant editing src/config.rs, recompiling, and shipping a new release. Themes are presentation data and didn't deserve that overhead. v1.7.1 introduces a runtime loader: drop a <name>.toml into <static_path>/themes/ (typically /opt/rustguac/static/themes/), restart rustguac, and the theme appears in the gear-menu picker. The filename is the theme id. A file named after a built-in (aurora.toml, corporate.toml, ...) overrides the built-in, so re-branding without a fork is just a matter of editing your own copy.
The eight built-in presets (aurora, dark, light, high-contrast, terminal, nord, corporate, jaguar) are unchanged and remain the always-available fallback when no themes directory exists. Theme ids are validated against [a-zA-Z0-9_-]{1,64} so they're safe to render in the UI picker and free of path-traversal mischief via crafted filenames.
The first community-contributed theme arrives in this release: Catppuccin Macchiato (lavender accent variant), contributed by @dav0l in #148. dav0l originally submitted against the old Rust-array model; we landed it instead as static/themes/catppuccin-macchiato.toml to validate the new mechanism on a real third-party theme.
New documentation: docs/themes.md. The [theme] section in docs/configuration.md is trimmed to a brief stub pointing there.
Backward compatibility is explicit. Every existing config.toml [theme] block keeps working bit-for-bit identically - no preset, preset only, preset + per-field overrides, overrides only, even typo'd preset names. A test (existing_user_config_with_theme_section_keeps_working_after_upgrade) parses five realistic upgrade scenarios and asserts byte-equal colour output between the legacy resolver and the new one. Users upgrading from 1.7.0 see one extra theme appear in the picker (catppuccin-macchiato) and nothing else changes.
Draw-op diagnostic helper in client.html
The browser client now keeps a 2000-entry ring buffer of recent draw-op instructions (rect, cfill, copy, img, dispose, size) decoded into {op, layer, rect, note} form. Call rustguacDumpDraws() in DevTools to dump everything as a console.table, or rustguacDumpDraws(x, y) to filter to ops covering a specific pixel - point it at the top-left of a visible artifact (e.g. a black tile) to see exactly which ops painted there. cfill rgba(0,0,0,255) is annotated <BLACK> so black fills jump out, and copy ops carry their source coordinates in the note. Append ?debug=draw to the client URL for live per-op logging instead of on-demand dumping.
Zero overhead in steady state - the ring buffer is just an in-memory array, no console writes unless you ask. This came out of the #118 investigation and stays in the codebase as reusable diagnostic infrastructure.
On #118 (RDP resize black tiles)
The errata on the v1.7.0 release notes and #118 itself explain in full: subsequent testing showed patch 005 (005-rdp-resize-dirty-flush) doesn't actually fix the user-visible bug. The black tiles come from the RDP server (xrdp in confirmed reproductions) explicitly painting the entire new viewport black via cfill instructions after a resize, then only incrementally repainting what its dirty tracking treats as changed - leaving "unchanged from the new black state" regions visibly black. Patch 005 doesn't address that mechanism.
v1.7.1 leaves patch 005 in tree because it isn't actively harmful in our build. The investigation continues; a real fix is on the radar for a future release.
Dependencies
No dependency changes in this release.
Thanks
v1.7.0
Feature and fix release. A long-standing RDP resize artifact is fixed, folders you can't access are now hidden instead of shown-then-denied, Ctrl+V no longer leaves modifiers stuck, and VDI gains a container-naming fix plus example images. Routine dependency bumps.
Highlights
RDP resize black/ghost regions fixed (#118)
Resizing the browser during an RDP session could leave sharp black rectangles where static window content had been, persisting until something repainted those pixels. The cause was in guacd: guac_rdp_gdi_desktop_resize() resized the GDI buffer and display layer but never marked the layer dirty, so the post-resize flush sent nothing and the client kept stale/blank regions.
We now carry a guacd patch (patches/005-rdp-resize-dirty-flush.patch) that marks the whole layer dirty and issues a RefreshRect for the new desktop area after a resize. This fixes the legacy bitmap path, which is rustguac's default (enable_gfx off). GFX/RDPGFX sessions use a separate surface-cache path that does not exhibit the artifact in practice.
Root cause diagnosed and the fix supplied by @Bails309 on the issue. Thank you.
Inaccessible folders are now hidden (#147)
Previously, subfolders of a folder you could open were all listed regardless of their own group ACLs, so you could see folders you weren't entitled to and only discover that on click ("no access"). This also leaked folder names to unauthorized users.
Subfolders are now filtered by access. A folder is shown if you can access it directly or can access any descendant of it, so deeper grants are never orphaned out of the tree. Admins still see everything. Reported by @Napsty (Claudio Kuenzler).
Ctrl+V no longer sticks modifier keys
Pasting with Ctrl+V could leave Ctrl (or Alt/Shift) stuck "down" on the remote until pressed again. The Ctrl+V paste path reads the browser clipboard, which shifts focus to a permission prompt; the modifier's keyup then landed off-page and was never forwarded. The client now releases all held keys on window blur / visibility change (the canonical Guacamole guard), which also covers alt-tab-with-modifier-held and clicking away to another app.
VDI container-name collisions fixed (#138)
A user with multiple VDI entries resolving to the same in-container username got the same Docker container name for all of them, so starting one could reuse or replace another. Container names now include the sanitized entry name (rustguac-vdi-{username}-{entry}); same user + same entry still reuses one container, different entries run independently. Contributed by @vk2amv (Lindsay Harvey).
VDI example images (#139)
Two example VDI container images added under contrib/: one with PulseAudio audio, one with PulseAudio + x264/GFX. Also from @vk2amv.
Dependencies
- pulldown-cmark 0.13.3 -> 0.13.4
- tower-http 0.6.10 -> 0.6.11
- serde_json 1.0.149 -> 1.0.150
- cbc 0.2.0 -> 0.2.1
- russh 0.60.2 -> 0.60.3 (held at the 0.60.x patch line; the 0.61.0 minor was declined for now)
Thanks
Thanks to @Bails309 for diagnosing and fixing the RDP resize artifact (#118), @Napsty (Claudio Kuenzler) for the folder-visibility report (#147), and @vk2amv (Lindsay Harvey) for the VDI container-naming fix (#138) and example images (#139).
Errata (added 2026-05-28)
The "RDP resize black/ghost regions fixed (#118)" change above does not actually resolve the user-visible black-tile symptom against xrdp (Linux RDP) targets, contrary to what we said when shipping v1.7.0. Subsequent testing against a reproducing session showed the black tiles are caused by the server emitting cfill rgba(0,0,0,255) instructions tiling the entire new viewport on resize, then only incrementally repainting changed regions - whatever the server's dirty tracking treats as "unchanged" stays black. Patch 005-rdp-resize-dirty-flush addresses a different (theoretical) failure mode and doesn't help this one; it may even amplify the server's full-paint-with-black cycle via RefreshRect.
The patch still ships in v1.7.0 because it isn't actively harmful, but #118 is reopened and under investigation. See #118 for the diagnostic trail.
The other shipped changes in v1.7.0 (folder access hiding #147, Ctrl+V stuck-modifier fix, VDI #138/#139, dependency bumps) are unaffected.
v1.6.9
Maintenance and improvements release. VDI gets three new operator capabilities (lifecycle hooks, host port range control, per-entry credential overrides) plus an editor fix that surfaced VDI fields properly. xrdp setup script extended for LMDE 7. Routine dependency bumps.
Headline changes
VDI host port range + lifecycle hooks (#137)
Two new [vdi] config options for operators who need predictable container networking:
[vdi]
port_range_start = 39000
port_range_end = 39999
container_hook_script = "/opt/rustguac/vdi-container-hook.sh"
container_hook_timeout_secs = 10Port range bounds the host port Docker publishes the container's RDP listener on. Without this, Docker picks an arbitrary high port at runtime, which is awkward to coordinate with firewalls, reverse proxies, and identity-aware gates. Selection inside the range is deterministic from the operator's identity (FNV-1a hash), so reconnects from the same user land on the same port. Falls through to the next port on collision; existing containers whose port falls outside the configured range are recreated so the constraint is consistently enforced.
Lifecycle hooks let deployments run external preparation and cleanup logic around each container without baking it into rustguac itself. Script is invoked as <script> up <port> <container_id> <container_name> after the port is known but before xrdp readiness checks, and again as <script> down ... before container removal. Same values also passed as RUSTGUAC_VDI_* env vars. Bounded by container_hook_timeout_secs. Useful for firewall opens, service-mesh registration, identity-aware network access control.
Per-entry VDI username/password override (#132)
Previously rustguac auto-derived the VDI container username from the operator's OIDC identity and generated an ephemeral password per connect, pushing both into the container as VDI_USERNAME / VDI_PASSWORD env vars. Images that read those vars and run useradd / chpasswd worked fine; images with a baked-in fixed account ignored them and the operator had to manually log in inside the session.
New optional container_username and container_password fields on VDI entries let admins specify a fixed account that matches the image's baked-in user. When set, rustguac uses those for the RDP login into the container. The values are still injected as VDI_USERNAME / VDI_PASSWORD for consistency, so images that also honour the env vars get a coherent state. Both fields support credential variables so the actual values can be sourced per-operator from saved credentials.
VDI editor surfaces all container fields (#131)
container_env, container_cpu_limit, and container_memory_limit were persisted to Vault correctly but never returned by the list endpoint, so the entry editor appeared blank for those fields on reopen even though the underlying Vault payload was intact. Added the three fields to EntryInfo and its From impl. No behavioural change on the write path; only fixes the read-side exposure to the UI.
xrdp setup script: LMDE 7 + Cinnamon (#130)
contrib/setup-xrdp-gfx.sh now detects Linux Mint Debian Edition 7, picks the right Debian source codename when LMDE reports its own, replaces a broken upstream install_pulseaudio_sources_apt.sh helper with an inline equivalent that works on both Debian and LMDE, and adds Cinnamon as a --desktop option. Bonus Microsoft Edge installer and per-session audio loader script with logging.
Dependencies
- bollard 0.20 -> 0.21 (Cargo.toml widened to "0.21")
- tower-http 0.6.8 -> 0.6.10
- tokio 1.52.1 -> 1.52.3
- rcgen 0.14.7 -> 0.14.8 (+ yasna 0.5.2 -> 0.6.0 transitive)
Closes dependabot PRs #125, #126, #128, #129.
Thanks
Thanks to Lindsay Harvey (@vk2amv) for the VDI port range and lifecycle hooks feature, the xrdp setup script extension, and the three carefully-filed VDI tickets (#131, #132, plus the JumpboxVDI-boundary discussion in #133).
v1.6.8
Bugfix and improvements release. Tunnel-stability work on the WebSocket proxy, a one-shot contrib/vault-quickstart.sh to take a fresh box from "no Vault" to "rustguac-ready Vault" in seconds, plus a clutch of small bug fixes from operator reports.
Headline changes
Tunnel stability: ping echo, instruction-boundary alignment, TCP keepalive
The Guacamole client sends an empty-opcode ping instruction every 500ms over the WebSocket to keep its receive timer alive. The Apache reference webapp echoes those pings back to the browser; rustguac was forwarding them straight to guacd, which silently dropped them as unknown opcodes. With nothing inbound to reset the client's 1.5s "unstable" timer, idle sessions logged a constant trickle of [rustguac] tunnel unstable and could close on genuinely quiet sessions after 15s. v1.6.8 mirrors Apache's filter: empty-opcode pings are echoed back, never forwarded.
The first cut of that change introduced a parser race (the browser saw (half-instruction)(ping bytes)(other half) and threw "Element terminator was not ';' nor ','"). The fix is a length-prefix-aware boundary scanner in src/protocol.rs that ensures every Message::Text from rustguac to the browser ends at a true Guacamole instruction boundary, even when an element value contains a literal ; (clipboard text, text streams). 11 new unit tests cover empty buffers, partial frames, embedded ;, multibyte truncation, and trailing garbage.
TCP keepalive (30s idle, 10s probe, 3 retries, ~60s detection) is now applied to the inbound listener (Linux inherits SO_KEEPALIVE to accepted sockets) and to both rustguac→guacd connect sites. Catches silent NAT/firewall path drops within ~60s on either leg of the proxy. None of this fixes a path with sustained packet loss; that remains a network problem.
Vault / OpenBao quickstart helper
New contrib/vault-quickstart.sh with three modes:
| Mode | Use case |
|---|---|
| (default) | Provision an existing Vault using $VAULT_ADDR + $VAULT_TOKEN |
--dev |
Spawn an in-memory dev-mode server and provision it (demos, throwaway dev) |
--local |
Install Vault or OpenBao as a systemd service with file storage and on-disk auto-unseal |
Auto-detects vault vs bao and picks the matching filesystem layout, system user, and service name (vault.service for Vault, openbao.service for OpenBao). The --local mode writes a SECURITY.txt next to the on-disk unseal key explicitly calling out the convenience-over-security trade. Idempotent: re-running detects existing user, mount, policy, AppRole, and systemd unit. The README and docs/integrations.md gain a clearer Requirements story making explicit that Vault or OpenBao is required for the Connections feature, not optional.
Bug fixes
- #121: OIDC
client_secretwas a non-Optional struct field inOidcConfig, so aconfig.tomlwithout aclient_secret = "..."line failed TOML parsing before the documentedOIDC_CLIENT_SECRETenv var override could fill it in. The field is nowOption<String>with explicit startup validation when[oidc]is configured. - #122: Authentik setup guide was missing the prerequisite step to create a Groups scope mapping under Customisation > Property Mappings (the
groupsscope does not exist by default in fresh Authentik instances). - #123 (parts 2 + 3): Per-session drive cleanup hardcoded
retention_secs = 0and never readcleanup_on_close, so both flags were dead code at end-of-session teardown. The fix routes both values through. Docs gain a "Cleanup behaviour" subsection clarifying thatretention_secsonly takes effect whencleanup_on_close = true. Items 1 (upload disconnects session) and 4 (drag-drop UX) postponed for further testing under the same issue.
Dependencies
Thanks
Thanks to Simon for feedback on the Vault dependency story and the shape of the quickstart script, and to Josh Matthews (@joshsol1) for the careful bug reports against #121, #122, and #123.
v1.6.7
v1.6.7: Connections quick-find search, RustCrypto batch upgrade
Find any connection in two keystrokes
A quick-find search bar now lives in the entries header on the Connections page, between the folder title and the admin buttons. It searches across every connection you have access to, not just the folder you happen to have selected. Press / from anywhere on the page to focus it; type a word or two; matching entries appear with the folder breadcrumb shown so you know where each one lives. The matched substrings are highlighted, results are scored (name-prefix beats name-substring beats host beats folder-path), and the list caps at 50 with a "+N more" footer if you need to narrow further.
Each result has a Connect button inline and an ↗ open folder link that expands the tree to the entry's folder, selects it, and scrolls it into view. Esc clears the query; second Esc blurs the input.
Backed by a new GET /api/addressbook/search-index endpoint that walks the full visible tree once and returns a flat list of credential-stripped entries. ACL is enforced per folder (a child can grant access independently of a denied parent, so the walk does not prune; only the entries it emits are gated). The index is fetched in the background after the initial folder load, so the page stays fast and search activates as soon as the request returns.
Thanks to JSC for raising the request.
RustCrypto family + rand 0.10 batch upgrade
Coordinated bump that has been pending since the v1.6.6 cycle:
aes0.8 to 0.9cbc0.1 to 0.2hmac0.12 to 0.13pbkdf20.12 to 0.13sha10.10 to 0.11rand0.9 to 0.10
These crates share digest 0.11 traits across the family and could not be bumped one at a time; pbkdf2 0.13.0 shipping stable was the trigger to harvest the group. API call-site fixes were minimal: cbc 0.2 renamed BlockEncryptMut to BlockModeEncrypt and encrypt_padded_mut to encrypt_padded, and rand 0.10 renamed the Rng trait to RngExt. The five Chromium password encryption tests pass after the bump, confirming the v10 / PBKDF2 / AES-128-CBC pipeline used for headless web-session autofill is bytewise unchanged.
Closes #107, #108, #109, #111, #113, #117.
Test suite
207 tests passing. cargo audit clean.
v1.6.6
v1.6.6: Zombie WebSocket fix, smarter Reconnect
Zombie WebSocket fix
When a mid-path TCP drop killed the WebSocket, the Guacamole client stayed stuck in CONNECTED forever. Tab looked frozen, no overlay, mouse moved locally but clicks went into a closed socket. Upstream Client.js doesn't subscribe to tunnel.onerror / onstatechange; the Apache webapp wires those externally. Our lean client.html missed that glue. Fixed.
Reconnect actually reconnects
The Reconnect button on the disconnected overlay used to just reload the dead session URL, which walked straight back into the same overlay. It now POSTs a fresh /connect against the original Connections entry and navigates to the new session. Ad-hoc and shareToken paths fall back to /connections.html. Bonus: client.onerror now clears the thumbnail upload interval, so the secondary leak of XHR 404s against a dead session id stops the moment the overlay shows.
Other polish
- OIDC discovery errors: trailing-slash mismatches now produce a single actionable line instead of raw
openidconnectDebug output. Generic provider, not just JumpCloud. contrib/setup-xrdp-gfx.shnow adds xrdp tossl-certand normaliseskey.pemperms, fixing the FreeRDP MAC-checksum failure after a sid rebuild.- Aurora theme now applies when
[theme]is absent inconfig.toml, not just when it's present-but-empty. - New
docs/reverse-proxies.mdcovering nginx / Caddy / Apache / Traefik, including the%2F-decoding gotcha that 404s nested folder paths. Thanks @mauroparente (#105).
Dependencies
rustls-webpki0.103.13 (RUSTSEC-2026-0104: CRL-parse panic + URI excluded-subtree fix)rustls0.23.39,russh0.60.1,libc0.2.186
RustCrypto batch (aes 0.9, cbc 0.2, hmac 0.13, pbkdf2 0.13) and rand 0.10 deferred to v1.6.7 (#117).
v1.6.5
v1.6.5: RDP default to NTLM, Connections tree remembers where you were
RDP: NTLM is now the default authentication package
Rustguac previously defaulted to FreeRDP's Negotiate (Kerberos first, NTLM fallback) when an address book entry had no explicit auth_pkg. In practice almost nobody has a working KDC reachable via DNS on the jumpbox host, and the Kerberos failure mode is a silent RDP hang that looks identical to a dead network. Every new or Guacamole-imported RDP entry hit this.
As of v1.6.5, the effective default is NTLM. Existing entries with no auth_pkg set (including everything imported from Guacamole) automatically resolve to NTLM on connect. Per-entry overrides still work, and there's a new config escape hatch for environments that do run Kerberos:
[rdp]
default_auth_pkg = "kerberos" # or "negotiate", or omit for the "ntlm" defaultThe entry modal's NLA dropdown now reads "Server default (NTLM)" instead of "Default (negotiate)" so the label matches the behaviour, with an explicit "Negotiate (Kerberos first, NTLM fallback)" option added for completeness. Resolver precedence is: per-entry value, else [rdp] default_auth_pkg, else hardcoded "ntlm".
If you hit a silent 30+ second RDP hang after login on any prior version, this release fixes it.
Connections: folder expansion and selection persist across reloads
The Connections page now remembers which folders you had expanded and which folder you had selected, so reopening the page or logging back in no longer collapses the whole tree or snaps you back to the alphabetical first folder. State lives in localStorage per-browser (not synced across devices); stale entries for deleted folders self-prune on the next restore.
rustguac_connections_expanded: map ofscope|path-> true, saved on every tree toggle and on auto-expand-after-new-subfolder.rustguac_connections_selected:{scope, path}, saved on every selection change (click, create, delete, move-entry).
On page load, the top-level folders fetch chains into a restore walk that depth-sorts saved expansion keys, fetches each level's subfolders, then attempts to restore the saved selection via the now-populated cache. If the saved selection no longer exists (deleted, ACL revoked), falls back to the existing auto-select-first behaviour. All storage calls are wrapped in try/catch so private-mode / quota errors degrade silently.
Dependency notes
No Dependabot updates bundled. The only open alert (GHSA-cq8v-f236-94qc, rand 0.8.6 soundness, low) still has no patched version available, and its UB preconditions don't apply to rustguac's tracing-based logging.
v1.6.4
v1.6.4: connections audit log
Destructive and mutating actions on the Connections address book are now persisted to a new SQLite table (addressbook_audit_log) and surfaced on the Admin page under a "Connections Audit Log" section. Scope covers: create_folder, update_folder, delete_folder, create_entry, update_entry, delete_entry.
Each row captures the caller's email (or API key name for admin-key access), action, scope, folder path, optional entry name, client IP (resolved through the existing trusted-proxy logic), a small JSON details blob, and a timestamp.
What gets logged
The details blob is deliberately headline-only:
delete_folder:{"subfolders_deleted": 3, "entries_deleted": 17}create_folder/update_folder:{"allowed_groups_count": 2, "inherit_from_parent": true}create_entry/update_entry:{"type": "ssh"}delete_entry: no details.
Entry field values (passwords, private keys, hostnames, usernames, domains) and full request bodies are never logged. Audit rows live in SQLite, not in Vault, so logging content would pull Vault-only secrets onto disk and break the credentials-only-in-Vault design.
API
New admin-only endpoint GET /api/admin/addressbook-audit?limit=100&email=<filter> (capped at 1000 rows). Same shape as the existing GET /api/admin/token-audit.
The retention sweeper (cleanup_old_audit_log) now prunes both audit tables on the same retention window.
Dependency notes
No Dependabot updates bundled. The only open alert (GHSA-cq8v-f236-94qc, rand 0.8.6 soundness, low) still has no patched version available, and its UB preconditions don't apply to rustguac's tracing-based logging.