#!/usr/bin/env python3 """WEVIA SSO GUARDIAN + CACHE BUSTER Agent 1: SSO — monitors ALL Authentik flows, containers, nginx, cookies. Auto-fix. Agent 2: Cache — purges CF + browser cache headers + nginx cache. Prevents stale content. Cron: */10 """ import subprocess as sp,json,os,time from datetime import datetime LOG="/var/log/wevia-sso-cache.log" STATUS="/var/www/html/api/wevia-sso-guardian.json" ts=datetime.now() issues=[] fixes=[] def lg(m): l=f"[{ts.strftime('%H:%M')}] {m}";print(l,flush=True) with open(LOG,"a") as f:f.write(l+"\n") def cmd(c,t=10): try:return sp.run(c,shell=True,capture_output=True,text=True,timeout=t,errors='replace').stdout.strip() except:return "" def curl_code(url,t=5): try: r=sp.run(["curl","-sk","-o","/dev/null","-w","%{http_code}",url,"--max-time",str(t)], capture_output=True,text=True,timeout=t+3) return int(r.stdout.strip()) except:return 0 lg("="*50) lg("SSO GUARDIAN + CACHE BUSTER") # ═══════════════════════════════════════ # AGENT SSO GUARDIAN # ═══════════════════════════════════════ lg("═══ SSO GUARDIAN ═══") # 1. Authentik containers health for ct in ["authentik-server","authentik-worker","authentik-db","authentik-redis"]: status=cmd(f"docker inspect -f '{{{{.State.Status}}}}' {ct} 2>/dev/null") if "running" not in status: lg(f" ❌ {ct} DOWN — restarting") cmd(f"docker restart {ct}",30) time.sleep(5) status2=cmd(f"docker inspect -f '{{{{.State.Status}}}}' {ct} 2>/dev/null") if "running" in status2: fixes.append(f"Restarted {ct}") lg(f" ✅ {ct} restarted") else: issues.append(f"{ct} won't start") # 2. Outpost HTTPS health outpost=curl_code("https://127.0.0.1:9443/",3) if outpost not in [200,301,302,403]: issues.append(f"Outpost HTTPS: {outpost}") lg(f" ❌ Outpost HTTPS: {outpost}") cmd("docker restart authentik-server",20) fixes.append("Restarted authentik-server for outpost") # 3. ALL SSO domains — flow page loads # AUTO-DISCOVER all SSO domains from nginx (SYSTEMIC, not hardcoded) sso_domains=[] for nf in glob.glob("/etc/nginx/sites-enabled/*"): if os.path.isfile(nf): nc=open(nf).read() if "auth_request" in nc and "goauthentik" in nc: import re m=re.search(r'server_name\s+([a-z0-9.-]+)', nc) if m: dom=m.group(1) if dom != "weval-consulting.com": sso_domains.append(dom.replace(".weval-consulting.com","")) lg(f" Auto-discovered {len(sso_domains)} SSO domains: {sso_domains}") for dom in sso_domains: full=f"{dom}.weval-consulting.com" # Check flow page (real user path) try: r=sp.run(["curl","-sk","--max-time","8",f"https://{full}/if/flow/default-authentication-flow/"], capture_output=True,text=True,timeout=12) content=r.stdout has_fatal="api.context.404" in content or "Cannot read" in content or "Unexpected token" in content has_form="ak-flow" in content or "authentik" in content.lower() if has_fatal: issues.append(f"{full}: SSO flow ERROR") lg(f" ❌ {full}: SSO broken — checking nginx /api/v3/") # Auto-fix: check if /api/v3/ is in nginx config nginx=open(f"/etc/nginx/sites-enabled/{full}").read() if os.path.exists(f"/etc/nginx/sites-enabled/{full}") else "" if "/api/v3/" not in nginx: os.system(f"chattr -i /etc/nginx/sites-enabled/{full} 2>/dev/null") # Add /api/v3/ proxy if "location /flows/" in nginx: nginx=nginx.replace("location /flows/ {", "location /api/v3/ {\n proxy_pass http://127.0.0.1:9090;\n proxy_set_header Host $host;\n proxy_set_header X-Forwarded-Proto https;\n }\n location /flows/ {") open(f"/etc/nginx/sites-enabled/{full}","w").write(nginx) os.system(f"chattr +i /etc/nginx/sites-enabled/{full} 2>/dev/null") fixes.append(f"Added /api/v3/ to {full}") lg(f" 🔧 AUTO-FIX: /api/v3/ added to {full}") elif has_form: lg(f" ✅ {full}: SSO OK") except Exception as e: lg(f" ⚠️ {full}: {e}") # 4. Check callback + cookie on ALL SSO domains for dom in sso_domains: full=f"{dom}.weval-consulting.com" f=f"/etc/nginx/sites-enabled/{full}" if not os.path.exists(f):continue c=open(f).read() needs_fix=False os.system(f"chattr -i {f} 2>/dev/null") # Check callback if "/outpost.goauthentik.io/callback" not in c and "outpost.goauthentik.io" in c: cb='\n location /outpost.goauthentik.io/callback {\n proxy_pass http://127.0.0.1:9090/outpost.goauthentik.io/callback;\n proxy_redirect off;\n proxy_set_header Host $host;\n proxy_set_header X-Forwarded-Proto https;\n }\n' marker="location /outpost.goauthentik.io {" if marker in c: c=c.replace(marker,cb+"\n "+marker) needs_fix=True fixes.append(f"Added callback to {full}") # Check cookie forwarding if "auth_request_set" not in c and "auth_request" in c: c=c.replace( "auth_request /outpost.goauthentik.io/auth/nginx;", "auth_request /outpost.goauthentik.io/auth/nginx;\n auth_request_set $auth_cookie $upstream_http_set_cookie;\n add_header Set-Cookie $auth_cookie;") needs_fix=True fixes.append(f"Added cookie forwarding to {full}") if needs_fix: open(f,"w").write(c) lg(f" 🔧 AUTO-FIX: {full} nginx updated") os.system(f"chattr +i {f} 2>/dev/null") # SYSTEMIC: scan ALL nginx configs for missing SSO components lg(" SYSTEMIC SCAN: all nginx configs...") for nf in glob.glob("/etc/nginx/sites-enabled/*"): if not os.path.isfile(nf): continue nc = open(nf).read() if "auth_request" not in nc or "goauthentik" not in nc: continue fname = os.path.basename(nf) needs_fix = False os.system(f"chattr -i {nf} 2>/dev/null") # Check ALL required Authentik paths required = { "/api/v3/": "location /api/v3/ { proxy_pass http://127.0.0.1:9090; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; }", "/application/": "location /application/ { proxy_pass http://127.0.0.1:9090; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; }", "/outpost.goauthentik.io/callback": "location /outpost.goauthentik.io/callback { proxy_pass http://127.0.0.1:9090/outpost.goauthentik.io/callback; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; }", } for path, block in required.items(): if path not in nc and "location /flows/" in nc: nc = nc.replace("location /flows/ {", block + " location /flows/ {") needs_fix = True fixes.append(f"Added {path} to {fname}") lg(f" 🔧 {fname}: added {path}") # Check cookie forwarding if "auth_request_set" not in nc and "auth_request /outpost" in nc: nc = nc.replace( "auth_request /outpost.goauthentik.io/auth/nginx;", "auth_request /outpost.goauthentik.io/auth/nginx; auth_request_set $auth_cookie $upstream_http_set_cookie; add_header Set-Cookie $auth_cookie;") needs_fix = True fixes.append(f"Added cookie forwarding to {fname}") lg(f" 🔧 {fname}: added cookie forwarding") # Check static/media paths for sp2 in ["/static/(authentik|dist)/", "/media/"]: key = sp2.replace("(authentik|dist)", "authentik") if key not in nc and "location /flows/" in nc: block2 = f"location ~ ^{sp2} {{ proxy_pass http://127.0.0.1:9090; proxy_set_header Host $host; }}" if "~" in sp2 or "(" in sp2 else f"location {sp2} {{ proxy_pass http://127.0.0.1:9090; proxy_set_header Host $host; }}" # Only add if not duplicate if sp2.split("/")[1] not in nc: nc = nc.replace("location /flows/ {", block2 + " location /flows/ {") needs_fix = True if needs_fix: open(nf, "w").write(nc) os.system(f"chattr +i {nf} 2>/dev/null") # Reload nginx if fixes applied if fixes: test=cmd("nginx -t 2>&1") if "successful" in test: cmd("systemctl reload nginx") lg(" ✅ Nginx reloaded") else: lg(f" ❌ Nginx syntax error: {test[:50]}") issues.append("Nginx syntax error after fix") # ═══════════════════════════════════════ # AGENT CACHE BUSTER # ═══════════════════════════════════════ lg("═══ CACHE BUSTER ═══") # 1. Cloudflare cache purge try: secrets={} for line in open("/etc/weval/secrets.env"): if "=" in line and not line.startswith("#"): k,v=line.strip().split("=",1) secrets[k]=v cf_token=secrets.get("CF_API_TOKEN","") cf_zone="1488bbba251c6fa282999fcc09aac9fe" if cf_token: # Purge specific URLs that tend to get stale stale_urls=[ "https://weval-consulting.com/weval-faq-fix.js", "https://weval-consulting.com/weval-translate.js", "https://weval-consulting.com/index.html", "https://weval-consulting.com/", ] r=sp.run(["curl","-sf","-X","POST", f"https://api.cloudflare.com/client/v4/zones/{cf_zone}/purge_cache", "-H",f"Authorization: Bearer {cf_token}", "-H","Content-Type: application/json", "-d",json.dumps({"files":stale_urls}), "--max-time","10"],capture_output=True,text=True,timeout=15) try: d=json.loads(r.stdout) if d.get("success"): lg(f" ✅ CF purge: {len(stale_urls)} URLs") else: lg(f" ⚠️ CF purge: {d.get('errors',['?'])}") except: lg(f" ⚠️ CF purge: no response") except:lg(" ⚠️ CF: no token") # 2. Check cache-busting version in index.html try: idx=open("/var/www/html/index.html").read() import re m=re.search(r'faq-fix\.js\?v=(\d+)',idx) if m: ver=int(m.group(1)) lg(f" Cache version: v={ver}") else: lg(" ⚠️ No cache version found") except:pass # 3. Set proper cache headers in nginx for JS/CSS nginx_main="/etc/nginx/sites-enabled/weval-consulting" try: nc=open(nginx_main).read() if "Cache-Control" not in nc and "no-cache" not in nc: # Add cache headers for dynamic content lg(" ⚠️ No cache-control headers in nginx") except:pass # ═══ SAVE ═══ result={ "timestamp":ts.isoformat(), "sso":{"status":"GREEN" if not issues else "RED","issues":issues,"fixes":fixes, "domains_checked":len(sso_domains),"containers_ok":4-len([i for i in issues if "container" in i.lower()])}, "cache":{"cf_purge":True,"version":"v19"}, "total_fixes":len(fixes) } json.dump(result,open(STATUS,"w"),indent=2) lg(f"\n{'='*50}") lg(f"SSO: {'GREEN' if not issues else 'RED'} | Issues:{len(issues)} | Fixes:{len(fixes)}") lg(f"Cache: purged") lg(f"{'='*50}")