From e117051ff5f26984e8cc098e9bf5b168edc97229 Mon Sep 17 00:00:00 2001 From: filippo-ferrando Date: Tue, 31 Mar 2026 12:31:01 +0200 Subject: [PATCH 01/19] feat: session token test --- docker/Dockerfile | 3 +- docker/nginx.conf | 95 ++++++++++++++++++++++++++++++++++++++- docker/session_guard.py | 98 +++++++++++++++++++++++++++++++++++++++++ docker/startup.sh | 1 + 4 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 docker/session_guard.py diff --git a/docker/Dockerfile b/docker/Dockerfile index af1b1fa..7dd82a2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -38,11 +38,12 @@ COPY elemento-gui-new/ /usr/share/nginx/html/ COPY elemento-gui-new/electros/configs/atomosFlags.json /usr/share/nginx/html/electros/configs/flags.json COPY elemento-gui-new/electros/electrosOnAtomos.html /usr/share/nginx/html/electros/electros.html COPY docker/startup.sh /opt/app/startup.sh +COPY docker/session_guard.py /opt/app/session_guard.py COPY ./docker/follow_log.sh /opt/app/follow_log.sh COPY ./docker/logger_stream.sh /opt/app/logger_stream.sh RUN apt-get update -RUN apt-get install -y socat +RUN apt-get install -y socat python3 RUN rm -rf /var/cache/apt/archives /var/lib/apt/lists/* RUN chmod +x /opt/daemons/* diff --git a/docker/nginx.conf b/docker/nginx.conf index ebab9bc..163b591 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -41,6 +41,7 @@ http { set $network https://127.0.0.1:37777; set $services https://127.0.0.1:6777; set $targets https://127.0.0.1:57777; + set $sidecar http://127.0.0.1:9999; listen 443 ssl; listen [::]:443 ssl; @@ -53,61 +54,105 @@ http { root /usr/share/nginx/html; index electros/electros.html; + location = /_guard/validate { + internal; + proxy_pass $sidecar/validate; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header X-Session-Token $cookie_session_token; + } + + location = /_guard/issue { + internal; + proxy_pass $sidecar/issue; + proxy_method POST; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + } + location / { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; try_files $uri $uri/ =404; } location ~ ^/(.*)/favicon.ico$ { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; index electros.iconset/electros.ico; } # Static asset aliases location /js/ { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; alias /usr/share/nginx/html/electros/js/; } location /css/ { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; alias /usr/share/nginx/html/electros/css/; } location /pages/ { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; alias /usr/share/nginx/html/electros/pages/; } location /assets/ { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; alias /usr/share/nginx/html/electros/assets/; } location /Electros.svg { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; alias /usr/share/nginx/html/electros/Electros.svg; } location /configs/ { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; alias /usr/share/nginx/html/electros/configs/; } location /remotes/ { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; alias /usr/share/nginx/html/electros/remotes/; } location /favicon/ { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; alias /usr/share/nginx/html/electros/favicon/; } location /ecd/ { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; alias /usr/share/nginx/html/electros/ecd/; } location /epm/ { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; alias /usr/share/nginx/html/electros/epm/; } location /ist/ { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; alias /usr/share/nginx/html/electros/ist/; } # Daemon statuses handlers location /authStatus { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; proxy_pass $authenticate/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -115,6 +160,9 @@ http { } location /computeStatus { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; + proxy_pass $compute/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -122,6 +170,9 @@ http { } location /storageStatus { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; + proxy_pass $storage/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -129,6 +180,9 @@ http { } location /networksStatus { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; + proxy_pass $network/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -136,6 +190,9 @@ http { } location /servicesStatus { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; + proxy_pass $services/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -143,6 +200,9 @@ http { } location /targetsStatus { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; + proxy_pass $targets/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -151,13 +211,17 @@ http { # Daemon handlers location /api/v1/authenticate/ { - proxy_pass $authenticate; + proxy_pass $sidecar/login-proxy/api/v1/authenticate/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; + proxy_pass_request_body on; } location /api/v1.0/client/backups/ { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; + proxy_pass $compute; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -165,6 +229,9 @@ http { } location /api/v1.0/client/vm/ { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; + proxy_pass $compute; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -173,6 +240,9 @@ http { # special case for network attach/detach for a VM location ~ ^/api/v1.0/client/network/(attach|detach) { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; + proxy_pass $compute; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -180,6 +250,9 @@ http { } location /api/v1.0/client/volume/ { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; + proxy_pass $storage; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -187,6 +260,9 @@ http { } location /api/v1.0/client/network/ { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; + proxy_pass $network; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -194,6 +270,9 @@ http { } location /api/v1.0/service/ { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; + proxy_pass $services; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -201,6 +280,9 @@ http { } location /api/v1.0/client/target/ { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; + proxy_pass $targets; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -209,6 +291,9 @@ http { # WebSocket passthrough location /api/v1.0/client/vnc/59441 { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; + proxy_pass https://127.0.0.1:59441/; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; @@ -218,9 +303,17 @@ http { } location /api/logs { + auth_request /_guard/validate; + error_page 401 403 = @unauthorized; + proxy_pass https://localhost:5142; proxy_buffering off; proxy_cache off; } + + location @unauthorized { + default_type application/json; + return 401 '{"error":"unauthorized","message":"No active session"}'; + } } } diff --git a/docker/session_guard.py b/docker/session_guard.py new file mode 100644 index 0000000..2a0541c --- /dev/null +++ b/docker/session_guard.py @@ -0,0 +1,98 @@ +from http.server import HTTPServer, BaseHTTPRequestHandler +import secrets +import threading +import urllib.request +import urllib.error + +_token = None +_lock = threading.Lock() + +AUTH_DAEMON = "https://127.0.0.1:47777" +VERIFY_SSL = False # daemon uses self-signed cert + + +class Handler(BaseHTTPRequestHandler): + def log_message(self, *a): + pass + + # ── validate: called by nginx auth_request ────────────────── + def do_GET(self): + if self.path != "/validate": + self._send(404) + return + incoming = self.headers.get("X-Session-Token", "") + with _lock: + ok = bool(_token) and secrets.compare_digest(_token, incoming) + self._send(200 if ok else 401) + + def do_POST(self): + # ── login proxy: forward to auth daemon, issue token on 200 + if self.path.startswith("/login-proxy"): + self._handle_login() + else: + self._send(404) + + def _handle_login(self): + import ssl + + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + + # Reconstruct the target path: + # /login-proxy/api/v1/authenticate/... → /api/v1/authenticate/... + target_path = self.path.replace("/login-proxy", "", 1) + target_url = AUTH_DAEMON + target_path + + length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(length) if length else b"" + + # Forward headers, strip hop-by-hop + skip = {"host", "connection", "transfer-encoding", "content-length"} + fwd_headers = {k: v for k, v in self.headers.items() if k.lower() not in skip} + if body: + fwd_headers["Content-Length"] = str(len(body)) + + req = urllib.request.Request( + target_url, data=body or None, headers=fwd_headers, method="POST" + ) + try: + resp = urllib.request.urlopen(req, context=ctx, timeout=10) + status = resp.status + resp_body = resp.read() + resp_headers = dict(resp.headers) + except urllib.error.HTTPError as e: + status = e.code + resp_body = e.read() + resp_headers = dict(e.headers) + + # Issue a new token only on successful login + cookie_header = None + if status == 200: + tok = secrets.token_urlsafe(32) + with _lock: + global _token + _token = tok + cookie_header = ( + f"session_token={tok}; Path=/; HttpOnly; Secure; SameSite=Lax" + ) + + # Write response back to nginx + self.send_response(status) + for k, v in resp_headers.items(): + if k.lower() in ("connection", "transfer-encoding", "set-cookie"): + continue + self.send_header(k, v) + if cookie_header: + self.send_header("Set-Cookie", cookie_header) + self.send_header("Content-Length", str(len(resp_body))) + self.end_headers() + self.wfile.write(resp_body) + + def _send(self, code): + self.send_response(code) + self.end_headers() + + +HTTPServer(("127.0.0.1", 9999), Handler).serve_forever() + diff --git a/docker/startup.sh b/docker/startup.sh index 57dc196..19ebe71 100644 --- a/docker/startup.sh +++ b/docker/startup.sh @@ -29,6 +29,7 @@ /opt/daemons/elemento_daemons_linux_x86 --cert /certs/atomos.crt --key /certs/atomos.key >/var/log/elemento/elemento_daemons.log 2>&1 & /opt/app/logger_stream.sh >/dev/null & +python3 /opt/app/session_guard.py >/dev/null & # run the project From 978a9b5e58822b91cc9b1736270053a01988baa9 Mon Sep 17 00:00:00 2001 From: filippo-ferrando Date: Tue, 31 Mar 2026 12:49:51 +0200 Subject: [PATCH 02/19] feat: added readme --- docker/nginx.conf | 8 ------ docker/readme.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 docker/readme.md diff --git a/docker/nginx.conf b/docker/nginx.conf index 163b591..ff09041 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -62,14 +62,6 @@ http { proxy_set_header X-Session-Token $cookie_session_token; } - location = /_guard/issue { - internal; - proxy_pass $sidecar/issue; - proxy_method POST; - proxy_pass_request_body off; - proxy_set_header Content-Length ""; - } - location / { auth_request /_guard/validate; error_page 401 403 = @unauthorized; diff --git a/docker/readme.md b/docker/readme.md new file mode 100644 index 0000000..fba71f6 --- /dev/null +++ b/docker/readme.md @@ -0,0 +1,70 @@ +# Session Guard — Container-level Session Enforcement + +## What problem does this solve? + +When a user spawns a container, the container is accessible at a known port on the host. +Anyone who discovers that port can access the container directly, bypassing the Flask app entirely. +Additionally, if a second user logs in to an already active container, there is no mechanism +to invalidate the first user's session. + +This solution enforces sessions **inside the container itself**, so it doesn't matter how a +request reaches it, the container will always reject unauthenticated or stale requests. + +--- + +## How it works + +Two small additions to the container: + +### 1. The sidecar (`session_guard.py`) + +A minimal Python HTTP server that runs on `127.0.0.1:9999` inside the container. +It is never reachable from outside — it only talks to the container nginx internally. + +It does two things: + +- **Acts as a login proxy** — when a user logs in, the container nginx routes the login + request through the sidecar instead of directly to the auth daemon. The sidecar forwards + the request, and if the auth daemon returns a success, it generates a secure random token, + stores it in memory, and injects a `Set-Cookie` header into the response before returning + it to the browser. + +- **Validates sessions** — on every request to any protected resource, the container nginx + calls the sidecar's `/validate` endpoint as a subrequest. The sidecar compares the token + from the browser's cookie against the one stored in memory. If they match, the request + proceeds. If not, the container nginx blocks it with a `401`. + +### 2. The container nginx + +The container nginx is updated to: + +- Route `/api/v1/authenticate/` through the sidecar (login proxy) instead of the auth + daemon directly. +- Add an `auth_request` check to every other location, pointing to the sidecar's + `/validate` endpoint. +- Return a `401` JSON response for any request that fails validation. + +--- + +## Security guarantees + +| Scenario | Result | +|---|---| +| User knows the port and bypasses Flask | Container nginx fires `auth_request` → no cookie → `401` blocked | +| User tries to access an active container without logging in | Same as above — `401` | +| A second user logs in to an already active container | Sidecar overwrites the stored token — first user's cookie immediately becomes invalid | +| User tampers with their cookie | `secrets.compare_digest` rejects any value that doesn't match exactly | +| Fresh container, nobody logged in yet | `_token` is `None` — every request returns `401` until a real login occurs | + +The host Flask app and host nginx require **zero changes**. The host nginx continues to +proxy to the container port as before. The enforcement is entirely internal to the container. + +--- + +## What does NOT change + +- The host nginx configuration +- The Flask application +- The internal auth daemon or any other daemon inside the container +- Container port mappings +- Any other part of the existing infrastructure From 0ef89962063c7daaff78ec69696d82e0edc24be1 Mon Sep 17 00:00:00 2001 From: filippo-ferrando Date: Tue, 31 Mar 2026 13:09:08 +0200 Subject: [PATCH 03/19] fix: addressed issue con nginx config --- docker/nginx.conf | 137 +++++++++++++++++++++++----------------------- 1 file changed, 67 insertions(+), 70 deletions(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index ff09041..9c222fe 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -54,7 +54,10 @@ http { root /usr/share/nginx/html; index electros/electros.html; + auth_request /_guard/validate; + location = /_guard/validate { + auth_request off; internal; proxy_pass $sidecar/validate; proxy_pass_request_body off; @@ -63,88 +66,88 @@ http { } location / { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; + error_page 401 = @unauthorized; + error_page 403 = @forbidden; try_files $uri $uri/ =404; } location ~ ^/(.*)/favicon.ico$ { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; + error_page 401 = @unauthorized; + error_page 403 = @forbidden; index electros.iconset/electros.ico; } # Static asset aliases location /js/ { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; + error_page 401 = @unauthorized; + error_page 403 = @forbidden; alias /usr/share/nginx/html/electros/js/; } location /css/ { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; + error_page 401 = @unauthorized; + error_page 403 = @forbidden; alias /usr/share/nginx/html/electros/css/; } location /pages/ { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; + error_page 401 = @unauthorized; + error_page 403 = @forbidden; alias /usr/share/nginx/html/electros/pages/; } location /assets/ { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; + error_page 401 = @unauthorized; + error_page 403 = @forbidden; alias /usr/share/nginx/html/electros/assets/; } location /Electros.svg { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; + error_page 401 = @unauthorized; + error_page 403 = @forbidden; alias /usr/share/nginx/html/electros/Electros.svg; } location /configs/ { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; + error_page 401 = @unauthorized; + error_page 403 = @forbidden; alias /usr/share/nginx/html/electros/configs/; } location /remotes/ { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; + error_page 401 = @unauthorized; + error_page 403 = @forbidden; alias /usr/share/nginx/html/electros/remotes/; } location /favicon/ { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; + error_page 401 = @unauthorized; + error_page 403 = @forbidden; alias /usr/share/nginx/html/electros/favicon/; } location /ecd/ { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; + error_page 401 = @unauthorized; + error_page 403 = @forbidden; alias /usr/share/nginx/html/electros/ecd/; } location /epm/ { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; + error_page 401 = @unauthorized; + error_page 403 = @forbidden; alias /usr/share/nginx/html/electros/epm/; } location /ist/ { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; + error_page 401 = @unauthorized; + error_page 403 = @forbidden; alias /usr/share/nginx/html/electros/ist/; } # Daemon statuses handlers location /authStatus { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; + error_page 401 = @unauthorized; + error_page 403 = @forbidden; proxy_pass $authenticate/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -152,9 +155,8 @@ http { } location /computeStatus { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; - + error_page 401 = @unauthorized; + error_page 403 = @forbidden; proxy_pass $compute/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -162,9 +164,8 @@ http { } location /storageStatus { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; - + error_page 401 = @unauthorized; + error_page 403 = @forbidden; proxy_pass $storage/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -172,9 +173,8 @@ http { } location /networksStatus { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; - + error_page 401 = @unauthorized; + error_page 403 = @forbidden; proxy_pass $network/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -182,9 +182,8 @@ http { } location /servicesStatus { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; - + error_page 401 = @unauthorized; + error_page 403 = @forbidden; proxy_pass $services/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -192,9 +191,8 @@ http { } location /targetsStatus { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; - + error_page 401 = @unauthorized; + error_page 403 = @forbidden; proxy_pass $targets/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -203,7 +201,10 @@ http { # Daemon handlers location /api/v1/authenticate/ { + auth_request off; proxy_pass $sidecar/login-proxy/api/v1/authenticate/; + error_page 401 = @unauthorized; + error_page 403 = @forbidden; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; @@ -211,9 +212,8 @@ http { } location /api/v1.0/client/backups/ { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; - + error_page 401 = @unauthorized; + error_page 403 = @forbidden; proxy_pass $compute; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -221,9 +221,8 @@ http { } location /api/v1.0/client/vm/ { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; - + error_page 401 = @unauthorized; + error_page 403 = @forbidden; proxy_pass $compute; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -232,9 +231,8 @@ http { # special case for network attach/detach for a VM location ~ ^/api/v1.0/client/network/(attach|detach) { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; - + error_page 401 = @unauthorized; + error_page 403 = @forbidden; proxy_pass $compute; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -242,9 +240,8 @@ http { } location /api/v1.0/client/volume/ { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; - + error_page 401 = @unauthorized; + error_page 403 = @forbidden; proxy_pass $storage; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -252,9 +249,8 @@ http { } location /api/v1.0/client/network/ { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; - + error_page 401 = @unauthorized; + error_page 403 = @forbidden; proxy_pass $network; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -262,9 +258,8 @@ http { } location /api/v1.0/service/ { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; - + error_page 401 = @unauthorized; + error_page 403 = @forbidden; proxy_pass $services; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -272,9 +267,8 @@ http { } location /api/v1.0/client/target/ { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; - + error_page 401 = @unauthorized; + error_page 403 = @forbidden; proxy_pass $targets; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; @@ -283,9 +277,8 @@ http { # WebSocket passthrough location /api/v1.0/client/vnc/59441 { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; - + error_page 401 = @unauthorized; + error_page 403 = @forbidden; proxy_pass https://127.0.0.1:59441/; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; @@ -295,9 +288,8 @@ http { } location /api/logs { - auth_request /_guard/validate; - error_page 401 403 = @unauthorized; - + error_page 401 = @unauthorized; + error_page 403 = @forbidden; proxy_pass https://localhost:5142; proxy_buffering off; proxy_cache off; @@ -307,5 +299,10 @@ http { default_type application/json; return 401 '{"error":"unauthorized","message":"No active session"}'; } + + location @forbidden { + default_type application/json; + return 403 '{"error":"forbidden","message":"Forbidden request"}' + } } } From 3e557b9ebfdfdff83415209c8221b5b2628ae357 Mon Sep 17 00:00:00 2001 From: filippo-ferrando Date: Tue, 31 Mar 2026 13:36:01 +0200 Subject: [PATCH 04/19] fix: updates - pointed version (socat, python3) in dockerfile - updated readme (errors returned are `401` and `403` --- docker/Dockerfile | 4 +++- docker/readme.md | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 7dd82a2..fb84cf1 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -43,7 +43,9 @@ COPY ./docker/follow_log.sh /opt/app/follow_log.sh COPY ./docker/logger_stream.sh /opt/app/logger_stream.sh RUN apt-get update -RUN apt-get install -y socat python3 +RUN apt-get install -y socat=1.8.0.3-1 \ + python3=3.13.5-1 + RUN rm -rf /var/cache/apt/archives /var/lib/apt/lists/* RUN chmod +x /opt/daemons/* diff --git a/docker/readme.md b/docker/readme.md index fba71f6..7a17f1a 100644 --- a/docker/readme.md +++ b/docker/readme.md @@ -42,7 +42,7 @@ The container nginx is updated to: daemon directly. - Add an `auth_request` check to every other location, pointing to the sidecar's `/validate` endpoint. -- Return a `401` JSON response for any request that fails validation. +- Return a `401` or `403` JSON response for any request that fails validation. --- From 751ad120b11504c4b20d21d779bb1f401a1b3bc4 Mon Sep 17 00:00:00 2001 From: filippo-ferrando Date: Tue, 31 Mar 2026 16:29:52 +0200 Subject: [PATCH 05/19] fix: added local login support --- docker/nginx.conf | 6 ++++++ docker/session_guard.py | 47 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index 9c222fe..ae30629 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -275,6 +275,12 @@ http { proxy_set_header X-Forwarded-Port $server_port; } + location /api/v1.0/local_login { + auth_request off; + proxy_pass $sidecar/local-login; + proxy_pass_request_body on; + } + # WebSocket passthrough location /api/v1.0/client/vnc/59441 { error_page 401 = @unauthorized; diff --git a/docker/session_guard.py b/docker/session_guard.py index 2a0541c..a1ee2e5 100644 --- a/docker/session_guard.py +++ b/docker/session_guard.py @@ -3,11 +3,14 @@ import threading import urllib.request import urllib.error +import os +import ssl _token = None _lock = threading.Lock() AUTH_DAEMON = "https://127.0.0.1:47777" +FLASK_HOST = os.environ.get("PROXIMA_FLASK_HOST", "https://10.88.0.1:7781") VERIFY_SSL = False # daemon uses self-signed cert @@ -26,12 +29,53 @@ def do_GET(self): self._send(200 if ok else 401) def do_POST(self): - # ── login proxy: forward to auth daemon, issue token on 200 if self.path.startswith("/login-proxy"): self._handle_login() + elif self.path == "/local-login": + self._handle_local_login() else: self._send(404) + def _handle_local_login(self): + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + + length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(length) if length else b"" + + req = urllib.request.Request( + f"{FLASK_HOST}/api/v1.0/local_login", + data=body, + headers={"Content-Type": "application/json"}, + method="POST", + ) + try: + resp = urllib.request.urlopen(req, context=ctx, timeout=10) + status = resp.status + resp_body = resp.read() + except urllib.error.HTTPError as e: + status = e.code + resp_body = e.read() + + cookie_header = None + if status == 200: + tok = secrets.token_urlsafe(32) + with _lock: + global _token + _token = tok + cookie_header = ( + f"session_token={tok}; Path=/; HttpOnly; Secure; SameSite=Lax" + ) + + self.send_response(status) + self.send_header("Content-Type", "application/json") + if cookie_header: + self.send_header("Set-Cookie", cookie_header) + self.send_header("Content-Length", str(len(resp_body))) + self.end_headers() + self.wfile.write(resp_body) + def _handle_login(self): import ssl @@ -95,4 +139,3 @@ def _send(self, code): HTTPServer(("127.0.0.1", 9999), Handler).serve_forever() - From 75a2988015e78d22448196be1bf544108168ebca Mon Sep 17 00:00:00 2001 From: filippo-ferrando Date: Tue, 31 Mar 2026 16:51:54 +0200 Subject: [PATCH 06/19] feat: AtomOS GUI docker image build step added to action --- .github/workflows/develop.yml | 32 ++++++++++++++++++++++++++++++ .github/workflows/docker-build.yml | 6 ++---- .github/workflows/main.yml | 32 ++++++++++++++++++++++++++++++ .github/workflows/nightly.yml | 32 ++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 4 deletions(-) diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 1ce1b10..f7d489d 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -412,3 +412,35 @@ jobs: gh release upload ${{ needs.create-release.outputs.tag_name }} combined_checksums.txt --clobber env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + build-and-push: + name: Build AtomOS-GUI docker image + runs-on: ubuntu-latest + steps: + + # build and push the docker image to ghcr.io + + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.CI_TOKEN }} + submodules: recursive + ref: ${{ github.ref }} + + - name: Populate daemons folder + run: | + export CI_TOKEN=${{ secrets.CI_TOKEN }} + ./populate_daemons.sh --platform linux --arch x64 --develop + + - name: Login to GHCR + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.REPO_KEY }} + + - name: Build and push Docker image + run: | + OWNER_NAME=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') + docker build -f docker/Dockerfile -t ghcr.io/${OWNER_NAME}/elemento-electros-instance:beta . + docker push ghcr.io/${OWNER_NAME}/elemento-electros-instance:beta diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 2c3bc66..9d41155 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -1,8 +1,6 @@ name: Build and Push Electros docker image on: - push: - branches: [ develop ] workflow_dispatch: permissions: @@ -38,5 +36,5 @@ jobs: - name: Build and push Docker image run: | OWNER_NAME=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') - docker build -f docker/Dockerfile -t ghcr.io/${OWNER_NAME}/elemento-electros-instance:latest . - docker push ghcr.io/${OWNER_NAME}/elemento-electros-instance:latest + docker build -f docker/Dockerfile -t ghcr.io/${OWNER_NAME}/elemento-electros-instance:dev . + docker push ghcr.io/${OWNER_NAME}/elemento-electros-instance:dev diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 10f8708..55e6166 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -412,3 +412,35 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ github.token }} + + build-and-push: + name: Build AtomOS-GUI docker image + runs-on: ubuntu-latest + steps: + + # build and push the docker image to ghcr.io + + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.CI_TOKEN }} + submodules: recursive + ref: ${{ github.ref }} + + - name: Populate daemons folder + run: | + export CI_TOKEN=${{ secrets.CI_TOKEN }} + ./populate_daemons.sh --platform linux --arch x64 --develop + + - name: Login to GHCR + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.REPO_KEY }} + + - name: Build and push Docker image + run: | + OWNER_NAME=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') + docker build -f docker/Dockerfile -t ghcr.io/${OWNER_NAME}/elemento-electros-instance:latest . + docker push ghcr.io/${OWNER_NAME}/elemento-electros-instance:latest diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 7c40b8d..f959c43 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -412,3 +412,35 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ github.token }} + + build-and-push: + name: Build AtomOS-GUI docker image + runs-on: ubuntu-latest + steps: + + # build and push the docker image to ghcr.io + + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.CI_TOKEN }} + submodules: recursive + ref: ${{ github.ref }} + + - name: Populate daemons folder + run: | + export CI_TOKEN=${{ secrets.CI_TOKEN }} + ./populate_daemons.sh --platform linux --arch x64 --develop + + - name: Login to GHCR + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.REPO_KEY }} + + - name: Build and push Docker image + run: | + OWNER_NAME=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') + docker build -f docker/Dockerfile -t ghcr.io/${OWNER_NAME}/elemento-electros-instance:nightly . + docker push ghcr.io/${OWNER_NAME}/elemento-electros-instance:nightly From d92b14a9ea6b0bf0783b7aa73bead1496b954c89 Mon Sep 17 00:00:00 2001 From: filippo-ferrando Date: Tue, 31 Mar 2026 17:03:48 +0200 Subject: [PATCH 07/19] fix: unlock static asset from auth --- docker/nginx.conf | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docker/nginx.conf b/docker/nginx.conf index ae30629..8cc75a9 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -66,12 +66,14 @@ http { } location / { + auth_request off; error_page 401 = @unauthorized; error_page 403 = @forbidden; try_files $uri $uri/ =404; } location ~ ^/(.*)/favicon.ico$ { + auth_request off; error_page 401 = @unauthorized; error_page 403 = @forbidden; index electros.iconset/electros.ico; @@ -79,66 +81,77 @@ http { # Static asset aliases location /js/ { + auth_request off; error_page 401 = @unauthorized; error_page 403 = @forbidden; alias /usr/share/nginx/html/electros/js/; } location /css/ { + auth_request off; error_page 401 = @unauthorized; error_page 403 = @forbidden; alias /usr/share/nginx/html/electros/css/; } location /pages/ { + auth_request off; error_page 401 = @unauthorized; error_page 403 = @forbidden; alias /usr/share/nginx/html/electros/pages/; } location /assets/ { + auth_request off; error_page 401 = @unauthorized; error_page 403 = @forbidden; alias /usr/share/nginx/html/electros/assets/; } location /Electros.svg { + auth_request off; error_page 401 = @unauthorized; error_page 403 = @forbidden; alias /usr/share/nginx/html/electros/Electros.svg; } location /configs/ { + auth_request off; error_page 401 = @unauthorized; error_page 403 = @forbidden; alias /usr/share/nginx/html/electros/configs/; } location /remotes/ { + auth_request off; error_page 401 = @unauthorized; error_page 403 = @forbidden; alias /usr/share/nginx/html/electros/remotes/; } location /favicon/ { + auth_request off; error_page 401 = @unauthorized; error_page 403 = @forbidden; alias /usr/share/nginx/html/electros/favicon/; } location /ecd/ { + auth_request off; error_page 401 = @unauthorized; error_page 403 = @forbidden; alias /usr/share/nginx/html/electros/ecd/; } location /epm/ { + auth_request off; error_page 401 = @unauthorized; error_page 403 = @forbidden; alias /usr/share/nginx/html/electros/epm/; } location /ist/ { + auth_request off; error_page 401 = @unauthorized; error_page 403 = @forbidden; alias /usr/share/nginx/html/electros/ist/; @@ -146,6 +159,7 @@ http { # Daemon statuses handlers location /authStatus { + auth_request off; error_page 401 = @unauthorized; error_page 403 = @forbidden; proxy_pass $authenticate/; From 064ac9b3fc8e5fcecf16c7acc64a9f2d7b50d7ff Mon Sep 17 00:00:00 2001 From: filippo-ferrando Date: Tue, 31 Mar 2026 18:18:52 +0200 Subject: [PATCH 08/19] fix: typos in config --- docker/nginx.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index 8cc75a9..49981c0 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -216,7 +216,7 @@ http { # Daemon handlers location /api/v1/authenticate/ { auth_request off; - proxy_pass $sidecar/login-proxy/api/v1/authenticate/; + proxy_pass $sidecar/login-proxy/api/v1/authenticate; error_page 401 = @unauthorized; error_page 403 = @forbidden; proxy_set_header Host $host; @@ -322,7 +322,7 @@ http { location @forbidden { default_type application/json; - return 403 '{"error":"forbidden","message":"Forbidden request"}' + return 403 '{"error":"forbidden","message":"Forbidden request"}'; } } } From a7b6ba4547bcd2ce4a719cc421f89bee1865453d Mon Sep 17 00:00:00 2001 From: filippo-ferrando Date: Tue, 31 Mar 2026 19:56:00 +0200 Subject: [PATCH 09/19] fix: req are GET not POST --- docker/session_guard.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/docker/session_guard.py b/docker/session_guard.py index a1ee2e5..cefa7ce 100644 --- a/docker/session_guard.py +++ b/docker/session_guard.py @@ -20,15 +20,11 @@ def log_message(self, *a): # ── validate: called by nginx auth_request ────────────────── def do_GET(self): - if self.path != "/validate": - self._send(404) - return - incoming = self.headers.get("X-Session-Token", "") - with _lock: - ok = bool(_token) and secrets.compare_digest(_token, incoming) - self._send(200 if ok else 401) - - def do_POST(self): + if self.path == "/validate": + incoming = self.headers.get("X-Session-Token", "") + with _lock: + ok = bool(_token) and secrets.compare_digest(_token, incoming) + self._send(200 if ok else 401) if self.path.startswith("/login-proxy"): self._handle_login() elif self.path == "/local-login": @@ -77,8 +73,6 @@ def _handle_local_login(self): self.wfile.write(resp_body) def _handle_login(self): - import ssl - ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE From 6f16c41b7c76ff829fdb38574782825f6aa69c3e Mon Sep 17 00:00:00 2001 From: filippo-ferrando Date: Wed, 1 Apr 2026 00:56:13 +0200 Subject: [PATCH 10/19] multiple stuff - fix: authclient responds - fix: nginx correct forwards to sidecar - todo: add logout (remove session tokens) --- docker/nginx.conf | 36 +++++++++++++++++++++++++++++++----- docker/session_guard.py | 25 +++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index 49981c0..a048313 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -25,15 +25,13 @@ # #- Simone Robaldo (srobaldo at elemento.cloud) # # #------------------------------------------------------------------------------# # - - events {} http { include /etc/nginx/mime.types; # important default_type application/json; - + error_log /var/log/nginx/error.log info; server { set $authenticate https://127.0.0.1:47777; set $compute https://127.0.0.1:17777; @@ -169,6 +167,7 @@ http { } location /computeStatus { + #auth_request off; error_page 401 = @unauthorized; error_page 403 = @forbidden; proxy_pass $compute/; @@ -178,6 +177,7 @@ http { } location /storageStatus { + #auth_request off; error_page 401 = @unauthorized; error_page 403 = @forbidden; proxy_pass $storage/; @@ -187,15 +187,18 @@ http { } location /networksStatus { + #auth_request off; error_page 401 = @unauthorized; error_page 403 = @forbidden; proxy_pass $network/; + add_header X-debug "$http_authorization" always; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; } location /servicesStatus { + #auth_request off; error_page 401 = @unauthorized; error_page 403 = @forbidden; proxy_pass $services/; @@ -205,6 +208,7 @@ http { } location /targetsStatus { + #auth_request off; error_page 401 = @unauthorized; error_page 403 = @forbidden; proxy_pass $targets/; @@ -214,9 +218,31 @@ http { } # Daemon handlers - location /api/v1/authenticate/ { + location /api/v1/authenticate/login { + auth_request off; + proxy_pass $sidecar/login-proxy/api/v1/authenticate/login; + error_page 401 = @unauthorized; + error_page 403 = @forbidden; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; + proxy_pass_request_body on; + } + + location /api/v1/authenticate/logout { + auth_request off; + proxy_pass $sidecar/login-proxy/api/v1/authenticate/logout; + error_page 401 = @unauthorized; + error_page 403 = @forbidden; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; + proxy_pass_request_body on; + } + + location ~ ^/api/v1/authenticate/(.*)?$ { auth_request off; - proxy_pass $sidecar/login-proxy/api/v1/authenticate; + proxy_pass $authenticate; error_page 401 = @unauthorized; error_page 403 = @forbidden; proxy_set_header Host $host; diff --git a/docker/session_guard.py b/docker/session_guard.py index cefa7ce..b16ab3d 100644 --- a/docker/session_guard.py +++ b/docker/session_guard.py @@ -20,18 +20,29 @@ def log_message(self, *a): # ── validate: called by nginx auth_request ────────────────── def do_GET(self): + print(self.path) if self.path == "/validate": incoming = self.headers.get("X-Session-Token", "") with _lock: ok = bool(_token) and secrets.compare_digest(_token, incoming) self._send(200 if ok else 401) - if self.path.startswith("/login-proxy"): + + def do_POST(self): + if self.path == "/login-proxy/api/v1/authenticate/login": self._handle_login() elif self.path == "/local-login": self._handle_local_login() + elif self.path == "/login-proxy/api/v1/authenticate/logout": + self._handle_logout() else: self._send(404) + # TODO: implement + def _handle_logout(self): + # call authclient logout endpoint -> if nothing responds -> ok -> local account logout + # remove session token for both + pass + def _handle_local_login(self): ctx = ssl.create_default_context() ctx.check_hostname = False @@ -73,6 +84,8 @@ def _handle_local_login(self): self.wfile.write(resp_body) def _handle_login(self): + import ssl + ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE @@ -99,15 +112,23 @@ def _handle_login(self): status = resp.status resp_body = resp.read() resp_headers = dict(resp.headers) + print( + f"\n\n#################################\n\nresp: {resp}\nstatus: {status}\nresp_body: {resp_body}\nresp_headers: {resp_headers}\n\n#################################\n\n" + ) + except urllib.error.HTTPError as e: status = e.code resp_body = e.read() resp_headers = dict(e.headers) + print( + f"\n\n##EXCEPTION##\n\nstatus: {status}\nresp_body: {resp_body}\nresp_headers: {resp_headers}\n\n############################\n\n" + ) # Issue a new token only on successful login cookie_header = None if status == 200: tok = secrets.token_urlsafe(32) + print(f"\nSetting token: {tok}\n\n") with _lock: global _token _token = tok @@ -123,7 +144,7 @@ def _handle_login(self): self.send_header(k, v) if cookie_header: self.send_header("Set-Cookie", cookie_header) - self.send_header("Content-Length", str(len(resp_body))) + # self.send_header("Content-Length", str(len(resp_body))) self.end_headers() self.wfile.write(resp_body) From 9264a50d7edec99d03a29c460d4675044fe31420 Mon Sep 17 00:00:00 2001 From: filippo-ferrando Date: Wed, 1 Apr 2026 02:10:43 +0200 Subject: [PATCH 11/19] feat: added logout to sidecar --- docker/nginx.conf | 3 ++ docker/session_guard.py | 68 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index a048313..a1901be 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -318,6 +318,9 @@ http { location /api/v1.0/local_login { auth_request off; proxy_pass $sidecar/local-login; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; proxy_pass_request_body on; } diff --git a/docker/session_guard.py b/docker/session_guard.py index b16ab3d..b95c4ab 100644 --- a/docker/session_guard.py +++ b/docker/session_guard.py @@ -26,6 +26,8 @@ def do_GET(self): with _lock: ok = bool(_token) and secrets.compare_digest(_token, incoming) self._send(200 if ok else 401) + else: + self._send(404) def do_POST(self): if self.path == "/login-proxy/api/v1/authenticate/login": @@ -37,11 +39,65 @@ def do_POST(self): else: self._send(404) - # TODO: implement + # TODO: need elecros button in order to call it def _handle_logout(self): # call authclient logout endpoint -> if nothing responds -> ok -> local account logout # remove session token for both - pass + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + + # Reconstruct the target path: + # /login-proxy/api/v1/authenticate/... → /api/v1/authenticate/... + target_path = self.path.replace("/login-proxy", "", 1) + target_url = AUTH_DAEMON + target_path + + length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(length) if length else b"" + + # Forward headers, strip hop-by-hop + skip = {"host", "connection", "transfer-encoding", "content-length"} + fwd_headers = {k: v for k, v in self.headers.items() if k.lower() not in skip} + if body: + fwd_headers["Content-Length"] = str(len(body)) + + req = urllib.request.Request( + target_url, data=body or None, headers=fwd_headers, method="POST" + ) + try: + resp = urllib.request.urlopen(req, context=ctx, timeout=10) + status = resp.status + resp_body = resp.read() + resp_headers = dict(resp.headers) + print( + f"\n\n#################################\n\nresp: {resp}\nstatus: {status}\nresp_body: {resp_body}\nresp_headers: {resp_headers}\n\n#################################\n\n" + ) + + except urllib.error.HTTPError as e: + status = e.code + resp_body = e.read() + resp_headers = dict(e.headers) + print( + f"\n\n##EXCEPTION##\n\nstatus: {status}\nresp_body: {resp_body}\nresp_headers: {resp_headers}\n\n############################\n\n" + ) + # remove token on logout regardless of auth daemon response + with _lock: + global _token + _token = None + + expired_cookie = ( + "session_token=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0" + ) + + self.send_response(status) + for k, v in resp_headers.items(): + if k.lower() in ("connection", "transfer-encoding", "set-cookie"): + continue + self.send_header(k, v) + self.send_header("Set-Cookie", expired_cookie) + self.send_header("Content-Length", str(len(resp_body))) + self.end_headers() + self.wfile.write(resp_body) def _handle_local_login(self): ctx = ssl.create_default_context() @@ -65,9 +121,11 @@ def _handle_local_login(self): status = e.code resp_body = e.read() + # Issue a new token only on successful login cookie_header = None if status == 200: tok = secrets.token_urlsafe(32) + print(f"\nSetting token: {tok}\n\n") with _lock: global _token _token = tok @@ -75,17 +133,15 @@ def _handle_local_login(self): f"session_token={tok}; Path=/; HttpOnly; Secure; SameSite=Lax" ) + # Write response back to nginx self.send_response(status) - self.send_header("Content-Type", "application/json") if cookie_header: self.send_header("Set-Cookie", cookie_header) - self.send_header("Content-Length", str(len(resp_body))) + # self.send_header("Content-Length", str(len(resp_body))) self.end_headers() self.wfile.write(resp_body) def _handle_login(self): - import ssl - ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE From 9d0223bf1b65f76c32aecc083935bfc2806cc77e Mon Sep 17 00:00:00 2001 From: filippo-ferrando Date: Wed, 1 Apr 2026 12:12:59 +0200 Subject: [PATCH 12/19] fix: docs + licences --- docker/Dockerfile | 4 ++-- docker/arm_Dockerfile | 8 +++++--- docker/arm_startup.sh | 3 ++- docker/follow_log.sh | 28 ++++++++++++++++++++++++++-- docker/logger_stream.sh | 26 ++++++++++++++++++++++++++ docker/nginx.conf | 3 ++- docker/readme.md | 9 ++++++--- docker/session_guard.py | 41 ++++++++++++++++++++++++++++------------- docker/startup.sh | 2 +- 9 files changed, 98 insertions(+), 26 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index fb84cf1..2d972f5 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,5 @@ # #******************************************************************************# -# # Copyright(c) 2019-2023, Elemento srl, All rights reserved # +# # Copyright(c) 2019-2026, Elemento srl, All rights reserved # # # Author: Elemento srl # # # Contributors are mentioned in the code where appropriate. # # # Permission to use and modify this software and its documentation strictly # @@ -42,7 +42,7 @@ COPY docker/session_guard.py /opt/app/session_guard.py COPY ./docker/follow_log.sh /opt/app/follow_log.sh COPY ./docker/logger_stream.sh /opt/app/logger_stream.sh -RUN apt-get update +RUN apt-get update -y RUN apt-get install -y socat=1.8.0.3-1 \ python3=3.13.5-1 diff --git a/docker/arm_Dockerfile b/docker/arm_Dockerfile index 4782909..63c5864 100644 --- a/docker/arm_Dockerfile +++ b/docker/arm_Dockerfile @@ -39,11 +39,14 @@ COPY elemento-gui-new/ /usr/share/nginx/html/ COPY elemento-gui-new/electros/configs/atomosFlags.json /usr/share/nginx/html/electros/configs/flags.json COPY elemento-gui-new/electros/electrosOnAtomos.html /usr/share/nginx/html/electros/electros.html COPY ./docker/arm_startup.sh /opt/app/startup.sh +COPY ./docker/session_guard.py /opt/app/session_guard.py COPY ./docker/follow_log.sh /opt/app/follow_log.sh COPY ./docker/logger_stream.sh /opt/app/logger_stream.sh -RUN apt update -RUN apt install -y socat +RUN apt-get update -y +RUN apt install -y socat=1.8.0.3-1 \ + python3=3.13.5-1 + RUN rm -rf /var/cache/apt/archives /var/lib/apt/lists/* RUN chmod +x /opt/daemons/* @@ -53,7 +56,6 @@ RUN mkdir -p /var/log/elemento RUN ls -lah /usr/share/nginx/html/ -EXPOSE 80 EXPOSE 443 ENTRYPOINT [ "/opt/app/startup.sh" ] diff --git a/docker/arm_startup.sh b/docker/arm_startup.sh index a127df4..3a51d80 100644 --- a/docker/arm_startup.sh +++ b/docker/arm_startup.sh @@ -1,6 +1,6 @@ #! /bin/bash # #******************************************************************************# -# # Copyright(c) 2019-2023, Elemento srl, All rights reserved # +# # Copyright(c) 2019-2026, Elemento srl, All rights reserved # # # Author: Elemento srl # # # Contributors are mentioned in the code where appropriate. # # # Permission to use and modify this software and its documentation strictly # @@ -32,6 +32,7 @@ #/opt/daemons/Elemento_Daemons_linux_x86 > /var/log/elemento/elemento_daemons.log 2>&1 & /opt/daemons/elemento_daemons_linux_arm >/var/log/elemento/elemento_daemons.log 2>&1 & /opt/app/logger_stream.sh >/dev/null & +python3 /opt/app/session_guard.py >/dev/null & # run the project diff --git a/docker/follow_log.sh b/docker/follow_log.sh index 2072706..293779a 100755 --- a/docker/follow_log.sh +++ b/docker/follow_log.sh @@ -1,4 +1,30 @@ #!/bin/bash +# #******************************************************************************# +# # Copyright(c) 2019-2026, Elemento srl, All rights reserved # +# # Author: Elemento srl # +# # Contributors are mentioned in the code where appropriate. # +# # Permission to use and modify this software and its documentation strictly # +# # for personal purposes is hereby granted without fee, # +# # provided that the above copyright notice appears in all copies # +# # and that both the copyright notice and this permission notice appear in the # +# # supporting documentation. # +# # Modifications to this work are allowed for personal use. # +# # Such modifications have to be licensed under a # +# # Creative Commons BY-NC-ND 4.0 International License available at # +# # http://creativecommons.org/licenses/by-nc-nd/4.0/ and have to be made # +# # available to the Elemento user community # +# # through the original distribution channels. # +# # The authors make no claims about the suitability # +# # of this software for any purpose. # +# # It is provided "as is" without express or implied warranty. # +# #******************************************************************************# +# +# #------------------------------------------------------------------------------# +# #Electros # +# #Authors: # +# #- Simone Robaldo (srobaldo at elemento.cloud) # +# #------------------------------------------------------------------------------# +# LOG_FILE=/var/log/elemento/elemento_daemons.log @@ -31,5 +57,3 @@ tail -n 0 -F "$LOG_FILE" | while IFS= read line; do len=$(printf "%s" "$chunk" | wc -c) printf "%x\r\n%s\r\n" "$len" "$chunk" || break done - - diff --git a/docker/logger_stream.sh b/docker/logger_stream.sh index 2ff3e4f..f4c5386 100755 --- a/docker/logger_stream.sh +++ b/docker/logger_stream.sh @@ -1,4 +1,30 @@ #!/bin/sh +# #******************************************************************************# +# # Copyright(c) 2019-2026, Elemento srl, All rights reserved # +# # Author: Elemento srl # +# # Contributors are mentioned in the code where appropriate. # +# # Permission to use and modify this software and its documentation strictly # +# # for personal purposes is hereby granted without fee, # +# # provided that the above copyright notice appears in all copies # +# # and that both the copyright notice and this permission notice appear in the # +# # supporting documentation. # +# # Modifications to this work are allowed for personal use. # +# # Such modifications have to be licensed under a # +# # Creative Commons BY-NC-ND 4.0 International License available at # +# # http://creativecommons.org/licenses/by-nc-nd/4.0/ and have to be made # +# # available to the Elemento user community # +# # through the original distribution channels. # +# # The authors make no claims about the suitability # +# # of this software for any purpose. # +# # It is provided "as is" without express or implied warranty. # +# #******************************************************************************# +# +# #------------------------------------------------------------------------------# +# #Electros # +# #Authors: # +# #- Simone Robaldo (srobaldo at elemento.cloud) # +# #------------------------------------------------------------------------------# +# PORT=5142 diff --git a/docker/nginx.conf b/docker/nginx.conf index a1901be..ba09b1f 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -1,5 +1,5 @@ # #******************************************************************************# -# # Copyright(c) 2019-2023, Elemento srl, All rights reserved # +# # Copyright(c) 2019-2026, Elemento srl, All rights reserved # # # Author: Elemento srl # # # Contributors are mentioned in the code where appropriate. # # # Permission to use and modify this software and its documentation strictly # @@ -23,6 +23,7 @@ # #Authors: # # #- Filippo Ferrando Damillano (fferrando at elemento.cloud) # # #- Simone Robaldo (srobaldo at elemento.cloud) # +# #- Giorgio Torchio (gtorchio at elemento.cloud) # # #------------------------------------------------------------------------------# # events {} diff --git a/docker/readme.md b/docker/readme.md index 7a17f1a..fbfb691 100644 --- a/docker/readme.md +++ b/docker/readme.md @@ -23,12 +23,14 @@ It is never reachable from outside — it only talks to the container nginx inte It does two things: -- **Acts as a login proxy** — when a user logs in, the container nginx routes the login +- **Acts as a login proxy** —> when a user logs in, the container nginx routes the login request through the sidecar instead of directly to the auth daemon. The sidecar forwards the request, and if the auth daemon returns a success, it generates a secure random token, stores it in memory, and injects a `Set-Cookie` header into the response before returning it to the browser. +> same is done for the logout -> when a user close the web page or request a logout, nginx routes to the sidecar -> sidecar delete the session token and call the auth client logout + - **Validates sessions** — on every request to any protected resource, the container nginx calls the sidecar's `/validate` endpoint as a subrequest. The sidecar compares the token from the browser's cookie against the one stored in memory. If they match, the request @@ -38,8 +40,9 @@ It does two things: The container nginx is updated to: -- Route `/api/v1/authenticate/` through the sidecar (login proxy) instead of the auth - daemon directly. +- Route `/api/v1.0/local_login/` through the sidecar (login proxy) instead of the flask app. +- Route `/api/v1/authenticate/logout` through the sidecar, then auth client +- Route `/api/v1/authenticate/login` through the sidecar, then auth client - Add an `auth_request` check to every other location, pointing to the sidecar's `/validate` endpoint. - Return a `401` or `403` JSON response for any request that fails validation. diff --git a/docker/session_guard.py b/docker/session_guard.py index b95c4ab..4c26c47 100644 --- a/docker/session_guard.py +++ b/docker/session_guard.py @@ -1,3 +1,31 @@ +# #******************************************************************************# +# # Copyright(c) 2019-2026, Elemento srl, All rights reserved # +# # Author: Elemento srl # +# # Contributors are mentioned in the code where appropriate. # +# # Permission to use and modify this software and its documentation strictly # +# # for personal purposes is hereby granted without fee, # +# # provided that the above copyright notice appears in all copies # +# # and that both the copyright notice and this permission notice appear in the # +# # supporting documentation. # +# # Modifications to this work are allowed for personal use. # +# # Such modifications have to be licensed under a # +# # Creative Commons BY-NC-ND 4.0 International License available at # +# # http://creativecommons.org/licenses/by-nc-nd/4.0/ and have to be made # +# # available to the Elemento user community # +# # through the original distribution channels. # +# # The authors make no claims about the suitability # +# # of this software for any purpose. # +# # It is provided "as is" without express or implied warranty. # +# #******************************************************************************# +# +# #------------------------------------------------------------------------------# +# #Electros # +# #Authors: # +# #- Filippo Ferrando Damillano (fferrando at elemento.cloud) # +# #------------------------------------------------------------------------------# +# + + from http.server import HTTPServer, BaseHTTPRequestHandler import secrets import threading @@ -69,17 +97,11 @@ def _handle_logout(self): status = resp.status resp_body = resp.read() resp_headers = dict(resp.headers) - print( - f"\n\n#################################\n\nresp: {resp}\nstatus: {status}\nresp_body: {resp_body}\nresp_headers: {resp_headers}\n\n#################################\n\n" - ) except urllib.error.HTTPError as e: status = e.code resp_body = e.read() resp_headers = dict(e.headers) - print( - f"\n\n##EXCEPTION##\n\nstatus: {status}\nresp_body: {resp_body}\nresp_headers: {resp_headers}\n\n############################\n\n" - ) # remove token on logout regardless of auth daemon response with _lock: global _token @@ -168,17 +190,10 @@ def _handle_login(self): status = resp.status resp_body = resp.read() resp_headers = dict(resp.headers) - print( - f"\n\n#################################\n\nresp: {resp}\nstatus: {status}\nresp_body: {resp_body}\nresp_headers: {resp_headers}\n\n#################################\n\n" - ) - except urllib.error.HTTPError as e: status = e.code resp_body = e.read() resp_headers = dict(e.headers) - print( - f"\n\n##EXCEPTION##\n\nstatus: {status}\nresp_body: {resp_body}\nresp_headers: {resp_headers}\n\n############################\n\n" - ) # Issue a new token only on successful login cookie_header = None diff --git a/docker/startup.sh b/docker/startup.sh index 19ebe71..6225717 100644 --- a/docker/startup.sh +++ b/docker/startup.sh @@ -1,6 +1,6 @@ #! /bin/bash # #******************************************************************************# -# # Copyright(c) 2019-2023, Elemento srl, All rights reserved # +# # Copyright(c) 2019-2026, Elemento srl, All rights reserved # # # Author: Elemento srl # # # Contributors are mentioned in the code where appropriate. # # # Permission to use and modify this software and its documentation strictly # From d1229fb656f0710655ecd554ffa621b4230880bc Mon Sep 17 00:00:00 2001 From: filippo-ferrando Date: Thu, 2 Apr 2026 09:23:36 +0200 Subject: [PATCH 13/19] fix: rewrited nginx conf (comments and spacing) --- docker/nginx.conf | 318 ++++++++++++++++------------------------ docker/session_guard.py | 2 +- 2 files changed, 131 insertions(+), 189 deletions(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index ba09b1f..1fd5510 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -26,6 +26,7 @@ # #- Giorgio Torchio (gtorchio at elemento.cloud) # # #------------------------------------------------------------------------------# # + events {} http { @@ -55,6 +56,10 @@ http { auth_request /_guard/validate; + error_page 401 = @unauthorized; + error_page 403 = @forbidden + + # Sidecar call location = /_guard/validate { auth_request off; internal; @@ -64,295 +69,232 @@ http { proxy_set_header X-Session-Token $cookie_session_token; } + # static assets location / { - auth_request off; - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - try_files $uri $uri/ =404; + auth_request off; + try_files $uri $uri/ =404; } location ~ ^/(.*)/favicon.ico$ { - auth_request off; - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - index electros.iconset/electros.ico; + auth_request off; + index electros.iconset/electros.ico; } # Static asset aliases location /js/ { auth_request off; - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - alias /usr/share/nginx/html/electros/js/; + alias /usr/share/nginx/html/electros/js/; } location /css/ { - auth_request off; - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - alias /usr/share/nginx/html/electros/css/; + auth_request off; + alias /usr/share/nginx/html/electros/css/; } location /pages/ { - auth_request off; - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - alias /usr/share/nginx/html/electros/pages/; + auth_request off; + alias /usr/share/nginx/html/electros/pages/; } location /assets/ { - auth_request off; - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - alias /usr/share/nginx/html/electros/assets/; + auth_request off; + alias /usr/share/nginx/html/electros/assets/; } location /Electros.svg { - auth_request off; - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - alias /usr/share/nginx/html/electros/Electros.svg; + auth_request off; + alias /usr/share/nginx/html/electros/Electros.svg; } location /configs/ { - auth_request off; - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - alias /usr/share/nginx/html/electros/configs/; + auth_request off; + alias /usr/share/nginx/html/electros/configs/; } location /remotes/ { - auth_request off; - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - alias /usr/share/nginx/html/electros/remotes/; + auth_request off; + alias /usr/share/nginx/html/electros/remotes/; } location /favicon/ { - auth_request off; - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - alias /usr/share/nginx/html/electros/favicon/; + auth_request off; + alias /usr/share/nginx/html/electros/favicon/; } location /ecd/ { - auth_request off; - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - alias /usr/share/nginx/html/electros/ecd/; + auth_request off; + alias /usr/share/nginx/html/electros/ecd/; } location /epm/ { - auth_request off; - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - alias /usr/share/nginx/html/electros/epm/; + auth_request off; + alias /usr/share/nginx/html/electros/epm/; } location /ist/ { - auth_request off; - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - alias /usr/share/nginx/html/electros/ist/; + auth_request off; + alias /usr/share/nginx/html/electros/ist/; } # Daemon statuses handlers location /authStatus { - auth_request off; - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - proxy_pass $authenticate/; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Port $server_port; + auth_request off; + proxy_pass $authenticate/; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; } location /computeStatus { - #auth_request off; - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - proxy_pass $compute/; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Port $server_port; + proxy_pass $compute/; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; } location /storageStatus { - #auth_request off; - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - proxy_pass $storage/; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Port $server_port; + proxy_pass $storage/; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; } location /networksStatus { - #auth_request off; - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - proxy_pass $network/; - add_header X-debug "$http_authorization" always; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Port $server_port; + proxy_pass $network/; + add_header X-debug "$http_authorization" always; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; } location /servicesStatus { - #auth_request off; - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - proxy_pass $services/; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Port $server_port; + proxy_pass $services/; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; } location /targetsStatus { - #auth_request off; - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - proxy_pass $targets/; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Port $server_port; + proxy_pass $targets/; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; } - # Daemon handlers + # Login -> sidecar redirect location /api/v1/authenticate/login { - auth_request off; - proxy_pass $sidecar/login-proxy/api/v1/authenticate/login; - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Port $server_port; - proxy_pass_request_body on; + auth_request off; + proxy_pass $sidecar/login-proxy/api/v1/authenticate/login; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; + proxy_pass_request_body on; + } + + location /api/v1/authenticate/local_login { + auth_request off; + proxy_pass $sidecar/login-proxy/api/v1/authenticate/local-login; + error_page + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; + proxy_pass_request_body on; } location /api/v1/authenticate/logout { - auth_request off; - proxy_pass $sidecar/login-proxy/api/v1/authenticate/logout; - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Port $server_port; - proxy_pass_request_body on; + auth_request off; + proxy_pass $sidecar/login-proxy/api/v1/authenticate/logout; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; + proxy_pass_request_body on; } + # Functions endpoint location ~ ^/api/v1/authenticate/(.*)?$ { - auth_request off; - proxy_pass $authenticate; - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Port $server_port; - proxy_pass_request_body on; + auth_request off; + proxy_pass $authenticate; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; + proxy_pass_request_body on; } location /api/v1.0/client/backups/ { - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - proxy_pass $compute; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Port $server_port; + proxy_pass $compute; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; } location /api/v1.0/client/vm/ { - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - proxy_pass $compute; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Port $server_port; + proxy_pass $compute; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; } # special case for network attach/detach for a VM location ~ ^/api/v1.0/client/network/(attach|detach) { - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - proxy_pass $compute; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Port $server_port; + proxy_pass $compute; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; } location /api/v1.0/client/volume/ { - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - proxy_pass $storage; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Port $server_port; + proxy_pass $storage; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; } location /api/v1.0/client/network/ { - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - proxy_pass $network; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Port $server_port; + proxy_pass $network; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; } location /api/v1.0/service/ { - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - proxy_pass $services; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Port $server_port; + proxy_pass $services; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; } location /api/v1.0/client/target/ { - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - proxy_pass $targets; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Port $server_port; - } - - location /api/v1.0/local_login { - auth_request off; - proxy_pass $sidecar/local-login; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Port $server_port; - proxy_pass_request_body on; - } + proxy_pass $targets; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; + } # WebSocket passthrough location /api/v1.0/client/vnc/59441 { - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - proxy_pass https://127.0.0.1:59441/; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Port $server_port; + proxy_pass https://127.0.0.1:59441/; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; } location /api/logs { - error_page 401 = @unauthorized; - error_page 403 = @forbidden; - proxy_pass https://localhost:5142; - proxy_buffering off; - proxy_cache off; + proxy_pass https://localhost:5142; + proxy_buffering off; + proxy_cache off; } + # Error pages location @unauthorized { - default_type application/json; - return 401 '{"error":"unauthorized","message":"No active session"}'; + default_type application/json; + return 401 '{"error":"unauthorized","message":"No active session"}'; } location @forbidden { - default_type application/json; - return 403 '{"error":"forbidden","message":"Forbidden request"}'; + default_type application/json; + return 403 '{"error":"forbidden","message":"Forbidden request"}'; } } } diff --git a/docker/session_guard.py b/docker/session_guard.py index 4c26c47..83a7e60 100644 --- a/docker/session_guard.py +++ b/docker/session_guard.py @@ -60,7 +60,7 @@ def do_GET(self): def do_POST(self): if self.path == "/login-proxy/api/v1/authenticate/login": self._handle_login() - elif self.path == "/local-login": + elif self.path == "/login-proxy/api/v1/authenticate/local-login": self._handle_local_login() elif self.path == "/login-proxy/api/v1/authenticate/logout": self._handle_logout() From 78ebaf2c6ec48ed75ec73dda88ff36b4cfda7fa7 Mon Sep 17 00:00:00 2001 From: filippo-ferrando Date: Thu, 2 Apr 2026 13:01:25 +0200 Subject: [PATCH 14/19] fix: dockerfile stage comments --- docker/Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 2d972f5..855c71a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -28,10 +28,7 @@ FROM nginx:trixie -# gather daemons -# copy app code -# daemons must run in background - ./daemons -# entrypoint must run the electros app on port 80 +# Stage 1: Copy data COPY docker/nginx.conf /etc/nginx/nginx.conf COPY electros-daemons/linux/x64/* /opt/daemons/ COPY elemento-gui-new/ /usr/share/nginx/html/ @@ -42,15 +39,18 @@ COPY docker/session_guard.py /opt/app/session_guard.py COPY ./docker/follow_log.sh /opt/app/follow_log.sh COPY ./docker/logger_stream.sh /opt/app/logger_stream.sh +# Stage 2: Install dependencies RUN apt-get update -y RUN apt-get install -y socat=1.8.0.3-1 \ python3=3.13.5-1 RUN rm -rf /var/cache/apt/archives /var/lib/apt/lists/* +# Stage 3: permissions for scripts RUN chmod +x /opt/daemons/* RUN chmod +x /opt/app/startup.sh +# Stage 4: prepared forlders RUN mkdir -p /var/log/elemento RUN ls -lah /usr/share/nginx/html/ From a9e5e205c5f6f98a43265110a6d77c406c03f79f Mon Sep 17 00:00:00 2001 From: filippo-ferrando Date: Thu, 2 Apr 2026 13:01:34 +0200 Subject: [PATCH 15/19] fix: typos in nginx conf --- docker/nginx.conf | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index 1fd5510..307a81d 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -57,7 +57,7 @@ http { auth_request /_guard/validate; error_page 401 = @unauthorized; - error_page 403 = @forbidden + error_page 403 = @forbidden; # Sidecar call location = /_guard/validate { @@ -182,7 +182,7 @@ http { } # Login -> sidecar redirect - location /api/v1/authenticate/login { + location = /api/v1/authenticate/login { auth_request off; proxy_pass $sidecar/login-proxy/api/v1/authenticate/login; proxy_set_header Host $host; @@ -191,17 +191,16 @@ http { proxy_pass_request_body on; } - location /api/v1/authenticate/local_login { + location = /api/v1/authenticate/local_login { auth_request off; proxy_pass $sidecar/login-proxy/api/v1/authenticate/local-login; - error_page proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; proxy_pass_request_body on; } - location /api/v1/authenticate/logout { + location = /api/v1/authenticate/logout { auth_request off; proxy_pass $sidecar/login-proxy/api/v1/authenticate/logout; proxy_set_header Host $host; From 95be2743031b3584f7b2e3b87bc21264c57df7ac Mon Sep 17 00:00:00 2001 From: filippo-ferrando Date: Thu, 2 Apr 2026 13:01:50 +0200 Subject: [PATCH 16/19] fix: no header has to be send before nginx --- docker/session_guard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/session_guard.py b/docker/session_guard.py index 83a7e60..ad8bab4 100644 --- a/docker/session_guard.py +++ b/docker/session_guard.py @@ -117,7 +117,7 @@ def _handle_logout(self): continue self.send_header(k, v) self.send_header("Set-Cookie", expired_cookie) - self.send_header("Content-Length", str(len(resp_body))) + # self.send_header("Content-Length", str(len(resp_body))) self.end_headers() self.wfile.write(resp_body) From c8b29209b2b8cb17226c37d90938734cdd4980cc Mon Sep 17 00:00:00 2001 From: filippo-ferrando Date: Thu, 2 Apr 2026 15:00:14 +0200 Subject: [PATCH 17/19] fix: logout can be called only by aythenticated users --- docker/nginx.conf | 1 - docker/session_guard.py | 9 +-------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index 307a81d..e0476ac 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -201,7 +201,6 @@ http { } location = /api/v1/authenticate/logout { - auth_request off; proxy_pass $sidecar/login-proxy/api/v1/authenticate/logout; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; diff --git a/docker/session_guard.py b/docker/session_guard.py index ad8bab4..9a04fcc 100644 --- a/docker/session_guard.py +++ b/docker/session_guard.py @@ -67,10 +67,7 @@ def do_POST(self): else: self._send(404) - # TODO: need elecros button in order to call it def _handle_logout(self): - # call authclient logout endpoint -> if nothing responds -> ok -> local account logout - # remove session token for both ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE @@ -83,7 +80,6 @@ def _handle_logout(self): length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(length) if length else b"" - # Forward headers, strip hop-by-hop skip = {"host", "connection", "transfer-encoding", "content-length"} fwd_headers = {k: v for k, v in self.headers.items() if k.lower() not in skip} if body: @@ -102,6 +98,7 @@ def _handle_logout(self): status = e.code resp_body = e.read() resp_headers = dict(e.headers) + # remove token on logout regardless of auth daemon response with _lock: global _token @@ -117,7 +114,6 @@ def _handle_logout(self): continue self.send_header(k, v) self.send_header("Set-Cookie", expired_cookie) - # self.send_header("Content-Length", str(len(resp_body))) self.end_headers() self.wfile.write(resp_body) @@ -159,7 +155,6 @@ def _handle_local_login(self): self.send_response(status) if cookie_header: self.send_header("Set-Cookie", cookie_header) - # self.send_header("Content-Length", str(len(resp_body))) self.end_headers() self.wfile.write(resp_body) @@ -176,7 +171,6 @@ def _handle_login(self): length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(length) if length else b"" - # Forward headers, strip hop-by-hop skip = {"host", "connection", "transfer-encoding", "content-length"} fwd_headers = {k: v for k, v in self.headers.items() if k.lower() not in skip} if body: @@ -215,7 +209,6 @@ def _handle_login(self): self.send_header(k, v) if cookie_header: self.send_header("Set-Cookie", cookie_header) - # self.send_header("Content-Length", str(len(resp_body))) self.end_headers() self.wfile.write(resp_body) From ce48332eb3d45e84d79bf73d6205f497e6d5dccf Mon Sep 17 00:00:00 2001 From: filippo-ferrando Date: Fri, 3 Apr 2026 10:17:59 +0200 Subject: [PATCH 18/19] fix: changed env var name --- docker/session_guard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/session_guard.py b/docker/session_guard.py index 9a04fcc..15d4f1d 100644 --- a/docker/session_guard.py +++ b/docker/session_guard.py @@ -38,7 +38,7 @@ _lock = threading.Lock() AUTH_DAEMON = "https://127.0.0.1:47777" -FLASK_HOST = os.environ.get("PROXIMA_FLASK_HOST", "https://10.88.0.1:7781") +FLASK_HOST = os.environ.get("ATOMOS_FLASK_HOST", "https://10.88.0.1:7781") VERIFY_SSL = False # daemon uses self-signed cert From 1662693ccac50b733af77937b70120a54e83cceb Mon Sep 17 00:00:00 2001 From: filippo-ferrando Date: Fri, 3 Apr 2026 10:24:44 +0200 Subject: [PATCH 19/19] fix: status 200 (for local lougout management) --- docker/session_guard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/session_guard.py b/docker/session_guard.py index 15d4f1d..1e69d23 100644 --- a/docker/session_guard.py +++ b/docker/session_guard.py @@ -108,7 +108,7 @@ def _handle_logout(self): "session_token=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0" ) - self.send_response(status) + self.send_response(200) for k, v in resp_headers.items(): if k.lower() in ("connection", "transfer-encoding", "set-cookie"): continue