From 6e2ba02b9ac5ee65d91fee0109334c634ea355a5 Mon Sep 17 00:00:00 2001 From: heather Date: Sun, 25 Oct 2026 22:56:01 +0000 Subject: [PATCH] Harden Heather: retire remote agents and proxy, disable Jupyter, sandbox Matrix admin --- hosts/heather/arion-compose.nix | 65 ++------------------------------- hosts/heather/caddy.nix | 13 +++---- hosts/heather/default.nix | 17 +++++++-- hosts/heather/paseo.nix | 56 ---------------------------- hosts/heather/tuwunel-admin.nix | 51 +++++++++++++++----------- 5 files changed, 53 insertions(+), 149 deletions(-) delete mode 100644 hosts/heather/paseo.nix diff --git a/hosts/heather/arion-compose.nix b/hosts/heather/arion-compose.nix index 906d270..bdcaed5 100644 --- a/hosts/heather/arion-compose.nix +++ b/hosts/heather/arion-compose.nix @@ -29,8 +29,6 @@ let icon: forgejo.png href: https://git.iscg.dev description: Git forge (ssh :2222) - server: heather - container: forgejo - Syncthing: icon: syncthing.png href: https://sync.iscg.dev @@ -44,33 +42,23 @@ let icon: matrix.png href: https://iscg.dev description: Matrix homeserver (iscg.dev) - server: heather - container: tuwunel - Cinny: icon: cinny.png href: https://chat.iscg.dev description: Matrix web client - server: heather - container: cinny - Jitsi Meet: icon: jitsi-meet.png href: https://jitsi.iscg.dev description: Video conferencing - server: heather - container: jitsi-web - Productivity: - Silverbullet: icon: silverbullet.png href: https://note.iscg.dev description: Notes wiki (herbarium) - server: heather - container: silverbullet - Teable: icon: /images/teable.png href: https://teable.iscg.dev description: No-code database - server: heather - container: teable ''; homepageSettingsYaml = pkgs.writeText "homepage-settings.yaml" '' @@ -86,8 +74,7 @@ let searchDescriptions: true hideInternetSearch: true hideVersion: true - # Live container status: green dot for healthy containers (docker-socket-proxy). - statusStyle: dot + # Static navigation only: no Docker API integration. # Background photo (mounted ro at /app/public/images/ from /var/lib/homepage-images). # cardBlur = frosted-glass / matte semi-transparent service & bookmark cards. # NB: cardBlur incompatible with background blur/saturate/brightness filters, @@ -105,14 +92,8 @@ let href: https://git.iscg.dev/mikl/nix-config ''; - # Docker integration: live container status (green dot / UP) per service. - # docker-socket-proxy exposes read-only /containers/json. homepage talks to it - # over the compose network (service name `dockerproxy`), no root on docker.sock. - homepageDockerYaml = pkgs.writeText "homepage-docker.yaml" '' - heather: - host: dockerproxy - port: 2375 - ''; + # Explicitly empty to avoid stale/default Docker integration configuration. + homepageDockerYaml = pkgs.writeText "homepage-docker.yaml" "{}\n"; in { @@ -195,26 +176,6 @@ in service.ports = [ "127.0.0.1:8080:80" ]; }; - # === Xray (VLESS + Reality, :8443) === - # Прокси-сервер для обхода блокировок. Не относится к iscg.dev-сервисам: - # НЕ за Caddy, публичный порт 8443 напрямую. Reality маскируется под - # www.microsoft.com (чужой TLS, своего домена/cert не нужно). - # Конфиг с UUID + Reality privateKey в /var/lib/xray-secrets/config.json - # (вне git, root:docker 0640). Клиент: v2RayTun / v2rayN / etc. - # Секреты в pass iscg.dev/xray/{uuid,reality-private-key,reality-public-key}. - xray = { - service.image = "ghcr.io/xtls/xray-core:latest"; - service.container_name = "xray"; - service.restart = "unless-stopped"; - # Образ по умолчанию читает -confdir /usr/local/etc/xray/ (split-конфиги). - # Переопределяем на единый config.json с VLESS+Reality. - service.command = [ "-config" "/etc/xray/config.json" ]; - service.volumes = [ - "/var/lib/xray-secrets/config.json:/etc/xray/config.json:ro" - ]; - service.ports = [ "8443:8443" ]; - }; - # === Teable (teable.iscg.dev) === # Стек: app + postgres + redis. Данные перенесены с iscg-dev (2026-07-12): # - postgres восстановлен из pg_dump (18M, consistent, без остановки источника) @@ -420,26 +381,6 @@ in }; }; - # === Docker socket proxy (read-only, for homepage live container status) === - # tecnativa/docker-socket-proxy: whitelisted read-only subset of Docker API. - # CONTAINERS=1 → only /containers/json + /containers/{id}/json. POST=0 → - # no mutations. homepage connects over compose network (host: dockerproxy). - # Avoids mounting docker.sock directly into homepage (which would need root). - dockerproxy = { - service.image = "ghcr.io/tecnativa/docker-socket-proxy:latest"; - service.container_name = "dockerproxy"; - service.restart = "unless-stopped"; - service.volumes = [ - "/var/run/docker.sock:/var/run/docker.sock:ro" - ]; - service.environment = { - CONTAINERS = "1"; - POST = "0"; - SERVICES = "1"; - TASKS = "1"; - }; - }; - # === Homepage (home.iscg.dev) — dashboard со всеми сервисами === # gethomepage/homepage: статичный список ссылок на все сервисы heather. # Конфиги (services/settings/bookmarks.yaml) генерятся через pkgs.writeText diff --git a/hosts/heather/caddy.nix b/hosts/heather/caddy.nix index 74d7e96..5bea89a 100644 --- a/hosts/heather/caddy.nix +++ b/hosts/heather/caddy.nix @@ -114,17 +114,16 @@ Allow: /" 200 encode zstd gzip ''; - # paseo daemon endpoint для desktop/mobile-клиентов. - # Basic Auth здесь намеренно нет: мобильный клиент Paseo его не умеет. - # Доступ защищён встроенным PASEO_PASSWORD самого демона. + # Retired control plane: keep TLS but never proxy to a daemon. "paseo.iscg.dev".extraConfig = '' - reverse_proxy 127.0.0.1:6767 + header Cache-Control "no-store" + respond "Paseo has been retired" 410 ''; - # JupyterLab: native login form (пароль хранится как bcrypt-хеш - # в /var/lib/jupyter-secrets/jupyter.env), наружу только HTTPS. + # Disabled pending isolation from the administrative account. "jupyter.iscg.dev".extraConfig = '' - reverse_proxy 127.0.0.1:8890 + header Cache-Control "no-store" + respond "Jupyter is temporarily disabled" 503 ''; # Nick timer prototype. Public timer pages; creation has no auth for now. diff --git a/hosts/heather/default.nix b/hosts/heather/default.nix index 47f56b8..8f22bf6 100644 --- a/hosts/heather/default.nix +++ b/hosts/heather/default.nix @@ -20,9 +20,9 @@ ./caddy.nix # edge proxy, TLS, роутинг по доменам ./syncthing.nix # sync + relay ./tuwunel-admin.nix # matrix admin web UI (systemd, статический бинарник) - ./paseo.nix # paseo daemon (web UI для CLI-агентов, systemd + npm global) ./nick.nix # Nick timer prototype (systemd, localhost:8787) - ./jupyter.nix # JupyterLab (systemd, localhost:8890, лимиты) + # Jupyter is intentionally disabled until it has a separate service identity. + # Keep its venv, notebooks and external credentials untouched. ./astrnet.nix # AstrNet (Sol): two WireGuard legs, FRR/BGP, ingress guard ./minecraft-api.nix # mesh-only typed Minecraft API, isolated DynamicUser ]; @@ -72,6 +72,18 @@ # explicitly with `arion --prebuilt-file up -d --no-deps SERVICE`. systemd.services.heather.restartIfChanged = false; + # The public dashboard is navigation only, never a Docker administration client. + assertions = [ + { + assertion = let + services = config.virtualisation.arion.projects.heather.settings.services; + in !(services ? dockerproxy) && lib.all + (service: lib.all (mount: !(lib.hasInfix "docker.sock" mount)) service.service.volumes) + (lib.attrValues services); + message = "Heather web stack must not expose Docker socket access or a dockerproxy service."; + } + ]; + networking = { hostName = "heather"; # Firewall выключен в ./image.nix (полагаемся на YC Security Groups). @@ -85,7 +97,6 @@ 22067 # strelaysrv (relay) 22070 # strelaysrv (status) 3478 # coturn TURN (tcp+udp) - 8443 # xray VLESS+Reality (публичный прокси) ]; firewall.allowedUDPPorts = [ 22000 # syncthing sync (QUIC) diff --git a/hosts/heather/paseo.nix b/hosts/heather/paseo.nix deleted file mode 100644 index 5ca3735..0000000 --- a/hosts/heather/paseo.nix +++ /dev/null @@ -1,56 +0,0 @@ -# heather — paseo daemon (self-hosted web UI для CLI-агентов: pi, codex и др.). -# -# https://github.com/getpaseo/paseo, npm-пакет @getpaseo/cli -# Daemon управляет агентами как дочерними процессами (provider = CLI на хосте, -# со своими кредами). Слушает 127.0.0.1:6767 → Caddy paseo.iscg.dev терминирует -# TLS. Этот hostname предназначен для desktop/mobile-клиентов: bundled web UI -# выключен, а Caddy Basic Auth намеренно не используется — мобильный клиент не -# умеет проходить дополнительный Basic Auth слой. -# -# Auth: встроенная парольная auth демона (bcrypt, Bearer header + WS subprotocol, -# без пароля — 401). Пароль: PASEO_PASSWORD из EnvironmentFile -# /var/lib/paseo-secrets/paseo.env (mikl:users 600), plaintext в -# pass iscg.dev/paseo/daemon. -# -# Конфиг демона: ~/.paseo/config.json (hostnames: paseo.iscg.dev — DNS rebinding -# protection, webUi disabled, relay off, dictation/voice off). Хост-allowlist -# проверен: чужой Host → 403. -# -# Установка @getpaseo/cli — ручная (npm global в ~/.npm-global, как pi-web): -# npm i -g @getpaseo/cli # node-pty собирается без тулчейна (prebuilds) -# Провайдеры: pi уже стоит (~/.local/bin/pi). - -{ config, pkgs, lib, ... }: -{ - systemd.services.paseo = { - description = "paseo daemon (self-hosted web UI for CLI coding agents)"; - after = [ "network-online.target" ]; - wants = [ "network-online.target" ]; - wantedBy = [ "multi-user.target" ]; - - # PATH: nodejs (node/npm) + ~/.npm-global/bin (paseo) + ~/.local/bin (pi). - # Для package/path NixOS добавляет /bin сам: каталоги должны быть - # уровнями выше фактических ~/.npm-global/bin и ~/.local/bin. - # Paseo запускает `ps` при управлении дочерними agent-процессами. - # Tool runner также ищет `sh` через PATH; на NixOS нельзя полагаться - # на FHS /bin/sh, поэтому добавляем bashInteractive явно. - path = [ "/run/current-system/sw" pkgs.nodejs pkgs.git pkgs.procps pkgs.bashInteractive "/home/mikl/.npm-global" "/home/mikl/.local" ]; - - environment = { - PASEO_HOME = "/home/mikl/.paseo"; - HOME = "/home/mikl"; - }; - - serviceConfig = { - Type = "simple"; - User = "mikl"; - Group = "users"; - ExecStart = "/home/mikl/.npm-global/bin/paseo daemon start --foreground --no-web-ui --no-relay"; - EnvironmentFile = "/var/lib/paseo-secrets/paseo.env"; - WorkingDirectory = "/home/mikl"; - Restart = "always"; - RestartSec = 5; - StartLimitIntervalSec = 0; - }; - }; -} diff --git a/hosts/heather/tuwunel-admin.nix b/hosts/heather/tuwunel-admin.nix index e0a6f9b..6a42035 100644 --- a/hosts/heather/tuwunel-admin.nix +++ b/hosts/heather/tuwunel-admin.nix @@ -1,20 +1,8 @@ -# heather — tuwunel-admin (web admin UI for tuwunel Matrix homeserver). -# -# https://github.com/knadh/tuwunel-admin (Rust, axum, self-contained static binary). -# Логинится matrix-пользователем на homeserver https://iscg.dev (см. [matrix] -# в config.toml). Чтобы UI мог выполнять admin-команды, залогиненный юзер -# должен быть server admin tuwunel (первый зарегистрированный = admin room member). -# -# Слушает 127.0.0.1:8009 → Caddy admin.iscg.dev терминирует TLS (см. caddy.nix). -# Конфиг: /var/lib/tuwunel-admin/config.toml (вне git, root:root 0644). -# -# Бинарь: публичного docker-образа НЕТ. Это статический x86_64 бинарник v0.0.0 -# (скопирован с iscg-dev-jul11 2026-07-13), лежит ВНЕ nix-store: -# /var/lib/tuwunel-admin/tuwunel-admin -# При пересоздании heather с нуля — перескопировать с jul11 или скачать релиз -# https://github.com/knadh/tuwunel-admin/releases (v0.1.0+). - -{ config, pkgs, lib, ... }: +# Public Matrix admin UI: a separate unprivileged, sandboxed identity. +# The root-owned binary and non-secret config remain read-only outside Nix. +# StateDirectory is deliberately DIFFERENT from the executable directory: +# systemd must never make the binary/config writable by the service user. +{ ... }: { systemd.services.tuwunel-admin = { description = "tuwunel-admin (Matrix admin web UI)"; @@ -23,13 +11,34 @@ wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "simple"; + DynamicUser = true; + User = "tuwunel-admin"; + StateDirectory = "tuwunel-admin-state"; + StateDirectoryMode = "0700"; + WorkingDirectory = "/var/lib/tuwunel-admin-state"; + Environment = [ "HOME=/var/lib/tuwunel-admin-state" ]; ExecStart = "/var/lib/tuwunel-admin/tuwunel-admin --config /var/lib/tuwunel-admin/config.toml"; - WorkingDirectory = "/var/lib/tuwunel-admin"; Restart = "on-failure"; RestartSec = 5; - # Бинарь статический, рут-запуск ок (биндит 127.0.0.1:8009). - User = "root"; - Environment = [ "HOME=/var/lib/tuwunel-admin" ]; + UMask = "0077"; + NoNewPrivileges = true; + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + PrivateDevices = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + ProtectClock = true; + RestrictSUIDSGID = true; + RestrictNamespaces = true; + LockPersonality = true; + CapabilityBoundingSet = ""; + AmbientCapabilities = ""; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; + MemoryMax = "256M"; + TasksMax = 128; }; }; }