#!/usr/bin/env bash# Public site kit (euler-site): what signed-out visitors read, served from the euler-content wheel.# =================================================================================================## Deploys / removes the public site — the published content (guides, blogs, the public# solutions, the progress grid) for visitors who are not signed in (docs/package-guide.md# § 3.4; docs/web-server-guide.md § The public site). Sibling to msg.sh / auth.sh.## Model:# - The service (python -m euler_site.site, EULER_SITE_PUBLIC=1) runs as the dedicated# `euler-site` user from the shared root-owned /opt/euler venv, which **auth.sh owns**:# its deploy also installs `euler-content`, the packaged public tree this serves. This# kit never builds or reinstalls the venv; it only restarts the service against it.# - It reads nothing but that installed package: no clone, no git, no home — the unit# has ProtectHome=true, so no collaborator's tree (and no private solution) is even# visible to it. There is no identity: it never reads X-User, and it has only the read# routes.# - One unix socket, /run/euler/site.sock (0660 euler-site:euler-web), Caddy's upstream# for GET/HEAD of the content paths from a browser carrying no session cookie# (frontend.sh). No network at all: RestrictAddressFamilies=AF_UNIX.## /etc/euler/site.env root:euler-site 0640 (generated here)# /etc/systemd/system/euler-site.service (root-owned, boot-enabled)## Actions: deploy | remove | upgrade | redeploy | status | help## Author: Vikas Munshi <vikas.munshi@gmail.com># Copyright (c) 2026. All rights reserved.# Licensed under the MIT License.set-euopipefailSCRIPT_DIR="$(cd"$(dirname"${BASH_SOURCE[0]}")"&&pwd)"# The system venv — OPT_DIR / VENV_DIR / VENV_PY / PYTHON, shared with the other app# kits. This kit reads it; auth.sh is the one that builds it.# shellcheck source=scripts/setup/venv.sh."${SCRIPT_DIR}/venv.sh"SYS_DIR="/etc/euler"SITE_ENV="${SYS_DIR}/site.env"# scoped runtime config (root:euler-site 0640)SITE_USER="euler-site"SITE_GROUP="euler-site"# The group site.sock is chgrp'd to: Caddy is its only client, so euler-edge (no per-user# uid), not euler-web. Membership also lets the service create the socket in /run/euler.EDGE_GROUP="euler-edge"SITE_SOCKET="/run/euler/site.sock"SERVICE_NAME="euler-site.service"SERVICE_DEST="/etc/systemd/system/${SERVICE_NAME}"usage(){cat<<USAGEUsage: $0 [deploy|remove|upgrade|redeploy|status|help] deploy Create the euler-site user and ${SITE_ENV}, and install the root-owned, boot-enabled ${SERVICE_NAME}. Idempotent. upgrade Alias of deploy — the deploy path is already the re-assert path. redeploy Fast path: restart the service against the current /opt/euler venv (auth.sh redeploy is what refreshes the venv and the content package); deploys it instead when it is not installed yet. remove Remove the service, its env file and the euler-site identity. status Report the unit, the identity, the config, and probe /healthz. Needs the shared venv with euler-content in it (make deploy-auth). This kit never touches ${VENV_DIR} itself.USAGE}check_can_sudo(){if!command-vsudo&>/dev/null;thenecho"Error: sudo is not installed or not found in PATH">&2return1fiif!sudo-v2>/dev/null;thenecho"Error: current user does not have sudo privileges">&2return1fi}require_systemd(){if[!-d/run/systemd/system]||!command-vsystemctl&>/dev/null;thenecho"Error: systemd is required for the public site; it is not active here.">&2return1fi}require_venv(){if[!-x"${VENV_PY}"];thenecho"Error: ${VENV_DIR} is not deployed — run 'make deploy-auth' first (it owns the venv).">&2return1fiif!sudo"${VENV_PY}"-P-c'import euler_site.site, euler_content'2>/dev/null;thenecho"Error: euler_site.site or euler_content is not importable from ${VENV_DIR}.">&2echo" Run 'make redeploy-auth' to reinstall this checkout and its content.">&2return1fi}# The dedicated euler-site user. In euler-edge only, to create its socket in /run/euler and# chgrp it at bind time; it leaves euler-web, which it no longer needs.ensure_site_user(){localgroupforgroupin"${SITE_GROUP}""${EDGE_GROUP}";dogetentgroup"${group}">/dev/null||sudogroupadd--system"${group}"doneif!getentpasswd"${SITE_USER}">/dev/null;thenecho"Creating system user ${SITE_USER}..."sudouseradd--system--no-create-home--shell/usr/sbin/nologin\-g"${SITE_GROUP}"-G"${EDGE_GROUP}""${SITE_USER}"elsesudousermod-G"${EDGE_GROUP}""${SITE_USER}"fi}# The scoped runtime config. Nothing secret: the public site holds no secret at all.deploy_site_env(){sudomkdir-p"${SYS_DIR}"sudotee"${SITE_ENV}">/dev/null<<EOF# GENERATED by scripts/setup/site.sh — scoped runtime config for euler-site.# The public site: the packaged content, read routes only, no identity, no secrets.EULER_SITE_PUBLIC=1EULER_CONTENT_SOCKET=${SITE_SOCKET}# The group site.sock is chgrp'd to (the setting keeps its historical name).EULER_WEB_GROUP=${EDGE_GROUP}EOFsudochownroot:"${SITE_GROUP}""${SITE_ENV}"sudochmod0640"${SITE_ENV}"}deploy_unit(){echo"Installing ${SERVICE_NAME} (as ${SITE_USER}, unix socket only)..."sudotee"${SERVICE_DEST}">/dev/null<<EOF[Unit]Description=euler public site (the published content, signed out)Documentation=https://github.com/vikasmunshi/euler/blob/master/docs/package-guide.mdAfter=network.target[Service]# Type=simple, like every sibling kit: the app never sd_notify()s.Type=simpleUser=${SITE_USER}Group=${SITE_GROUP}# euler-edge: to create its socket in /run/euler and chgrp it, so Caddy can connect.SupplementaryGroups=${EDGE_GROUP}EnvironmentFile=${SITE_ENV}ExecStart=${VENV_PY} -m euler_site.siteRestart=on-failureRestartSec=5s# Egress: none — AF_UNIX only. It serves files from a package and talks to nobody.IPAddressDeny=anyRestrictAddressFamilies=AF_UNIX# Hardening. ProtectHome=true is the point: no clone, and so no private solution, is# visible to this process at all — what it can serve is what the package installed.NoNewPrivileges=trueProtectSystem=strictProtectHome=true# The shared socket dir is provisioned by tmpfiles.d (auth.sh) and owned by no service,# so ProtectSystem=strict's read-only /run must be opened for the socket.ReadWritePaths=/run/eulerPrivateTmp=truePrivateDevices=trueCapabilityBoundingSet=LockPersonality=trueMemoryDenyWriteExecute=trueRestrictRealtime=trueSystemCallArchitectures=nativeProtectKernelTunables=trueProtectKernelModules=trueProtectKernelLogs=trueProtectControlGroups=trueProtectClock=trueProtectHostname=trueProtectProc=invisibleProcSubset=pidRestrictNamespaces=trueRestrictSUIDSGID=trueRemoveIPC=trueSystemCallFilter=@system-serviceSystemCallErrorNumber=EPERM# No network namespace of its own to speak of: it serves and dials only unix sockets on the# filesystem, which reach across network namespaces, so it can have none at all.PrivateNetwork=trueUMask=0007[Install]WantedBy=multi-user.targetEOFsudosystemctldaemon-reload}# `systemctl restart` returns before the service binds; wait (bounded) so do_status reports# what is, not a race.wait_for_socket(){localifor((i=0;i<50;i++));doifsudotest-S"${SITE_SOCKET}";thenreturn0;fisleep0.1donereturn0}do_deploy(){check_can_sudo||return1require_systemd||return1require_venv||return1ensure_site_userdeploy_site_envdeploy_unitsudosystemctlenable--now"${SERVICE_NAME}"sudosystemctlrestart"${SERVICE_NAME}"# The service uid joins the nftables egress drop set (defence in depth: the unit# already makes off-host traffic impossible).if[-f/etc/systemd/system/euler-firewall.service];thenecho"Reloading the egress firewall to include ${SITE_USER}...""${SCRIPT_DIR}/firewall.sh"reloadfiwait_for_socketdo_status}do_redeploy(){check_can_sudo||return1require_venv||return1# The first redeploy after this kit appeared finds no unit: the edge already routes# signed-out reads here (frontend.sh), so lay the service down rather than leave them# on the holding page. Every later redeploy is the restart below.if[!-f"${SERVICE_DEST}"];thenecho"${SERVICE_NAME} is not installed yet — deploying it."do_deployreturnfiecho"Restarting ${SERVICE_NAME} against ${VENV_DIR}..."sudosystemctlrestart"${SERVICE_NAME}"wait_for_socketdo_status}do_remove(){check_can_sudo||return1if[-f"${SERVICE_DEST}"];thensudosystemctldisable--now"${SERVICE_NAME}"2>/dev/null||truesudorm-f"${SERVICE_DEST}"sudosystemctldaemon-reloadfisudorm-f"${SITE_SOCKET}""${SITE_ENV}"ifgetentpasswd"${SITE_USER}">/dev/null;thensudouserdel"${SITE_USER}"2>/dev/null||true;fiifgetentgroup"${SITE_GROUP}">/dev/null;thensudogroupdel"${SITE_GROUP}"2>/dev/null||true;fiecho"Public site uninstall complete (signed-out visitors now get the login page)."}# Probe the socket's /healthz and the start page. Needs sudo: the socket is euler-web-only# and the operator deliberately is not in that group.probe_socket(){if!command-vcurl>/dev/null2>&1;thenecho"healthz: … unknown (curl not installed)"return0filocalreplycodereply="$(sudocurl-sS--max-time3--unix-socket"${SITE_SOCKET}"\http://localhost/healthz2>/dev/null||true)"if["${reply}"="ok"];thenecho"healthz: ✓ ${SITE_SOCKET} serving"elseecho"healthz: ✗ ${SITE_SOCKET} not answering"return0ficode="$(sudocurl-sS-o/dev/null-w'%{http_code}'--max-time5--unix-socket"${SITE_SOCKET}"\http://localhost/solutions/2>/dev/null||true)"echo"content: $(["${code}"=200]&&echo'✓'||echo'✗') /solutions/ → ${code}"}do_status(){if[-f"${SERVICE_DEST}"]&&command-vsystemctl&>/dev/null;thenecho"${SERVICE_NAME}: $(systemctlis-active"${SERVICE_NAME}"2>/dev/null)/$(systemctlis-enabled"${SERVICE_NAME}"2>/dev/null)"elseecho"${SERVICE_NAME}: ✗ not installed"fiifgetentpasswd"${SITE_USER}">/dev/null;thenecho"identity: ✓ ${SITE_USER} (groups: $(id-nG"${SITE_USER}"2>/dev/null))"elseecho"identity: ✗ ${SITE_USER} missing"fiif[-f"${SITE_ENV}"];thenecho"config: ✓ ${SITE_ENV} ($(stat-c'%U:%G %a'"${SITE_ENV}"2>/dev/null))"elseecho"config: ✗ ${SITE_ENV} missing"fiif[-x"${VENV_PY}"];thenlocalversionversion="$(sudo"${VENV_PY}"-P-c'import euler_content._version as v; print(v.__version__)'2>/dev/null||true)"if[-n"${version}"];thenecho"package: ✓ euler-content ${version}"elseecho"package: ✗ euler-content not installed (make redeploy-auth)"fifiprobe_socket}# ── Dispatch ──────────────────────────────────────────────────────────────────────ACTION="${1:-status}"case"${ACTION}"indeploy)do_deploy;;upgrade)do_deploy;;redeploy)do_redeploy;;remove)do_remove;;status)do_status;;-h|--help|help)usage;;*)echo"Unknown action: ${ACTION}";usage;exit1;;esac