Harden Heather: retire remote agents and proxy, disable Jupyter, sandbox Matrix admin

This commit is contained in:
heather 2026-10-25 22:56:01 +00:00
commit 6e2ba02b9a
5 changed files with 53 additions and 149 deletions

View file

@ -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

View file

@ -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.

View file

@ -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 <active-unit-compose> 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)

View file

@ -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;
};
};
}

View file

@ -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;
};
};
}