295 lines
12 KiB
Python
Executable File
295 lines
12 KiB
Python
Executable File
#!/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}")
|