From 32ab0864c462d566cb5158b2a0112f4c2b78e560 Mon Sep 17 00:00:00 2001 From: Derek Date: Thu, 3 Sep 2026 10:25:00 +1000 Subject: [PATCH] chore(soe): make the Arcane long-lived login an opt-in Arcane's default login lasts about a day, which is a prompt every morning on a dev box. Two knobs move it and both are needed -- either alone still logs you out. authSessionTimeout sets the access token's life, and the access-token cookie is the only thing that survives a browser restart. The server clamps it to 15..525600 minutes with no never-expire value, so a year from each login is the ceiling. JWT_REFRESH_EXPIRY stamps the session row's expiry at login and is never extended on refresh, so the image's 168h means every request 401s at day seven whatever the timeout says. Off by default: Arcane holds the Docker socket, so a year-long cookie is a per-machine decision. Both settings stay absent while the flag is off rather than being written at a default, so a value set by hand in the UI survives a re-run. Verified on desktop-derek with real runs. Flag off: neither JWT_REFRESH_EXPIRY nor a written authSessionTimeout. Flag on: JWT_REFRESH_EXPIRY=87600h in the container env and authSessionTimeout=525600 in the settings table. Flag off again: the timeout is left at 525600 rather than reset. Not achievable on v2.10.1: changed=0 on a re-run. That version's settings GET returns none of the keys the role manages, so the diff always fires. It predates this change -- with the flag off, carrying exactly what main carries, the assert still reports changed. --- README.md | 4 ++-- ansible/roles/soe/defaults/main.yml | 19 +++++++++++++++++ ansible/roles/soe/tasks/arcane.yml | 25 ++++++++++++++++------- ansible/roles/soe/templates/arcane.env.j2 | 5 +++++ docs/install-matrix.md | 2 +- install.sh | 3 ++- 6 files changed, 47 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 09ce0cd..a233273 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ flowchart TD | `vpn-clients` | OpenVPN 3, WireGuard, Tunnelblick (macOS) | | `vm` | VM guest optimisations (QEMU/SPICE agents) | | `power-profile` | Sleep/idle/lid policy. `always-on` (default) or `vm`, via `-e power_profile=` | -| `arcane` | Arcane container UI, localhost-only. Off unless `-e soe_arcane_enabled=true` | +| `arcane` | Arcane container UI, localhost-only. Off unless `-e soe_arcane_enabled=true`. Add `-e soe_arcane_long_session=true` for a year-long login | | `local-services` | Persistent local ClickHouse + Redpanda for spikes. Off unless `-e soe_local_services_enabled=true` | ### Remote Login is not Desktop Sharing @@ -175,7 +175,7 @@ reports success -- the settings simply land where nobody sees them. - `contributor`: hyperi-ci and the tools its checks drive (semgrep, alint), gitleaks, trivy, hadolint, pip-audit, ansible-lint, pre-commit, act, and git-scrub for rewriting AI residue or a leaked secret out of git history -- gitleaks scans FULL history, so a secret removed from HEAD still fails the gate. macbash is here too: it flags the GNU-only bash constructs that break on macOS, and CONTRIBUTING.md asks for it on every shell change - `soe` / `soe-gui`: HyperI org policy: VPN clients, Claude Code, Slack, LibreOffice, RDP client, telemetry-disable, auto-updates, GNOME taskbar - `power-profile` (off by default, and deliberately not in `soe`): sleep, idle and lid policy, selected per machine. `always-on` (the default profile) never idle-suspends on mains power and does not sleep when the lid shuts -- for a repurposed laptop doing build work, or a desktop that has to answer ssh. `vm` never sleeps or suspends at all, for an unattended RDP guest that nobody can walk over and wake. Battery behaviour stays stock under `always-on`, because a machine that will not sleep in a bag cooks itself. Profiles are data files, so adding one is adding a file -- see [roles/power-profile/README.md](ansible/roles/power-profile/README.md) -- `arcane` (off by default): [Arcane](https://getarcane.app), a web UI for the containers on the box. Enable it with `-e soe_arcane_enabled=true` and you get a daemon on `http://localhost:3552` that comes back after a reboot and keeps itself updated. Works against docker-ce on Linux and colima on macOS. Bound to loopback because it holds the Docker socket, so whatever reaches that port owns the machine. Login is whatever Arcane seeds -- `arcane` / `arcane-admin` as upstream documents it. The role sets neither, and only clears the forced first-login password prompt, which it does by re-submitting that seeded password so the credentials stay unchanged. That needs the password policy relaxed to `basic` (`soe_arcane_password_policy`), because upstream's default `strong` policy rejects its own seeded password. There is still a login -- auto-login sits behind a `buildables` Go build tag that no published image is compiled with, so zero-auth is not available without building your own image +- `arcane` (off by default): [Arcane](https://getarcane.app), a web UI for the containers on the box. Enable it with `-e soe_arcane_enabled=true` and you get a daemon on `http://localhost:3552` that comes back after a reboot and keeps itself updated. Works against docker-ce on Linux and colima on macOS. Bound to loopback because it holds the Docker socket, so whatever reaches that port owns the machine. Login is whatever Arcane seeds -- `arcane` / `arcane-admin` as upstream documents it. The role sets neither, and only clears the forced first-login password prompt, which it does by re-submitting that seeded password so the credentials stay unchanged. That needs the password policy relaxed to `basic` (`soe_arcane_password_policy`), because upstream's default `strong` policy rejects its own seeded password. There is still a login -- auto-login sits behind a `buildables` Go build tag that no published image is compiled with, so zero-auth is not available without building your own image. The login lasts about a day by default, which is a prompt every morning on a dev box -- `-e soe_arcane_long_session=true` stretches it to a year from each login. It is off by default because Arcane holds the Docker socket, and it takes one log-out and log-in to take effect, since the session expiry is stamped at login - `local-services` (off by default): a persistent local ClickHouse and Redpanda for ad-hoc work -- somewhere to poke at a query or hand-feed a topic without waiting for a suite to build. Enable with `-e soe_local_services_enabled=true`. Deployed **stopped**: `restart: no`, so a reboot leaves them down and they cost nothing until `local-services up`, which pulls latest and takes seconds. Both capped at 1GB and bound to loopback. They are spike instances -- integration and e2e suites create and tear down their own containers, because a shared daemon makes a suite non-hermetic and order-dependent **Desktop UI** (`winlike` or `maclike` tag): GNOME extensions, a transparent taskbar (winlike) or a dock (maclike). diff --git a/ansible/roles/soe/defaults/main.yml b/ansible/roles/soe/defaults/main.yml index 6e0d9d1..173eea8 100644 --- a/ansible/roles/soe/defaults/main.yml +++ b/ansible/roles/soe/defaults/main.yml @@ -49,6 +49,25 @@ soe_arcane_timezone: UTC # says -- the request schema rejects anything shorter before the policy is read. soe_arcane_password_policy: basic +# Arcane's default login lasts about a day. Two knobs move that and BOTH are +# needed -- either alone still logs you out. +# +# soe_arcane_session_timeout_minutes sets the access token's life, and the +# access-token cookie is the only thing that survives a browser restart. The +# server clamps it to 15..525600 minutes and has no never-expire value, so a +# year from each login is the practical ceiling. +# +# soe_arcane_refresh_expiry stamps the session row's expiry at login and is +# never extended on refresh, so leaving it at the image's 168h means every +# request 401s at day seven whatever the timeout says. +# +# Off by default: Arcane holds the Docker socket, so a year-long cookie is a +# per-machine decision. Both settings stay ABSENT while the flag is off, rather +# than being written at a default, so a value set by hand in the UI survives. +soe_arcane_long_session: false +soe_arcane_session_timeout_minutes: 525600 +soe_arcane_refresh_expiry: 87600h + # ============================================================================ # Local services -- persistent ClickHouse + Redpanda for ad-hoc work, OPT-IN. # ============================================================================ diff --git a/ansible/roles/soe/tasks/arcane.yml b/ansible/roles/soe/tasks/arcane.yml index 9eedc33..8f70645 100644 --- a/ansible/roles/soe/tasks/arcane.yml +++ b/ansible/roles/soe/tasks/arcane.yml @@ -234,17 +234,22 @@ | map(attribute='value'))) }} when: not ansible_check_mode - - name: Work out the auto-update settings this host should have + # authSessionTimeout is added only when the long session is opted into, so a + # run with the flag off leaves whatever is there rather than stamping a + # default over a value set by hand in the UI. + - name: Work out the settings this host should have # noqa: var-naming[no-role-prefix] -- soe_ IS the role prefix here ansible.builtin.set_fact: - soe_arcane_settings_want: - autoUpdate: "{{ soe_arcane_auto_update | bool | lower }}" - autoUpdateExcludedContainers: "{{ soe_arcane_auto_update_exclude | join(',') }}" + soe_arcane_settings_want: >- + {{ {'autoUpdate': soe_arcane_auto_update | bool | lower, + 'autoUpdateExcludedContainers': soe_arcane_auto_update_exclude | join(',')} + | combine({'authSessionTimeout': soe_arcane_session_timeout_minutes | string} + if soe_arcane_long_session | bool else {}) }} - # `autoUpdate` is a database setting rather than an environment override, so - # asserting it needs the API. Values are strings upstream, not booleans. + # These are database settings rather than environment overrides, so asserting + # them needs the API. Values are strings upstream, not booleans or numbers. # Only PUT on a real difference, otherwise every run reports changed. - - name: Assert Arcane's auto-update settings + - name: Assert Arcane's managed settings ansible.builtin.uri: url: "http://127.0.0.1:{{ soe_arcane_port }}/api/environments/0/settings" method: PUT @@ -273,6 +278,12 @@ There is still a login: auto-login sits behind a `buildables` Go build tag that no published image is compiled with. + {% if soe_arcane_long_session %} + Long sessions are on ({{ soe_arcane_session_timeout_minutes }} minutes). + Log out and back in once -- the session expiry is stamped at login, so + the session you are already in keeps its old one. + {% endif %} + Stack: {{ soe_arcane_dir }} Updates: hyperi-update pulls and recreates it; Arcane's own updater keeps the containers it manages current. diff --git a/ansible/roles/soe/templates/arcane.env.j2 b/ansible/roles/soe/templates/arcane.env.j2 index 94269a8..de6c30a 100644 --- a/ansible/roles/soe/templates/arcane.env.j2 +++ b/ansible/roles/soe/templates/arcane.env.j2 @@ -9,3 +9,8 @@ TZ={{ soe_arcane_timezone }} ENCRYPTION_KEY={{ soe_arcane_encryption_key }} JWT_SECRET={{ soe_arcane_jwt_secret }} ADMIN_STATIC_API_KEY={{ soe_arcane_admin_api_key }} +{% if soe_arcane_long_session %} +# Only present when soe_arcane_long_session is on. Absent, the image's own 168h +# applies and every session dies at day seven. +JWT_REFRESH_EXPIRY={{ soe_arcane_refresh_expiry }} +{% endif %} diff --git a/docs/install-matrix.md b/docs/install-matrix.md index 5b86af7..fc979da 100644 --- a/docs/install-matrix.md +++ b/docs/install-matrix.md @@ -343,7 +343,7 @@ hyperi-ci. | LibreOffice (org office suite) | Linux | distro repo | | Nemo, GNOME extensions (gext), fonts | Linux | distro / uv-tool / vendored | | colima + Apple `container` (macOS only) | macOS | brew / github-binary | -| Arcane container UI (opt-in `soe_arcane_enabled`) | all | container image | +| Arcane container UI (opt-in `soe_arcane_enabled`; `soe_arcane_long_session` for a year-long login) | all | container image | | Local ClickHouse + Redpanda (opt-in `soe_local_services_enabled`) | all | container image | | removals / update_command / admin-scripts (opt-in `never`, on for soe) | Linux | tombstones + scripts | diff --git a/install.sh b/install.sh index acaab66..b833bc5 100755 --- a/install.sh +++ b/install.sh @@ -203,7 +203,8 @@ HyperI SOE (soe, soe-gui) - org policy, includes everything above: forgejo/codeberg tea (Forgejo/Gitea CLI) colima macOS container daemon + Apple container (macOS only) arcane Container management UI, localhost-only (OPT-IN: - -e soe_arcane_enabled=true) + -e soe_arcane_enabled=true; add + -e soe_arcane_long_session=true for a year-long login) local-services Persistent local ClickHouse + Redpanda for spikes, deployed stopped (OPT-IN: -e soe_local_services_enabled=true)