Harden Heather: retire remote agents and proxy, disable Jupyter, sandbox Matrix admin
This commit is contained in:
parent
8c4840a0d3
commit
6e2ba02b9a
5 changed files with 53 additions and 149 deletions
|
|
@ -29,8 +29,6 @@ let
|
||||||
icon: forgejo.png
|
icon: forgejo.png
|
||||||
href: https://git.iscg.dev
|
href: https://git.iscg.dev
|
||||||
description: Git forge (ssh :2222)
|
description: Git forge (ssh :2222)
|
||||||
server: heather
|
|
||||||
container: forgejo
|
|
||||||
- Syncthing:
|
- Syncthing:
|
||||||
icon: syncthing.png
|
icon: syncthing.png
|
||||||
href: https://sync.iscg.dev
|
href: https://sync.iscg.dev
|
||||||
|
|
@ -44,33 +42,23 @@ let
|
||||||
icon: matrix.png
|
icon: matrix.png
|
||||||
href: https://iscg.dev
|
href: https://iscg.dev
|
||||||
description: Matrix homeserver (iscg.dev)
|
description: Matrix homeserver (iscg.dev)
|
||||||
server: heather
|
|
||||||
container: tuwunel
|
|
||||||
- Cinny:
|
- Cinny:
|
||||||
icon: cinny.png
|
icon: cinny.png
|
||||||
href: https://chat.iscg.dev
|
href: https://chat.iscg.dev
|
||||||
description: Matrix web client
|
description: Matrix web client
|
||||||
server: heather
|
|
||||||
container: cinny
|
|
||||||
- Jitsi Meet:
|
- Jitsi Meet:
|
||||||
icon: jitsi-meet.png
|
icon: jitsi-meet.png
|
||||||
href: https://jitsi.iscg.dev
|
href: https://jitsi.iscg.dev
|
||||||
description: Video conferencing
|
description: Video conferencing
|
||||||
server: heather
|
|
||||||
container: jitsi-web
|
|
||||||
- Productivity:
|
- Productivity:
|
||||||
- Silverbullet:
|
- Silverbullet:
|
||||||
icon: silverbullet.png
|
icon: silverbullet.png
|
||||||
href: https://note.iscg.dev
|
href: https://note.iscg.dev
|
||||||
description: Notes wiki (herbarium)
|
description: Notes wiki (herbarium)
|
||||||
server: heather
|
|
||||||
container: silverbullet
|
|
||||||
- Teable:
|
- Teable:
|
||||||
icon: /images/teable.png
|
icon: /images/teable.png
|
||||||
href: https://teable.iscg.dev
|
href: https://teable.iscg.dev
|
||||||
description: No-code database
|
description: No-code database
|
||||||
server: heather
|
|
||||||
container: teable
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
homepageSettingsYaml = pkgs.writeText "homepage-settings.yaml" ''
|
homepageSettingsYaml = pkgs.writeText "homepage-settings.yaml" ''
|
||||||
|
|
@ -86,8 +74,7 @@ let
|
||||||
searchDescriptions: true
|
searchDescriptions: true
|
||||||
hideInternetSearch: true
|
hideInternetSearch: true
|
||||||
hideVersion: true
|
hideVersion: true
|
||||||
# Live container status: green dot for healthy containers (docker-socket-proxy).
|
# Static navigation only: no Docker API integration.
|
||||||
statusStyle: dot
|
|
||||||
# Background photo (mounted ro at /app/public/images/ from /var/lib/homepage-images).
|
# Background photo (mounted ro at /app/public/images/ from /var/lib/homepage-images).
|
||||||
# cardBlur = frosted-glass / matte semi-transparent service & bookmark cards.
|
# cardBlur = frosted-glass / matte semi-transparent service & bookmark cards.
|
||||||
# NB: cardBlur incompatible with background blur/saturate/brightness filters,
|
# NB: cardBlur incompatible with background blur/saturate/brightness filters,
|
||||||
|
|
@ -105,14 +92,8 @@ let
|
||||||
href: https://git.iscg.dev/mikl/nix-config
|
href: https://git.iscg.dev/mikl/nix-config
|
||||||
'';
|
'';
|
||||||
|
|
||||||
# Docker integration: live container status (green dot / UP) per service.
|
# Explicitly empty to avoid stale/default Docker integration configuration.
|
||||||
# docker-socket-proxy exposes read-only /containers/json. homepage talks to it
|
homepageDockerYaml = pkgs.writeText "homepage-docker.yaml" "{}\n";
|
||||||
# over the compose network (service name `dockerproxy`), no root on docker.sock.
|
|
||||||
homepageDockerYaml = pkgs.writeText "homepage-docker.yaml" ''
|
|
||||||
heather:
|
|
||||||
host: dockerproxy
|
|
||||||
port: 2375
|
|
||||||
'';
|
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|
@ -195,26 +176,6 @@ in
|
||||||
service.ports = [ "127.0.0.1:8080:80" ];
|
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) ===
|
# === Teable (teable.iscg.dev) ===
|
||||||
# Стек: app + postgres + redis. Данные перенесены с iscg-dev (2026-07-12):
|
# Стек: app + postgres + redis. Данные перенесены с iscg-dev (2026-07-12):
|
||||||
# - postgres восстановлен из pg_dump (18M, consistent, без остановки источника)
|
# - 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 со всеми сервисами ===
|
# === Homepage (home.iscg.dev) — dashboard со всеми сервисами ===
|
||||||
# gethomepage/homepage: статичный список ссылок на все сервисы heather.
|
# gethomepage/homepage: статичный список ссылок на все сервисы heather.
|
||||||
# Конфиги (services/settings/bookmarks.yaml) генерятся через pkgs.writeText
|
# Конфиги (services/settings/bookmarks.yaml) генерятся через pkgs.writeText
|
||||||
|
|
|
||||||
|
|
@ -114,17 +114,16 @@ Allow: /" 200
|
||||||
encode zstd gzip
|
encode zstd gzip
|
||||||
'';
|
'';
|
||||||
|
|
||||||
# paseo daemon endpoint для desktop/mobile-клиентов.
|
# Retired control plane: keep TLS but never proxy to a daemon.
|
||||||
# Basic Auth здесь намеренно нет: мобильный клиент Paseo его не умеет.
|
|
||||||
# Доступ защищён встроенным PASEO_PASSWORD самого демона.
|
|
||||||
"paseo.iscg.dev".extraConfig = ''
|
"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-хеш
|
# Disabled pending isolation from the administrative account.
|
||||||
# в /var/lib/jupyter-secrets/jupyter.env), наружу только HTTPS.
|
|
||||||
"jupyter.iscg.dev".extraConfig = ''
|
"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.
|
# Nick timer prototype. Public timer pages; creation has no auth for now.
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,9 @@
|
||||||
./caddy.nix # edge proxy, TLS, роутинг по доменам
|
./caddy.nix # edge proxy, TLS, роутинг по доменам
|
||||||
./syncthing.nix # sync + relay
|
./syncthing.nix # sync + relay
|
||||||
./tuwunel-admin.nix # matrix admin web UI (systemd, статический бинарник)
|
./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)
|
./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
|
./astrnet.nix # AstrNet (Sol): two WireGuard legs, FRR/BGP, ingress guard
|
||||||
./minecraft-api.nix # mesh-only typed Minecraft API, isolated DynamicUser
|
./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`.
|
# explicitly with `arion --prebuilt-file <active-unit-compose> up -d --no-deps SERVICE`.
|
||||||
systemd.services.heather.restartIfChanged = false;
|
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 = {
|
networking = {
|
||||||
hostName = "heather";
|
hostName = "heather";
|
||||||
# Firewall выключен в ./image.nix (полагаемся на YC Security Groups).
|
# Firewall выключен в ./image.nix (полагаемся на YC Security Groups).
|
||||||
|
|
@ -85,7 +97,6 @@
|
||||||
22067 # strelaysrv (relay)
|
22067 # strelaysrv (relay)
|
||||||
22070 # strelaysrv (status)
|
22070 # strelaysrv (status)
|
||||||
3478 # coturn TURN (tcp+udp)
|
3478 # coturn TURN (tcp+udp)
|
||||||
8443 # xray VLESS+Reality (публичный прокси)
|
|
||||||
];
|
];
|
||||||
firewall.allowedUDPPorts = [
|
firewall.allowedUDPPorts = [
|
||||||
22000 # syncthing sync (QUIC)
|
22000 # syncthing sync (QUIC)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +1,8 @@
|
||||||
# heather — tuwunel-admin (web admin UI for tuwunel Matrix homeserver).
|
# Public Matrix admin UI: a separate unprivileged, sandboxed identity.
|
||||||
#
|
# The root-owned binary and non-secret config remain read-only outside Nix.
|
||||||
# https://github.com/knadh/tuwunel-admin (Rust, axum, self-contained static binary).
|
# StateDirectory is deliberately DIFFERENT from the executable directory:
|
||||||
# Логинится matrix-пользователем на homeserver https://iscg.dev (см. [matrix]
|
# systemd must never make the binary/config writable by the service user.
|
||||||
# в 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, ... }:
|
|
||||||
{
|
{
|
||||||
systemd.services.tuwunel-admin = {
|
systemd.services.tuwunel-admin = {
|
||||||
description = "tuwunel-admin (Matrix admin web UI)";
|
description = "tuwunel-admin (Matrix admin web UI)";
|
||||||
|
|
@ -23,13 +11,34 @@
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "simple";
|
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";
|
ExecStart = "/var/lib/tuwunel-admin/tuwunel-admin --config /var/lib/tuwunel-admin/config.toml";
|
||||||
WorkingDirectory = "/var/lib/tuwunel-admin";
|
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
RestartSec = 5;
|
RestartSec = 5;
|
||||||
# Бинарь статический, рут-запуск ок (биндит 127.0.0.1:8009).
|
UMask = "0077";
|
||||||
User = "root";
|
NoNewPrivileges = true;
|
||||||
Environment = [ "HOME=/var/lib/tuwunel-admin" ];
|
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;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue