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 diff --git a/docker/Dockerfile b/docker/Dockerfile index af1b1fa..855c71a 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 # @@ -28,26 +28,29 @@ 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/ 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 +# 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/ 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 ebab9bc..e0476ac 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,17 +23,17 @@ # #Authors: # # #- Filippo Ferrando Damillano (fferrando at elemento.cloud) # # #- Simone Robaldo (srobaldo at elemento.cloud) # +# #- Giorgio Torchio (gtorchio 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; @@ -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,174 +54,245 @@ http { root /usr/share/nginx/html; index electros/electros.html; + auth_request /_guard/validate; + + error_page 401 = @unauthorized; + error_page 403 = @forbidden; + + # Sidecar call + location = /_guard/validate { + auth_request off; + internal; + proxy_pass $sidecar/validate; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header X-Session-Token $cookie_session_token; + } + + # static assets location / { - try_files $uri $uri/ =404; + auth_request off; + try_files $uri $uri/ =404; } location ~ ^/(.*)/favicon.ico$ { - index electros.iconset/electros.ico; + auth_request off; + index electros.iconset/electros.ico; } # Static asset aliases location /js/ { - alias /usr/share/nginx/html/electros/js/; + auth_request off; + alias /usr/share/nginx/html/electros/js/; } location /css/ { - alias /usr/share/nginx/html/electros/css/; + auth_request off; + alias /usr/share/nginx/html/electros/css/; } location /pages/ { - alias /usr/share/nginx/html/electros/pages/; + auth_request off; + alias /usr/share/nginx/html/electros/pages/; } location /assets/ { - alias /usr/share/nginx/html/electros/assets/; + auth_request off; + alias /usr/share/nginx/html/electros/assets/; } location /Electros.svg { - alias /usr/share/nginx/html/electros/Electros.svg; + auth_request off; + alias /usr/share/nginx/html/electros/Electros.svg; } location /configs/ { - alias /usr/share/nginx/html/electros/configs/; + auth_request off; + alias /usr/share/nginx/html/electros/configs/; } location /remotes/ { - alias /usr/share/nginx/html/electros/remotes/; + auth_request off; + alias /usr/share/nginx/html/electros/remotes/; } location /favicon/ { - alias /usr/share/nginx/html/electros/favicon/; + auth_request off; + alias /usr/share/nginx/html/electros/favicon/; } location /ecd/ { - alias /usr/share/nginx/html/electros/ecd/; + auth_request off; + alias /usr/share/nginx/html/electros/ecd/; } location /epm/ { - alias /usr/share/nginx/html/electros/epm/; + auth_request off; + alias /usr/share/nginx/html/electros/epm/; } location /ist/ { - alias /usr/share/nginx/html/electros/ist/; + auth_request off; + alias /usr/share/nginx/html/electros/ist/; } # Daemon statuses handlers location /authStatus { - 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 { - 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 { - 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 { - 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/; + 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 { - 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 { - 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; + } + + # Login -> sidecar redirect + location = /api/v1/authenticate/login { + 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; } - # Daemon handlers - location /api/v1/authenticate/ { - proxy_pass $authenticate; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Port $server_port; + location = /api/v1/authenticate/local_login { + auth_request off; + proxy_pass $sidecar/login-proxy/api/v1/authenticate/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; + } + + location = /api/v1/authenticate/logout { + 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; + 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/ { - 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/ { - 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) { - 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/ { - 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/ { - 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/ { - 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/ { - 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; + } # WebSocket passthrough location /api/v1.0/client/vnc/59441 { - 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 { - 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"}'; + } + + location @forbidden { + default_type application/json; + return 403 '{"error":"forbidden","message":"Forbidden request"}'; } } } diff --git a/docker/readme.md b/docker/readme.md new file mode 100644 index 0000000..fbfb691 --- /dev/null +++ b/docker/readme.md @@ -0,0 +1,73 @@ +# 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. + +> 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 + proceeds. If not, the container nginx blocks it with a `401`. + +### 2. The container nginx + +The container nginx is updated to: + +- 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. + +--- + +## 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 diff --git a/docker/session_guard.py b/docker/session_guard.py new file mode 100644 index 0000000..1e69d23 --- /dev/null +++ b/docker/session_guard.py @@ -0,0 +1,220 @@ +# #******************************************************************************# +# # 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 +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("ATOMOS_FLASK_HOST", "https://10.88.0.1:7781") +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): + 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) + else: + self._send(404) + + def do_POST(self): + if self.path == "/login-proxy/api/v1/authenticate/login": + self._handle_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() + else: + self._send(404) + + def _handle_logout(self): + 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"" + + 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) + + # 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(200) + 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.end_headers() + self.wfile.write(resp_body) + + 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() + + # 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 + cookie_header = ( + f"session_token={tok}; Path=/; HttpOnly; Secure; SameSite=Lax" + ) + + # Write response back to nginx + self.send_response(status) + if cookie_header: + self.send_header("Set-Cookie", cookie_header) + self.end_headers() + self.wfile.write(resp_body) + + def _handle_login(self): + 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"" + + 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) + print(f"\nSetting token: {tok}\n\n") + 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.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..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 # @@ -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