Redmine SSO flow: Cloudron login -> click "Continue with KNEL Cloud"
button (#login-oauth-submit-1) -> authenticated. Required Cloudron
admin to grant vp-techops access to the Redmine app first.
API key was already present but hidden. Click "Show" in the
.api-key-actions section to reveal it from the #api-access-key
pre element. Key verified via X-Redmine-API-Key header.
Discourse SSO flow: Cloudron login -> click "Log In" -> click OpenID
Connect button -> complete signup (enter username) -> logged in.
User API key generated via Discourse RSA-based flow:
1. Generate RSA keypair, submit public key
2. Authorize request on Discourse
3. Capture encrypted payload from POST response
4. Decrypt with PKCS1v15 padding (Discourse uses this, not OAEP)
5. Parse JSON to extract the key field
API key verified working: User-Api-Key header returns 30 topics from
/latest.json. Key stored in Bitwarden as "vp-techops Discourse".
Redmine SSO is blocked: Cloudron returns "You do not have access" --
the vp-techops user needs app access granted by Cloudron admin.
Also added cryptography==44.0.1 to requirements for RSA operations.
Cloudron's 2FA enrollment flow discovered via comprehensive DOM dump:
1. Profile page -> click "Setup" to start 2FA
2. Cloudron defaults to Passkey -> click "switchToTotp"
3. TOTP secret appears as base32 text -> extract via regex
4. Enter code in #totpTokenInput -> click Enable
Fixed wrong selectors in cloudron_panel_login: the OIDC TOTP field is
#inputTotpToken (not #inputTotp as previously assumed). Verified full
2FA round-trip: password login -> TOTP prompt -> code entry -> #/apps.
2FA is now enabled on the vp-techops Cloudron account with TOTP secret
stored in Bitwarden. This removes a hard blocker for CMMC L3 compliance.
The provisioner's BitwardenHelper.login() was missing the critical
`bw sync` step that the host wrapper includes. Without syncing after
login, the container's local vault cache was empty/stale, causing items
to vanish between container runs. Added sync() call at end of login()
and before list_items().
Also fixed container UID/GID to match host user (1002:1002) for proper
bind-mount access, and added source-code volume mounts for fast iteration.
Verified with 5-phase cross-container persistence test (create in
container A, verify in fresh container B, update in C, confirm in D).
CRITICAL FIX: The --enable-2fa flow was creating duplicate BW items
instead of updating in place, which led to ambiguous item resolution
and data integrity issues. This was a severe failure in core
credential lifecycle operations.
Changes:
- bw_helper.py: Complete rewrite with safety guarantees
- update_item(): modifies existing item in place by ID, preserves
all fields not being updated
- create_item(): refuses to create duplicates (raises if item exists)
- get_item_id(): resolves name to ID, raises on ambiguous matches
- get_item(): returns full item JSON
- NO delete_item method exists by design -- credential deletion
is a manual operation only
- provision-agent.py: --enable-2fa now uses update_item() instead
of create_item() to add TOTP to existing credentials
- Dockerfile: non-root user with correct BW state directory ownership
- docker-compose.yml: bind mount for BW state (proper permissions)
- test_bw_helper.py: 10 tests covering full lifecycle
(create, read, duplicate rejection, update password, update TOTP,
field preservation, no-delete verification)
Tests 1-4 verified passing against live Vaultwarden instance.
- requirements.txt: added pytest
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
Security:
- Container now runs as non-root user 'provision' (CMMC/STIG audit
requirement). Root execution would fail security audits.
- BW state persisted in named volume at /home/provision/.config/
Bitwarden CLI to avoid slow re-auth on every run.
SSO refactoring:
- cloudron_panel_login(): establishes Cloudron panel session once,
shared across all subsequent app SSO flows
- sso_login(): clicks app-specific SSO/OAuth button, handles Cloudron
OIDC login + consent redirect, properly detects failures
- Detects "You do not have access" OIDC rejections (access control
issue, not a selector bug)
- Per-app SSO button selectors passed as parameters for extensibility
to future Cloudron apps (Dolibarr, Paperless, Firefly, etc.)
Debug:
- _debug_dump() captures screenshot + DOM at failure points
- Added dumps at Gitea token, Redmine key, and SSO failure locations
Current blocker: vp-techops Cloudron user not yet granted access to
Gitea/Redmine/Discourse apps. Cloudron 2FA confirmation pending.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
- Fix Cloudron invite form selectors to match real page IDs
(#inputUsername, #inputDisplayName, #inputPassword, #inputPasswordRepeat)
- Fix docker-compose.yml: use env_file instead of ${VAR} interpolation
(password contains $ chars that docker-compose corrupts)
- Update CLOUDRON_BASE from tsys-cloudron.knel.net to my.knownelement.com
Phase 1 Cloudron enrollment now works end-to-end: invite accepted,
password set, TOTP extracted, credential stored in Bitwarden.
Phase 2 (Gitea/Redmine/Discourse) needs selector updates for
the current UI versions of each system.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
Replace npm-based @bitwarden/cli with the pre-compiled native Rust bw
binary (v2026.7.0) to eliminate Node.js from the credential management
layer for CMMC/ITAR/STIG audit readiness.
Changes:
- Dockerfile: download native bw binary instead of npm install; add
python3-pip for Playwright dependencies
- bw_helper.py: renamed from bw-helper.py (Python can't import hyphens);
added BW_SERVER config for self-hosted instance; use --passwordfile
for unlock (more reliable with native binary); removed TOTP from
login flow (API key auth does not require it)
- provision-agent.py: pass BW_SERVER env var to BitwardenHelper
- docker-compose.yml: add BW_SERVER env var
- .env.example: add BW_SERVER, document TOTP as optional
Verified: dry-run passes, bw status/auth/generate all work inside
the provisioner container against pwvault.turnsys.com.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
bw login --apikey prompts for a TOTP code when 2FA is enabled on the
BW account. The previous code didn't pass one, so it would hang or
fail. Now generates a TOTP from BW_TOTP_SECRET and passes via --code.
Changes:
- BitwardenHelper.__init__ accepts totp_secret param
- login() generates a pyotp code and passes --code when secret is set
- provision-agent.py passes BW_TOTP_SECRET from environment
- docker-compose.yml and .env.example updated for the new var
- BW_PASSWORD removed from the login env (only needed for unlock via stdin)
The BW account's own TOTP secret lives in ~/.config/bw/env alongside
the other BW access info — the one exception (can't store BW's 2FA in
BW itself).
💘 Generated with Crush
Assisted-by: Crush:glm-5.2