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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
#
|
||||
# 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;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue