feat: enable Cloudron TOTP 2FA for vp-techops (CMMC compliance)

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.
This commit is contained in:
TSYS Group COO
2026-08-13 21:01:22 -05:00
parent f633a10f80
commit be2f607839
6 changed files with 905 additions and 73 deletions
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""
verify-cloudron-2fa.py -- Verify 2FA round-trip with correct TOTP selector.
The Cloudron OIDC login uses #inputTotpToken (not #inputTotp).
Usage:
docker compose run --rm --entrypoint python3 provision verify-cloudron-2fa.py
"""
import os, sys, time
from pathlib import Path
from playwright.sync_api import sync_playwright
sys.path.insert(0, os.path.dirname(__file__))
from bw_helper import BitwardenHelper
CLOUDRON_BASE = os.environ.get("CLOUDRON_BASE", "https://my.knownelement.com")
STATE_DIR = Path("/app/state")
BW_ITEM = "vp-techops Cloudron"
EMAIL = "tsgstaff-coo-vptechops@turnsys.com"
def main():
bw = BitwardenHelper(
client_id=os.environ["BW_CLIENTID"],
client_secret=os.environ["BW_CLIENTSECRET"],
password=os.environ["BW_PASSWORD"],
server_url=os.environ.get("BW_SERVER", ""),
)
bw.login()
password = bw.get_item_password(BW_ITEM)
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_context(viewport={"width": 1280, "height": 1024}).new_page()
print("=== Fresh login (should prompt for TOTP) ===")
page.goto(f"{CLOUDRON_BASE}/login.html", wait_until="networkidle", timeout=15000)
page.wait_for_timeout(2000)
page.wait_for_selector("#inputPassword", timeout=15000)
page.click("#inputUsername")
page.keyboard.type(EMAIL)
page.click("#inputPassword")
page.keyboard.type(password)
page.locator('[role="button"]:has-text("Log in")').first.click()
page.wait_for_timeout(3000)
# Check for TOTP token field (correct selector: #inputTotpToken)
totp_input = page.query_selector("#inputTotpToken")
if totp_input and totp_input.is_visible():
print(" TOTP prompt appeared! 2FA is confirmed working.")
totp_code = bw.get_totp(BW_ITEM)
print(f" Entering TOTP code: {totp_code}")
totp_input.click()
page.keyboard.type(totp_code)
page.locator("#totpTokenSubmitButton").click()
page.wait_for_timeout(5000)
print(f" Post-TOTP URL: {page.url}")
if "login" not in page.url.lower() and "openid" not in page.url.lower():
print(" FULL 2FA ROUND-TRIP VERIFIED!")
else:
body = page.evaluate("() => document.body.innerText.substring(0, 200)")
print(f" Still on login/OIDC page. Body: {body}")
else:
print(" WARNING: No TOTP prompt appeared")
print(f" URL: {page.url}")
page.screenshot(path=str(STATE_DIR / "verify-2fa-final.png"))
browser.close()
if __name__ == "__main__":
main()