diff --git a/docker/hermes/Dockerfile.full b/docker/hermes/Dockerfile.full new file mode 100644 index 0000000..1edd524 --- /dev/null +++ b/docker/hermes/Dockerfile.full @@ -0,0 +1,71 @@ +FROM ghcr.io/astral-sh/uv:0.11.6-python3.13-trixie@sha256:b3c543b6c4f23a5f2df22866bd7857e5d304b67a564f4feab6ac22044dde719b AS uv_source +FROM tianon/gosu:1.19-trixie@sha256:3b176695959c71e123eb390d427efc665eeb561b1540e82679c15e992006b8b9 AS gosu_source +FROM debian:13.4 + +# Disable Python stdout buffering to ensure logs are printed immediately +ENV PYTHONUNBUFFERED=1 + +# Store Playwright browsers outside the volume mount so the build-time +# install survives the /opt/data volume overlay at runtime. +ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright + +# Install system dependencies in one layer, clear APT cache +# tini reaps orphaned zombie processes (MCP stdio subprocesses, git, bun, etc.) +# that would otherwise accumulate when hermes runs as PID 1. See #15012. +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + build-essential nodejs npm python3 ripgrep ffmpeg gcc python3-dev libffi-dev procps git openssh-client docker-cli tini \ + curl poppler-utils imagemagick \ + chromium xvfb fonts-noto-color-emoji fonts-unifont fonts-liberation fonts-ipafont-gothic fonts-wqy-zenhei fonts-tlwg-loma-otf fonts-freefont-ttf \ + libasound2t64 libatk-bridge2.0-0t64 libatk1.0-0t64 libatspi2.0-0t64 libcairo2 libcups2t64 libdbus-1-3 libdrm2 libgbm1 libglib2.0-0t64 libnspr4 libnss3 libpango-1.0-0 libx11-6 libxcb1 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2 \ + texlive-latex-base texlive-latex-extra texlive-fonts-recommended texlive-xetex texlive-science \ + qemu-user-static binfmt-support qemu-user-binfmt \ + emacs-nox \ + libportaudio2 && \ + rm -rf /var/lib/apt/lists/* + +# Non-root user for runtime; UID can be overridden via HERMES_UID at runtime +RUN useradd -u 10000 -m -d /opt/data hermes + +COPY --chmod=0755 --from=gosu_source /gosu /usr/local/bin/ +COPY --chmod=0755 --from=uv_source /usr/local/bin/uv /usr/local/bin/uvx /usr/local/bin/ + +WORKDIR /opt/hermes + +# ---------- Layer-cached dependency install ---------- +# Copy only package manifests first so npm install + Playwright are cached +# unless the lockfiles themselves change. +COPY package.json package-lock.json ./ +COPY web/package.json web/package-lock.json web/ + +RUN npm install --prefer-offline --no-audit && \ + npx playwright install --with-deps chromium --only-shell && \ + (cd web && npm install --prefer-offline --no-audit) && \ + npm cache clean --force + +# ---------- Source code ---------- +# .dockerignore excludes node_modules, so the installs above survive. +COPY --chown=hermes:hermes . . + +# Build web dashboard (Vite outputs to hermes_cli/web_dist/) +RUN cd web && npm run build + +# ---------- Permissions ---------- +# Make install dir world-readable so any HERMES_UID can read it at runtime. +# The venv needs to be traversable too. +USER root +RUN chmod -R a+rX /opt/hermes +# Start as root so the entrypoint can usermod/groupmod + gosu. +# If HERMES_UID is unset, the entrypoint drops to the default hermes user (10000). + +# ---------- Python virtualenv ---------- +RUN uv venv && \ + uv pip install --no-cache-dir -e ".[all]" && \ + uv pip install --no-cache-dir sounddevice numpy faster-whisper + +# ---------- Runtime ---------- +ENV HERMES_WEB_DIST=/opt/hermes/hermes_cli/web_dist +ENV HERMES_HOME=/opt/data +ENV PATH="/opt/data/.local/bin:${PATH}" +VOLUME [ "/opt/data" ] +ENTRYPOINT [ "/usr/bin/tini", "-g", "--", "/opt/hermes/docker/entrypoint.sh" ] diff --git a/hosts/lazyworkhorse/configuration.nix b/hosts/lazyworkhorse/configuration.nix index 1593b0f..9485442 100644 --- a/hosts/lazyworkhorse/configuration.nix +++ b/hosts/lazyworkhorse/configuration.nix @@ -158,7 +158,7 @@ settings = { PasswordAuthentication = false; KbdInteractiveAuthentication = false; - PermitRootLogin = "prohibit-password"; + # Additional hardening settings below in SERVER HARDENING section }; hostKeys = [ { @@ -308,6 +308,176 @@ # Or disable the firewall altogether. # networking.firewall.enable = false; + # ============================================================================= + # SERVER HARDENING - Firewall, Fail2ban, SSH, Kernel + # ============================================================================= + + # Firewall - default deny, explicit allow + networking.firewall = { + enable = true; + allowPing = true; + defaultAllow = false; + + # Only essential ports exposed to internet + allowedTCPPorts = [ + 2424 # SSH (non-standard port) + 2222 # Gitea (version control) + 80 # HTTP (Traefik redirect) + 443 # HTTPS (Traefik) + # 8000 # Portainer - REVIEW: internal only? + # 4242 # Coms - REVIEW: internal only? + # 5000 # TAK API - REVIEW: internal only? + # 8087 # TAK Connect - REVIEW: internal only? + # 8089 # TAK Management - REVIEW: internal only? + ]; + + allowedUDPPorts = [ + # Add UDP ports if required + ]; + + # Rate limiting and attack prevention + extraCommands = '' + # Rate limit SSH connections (max 4 new connections per 60 seconds) + iptables -A INPUT -p tcp --dport 2424 -m state --state NEW -m recent --set + iptables -A INPUT -p tcp --dport 2424 -m state --state NEW -m recent --update --seconds 60 --hitcount 4 -j DROP + + # Rate limit HTTP/HTTPS (protects Traefik) + iptables -A INPUT -p tcp --dport 80 -m state --state NEW -m limit --limit 25/minute --limit-burst 100 -j ACCEPT + iptables -A INPUT -p tcp --dport 443 -m state --state NEW -m limit --limit 25/minute --limit-burst 100 -j ACCEPT + + # Drop invalid packets + iptables -A INPUT -m state --state INVALID -j DROP + + # Log dropped packets (rate limited) + iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "IPTables-Dropped: " --log-level 4 + ''; + }; + + # Fail2ban - automatic IP banning + services.fail2ban = { + enable = true; + maxRetry = 3; + findtime = 600; + bantime = 3600; + banaction = "iptables-multiport"; + + # Ban repeat offenders for 1 week + recidive = { + enabled = true; + filter = "recidive"; + logpath = "/var/log/fail2ban.log"; + bantime = 604800; + findtime = 86400; + maxretry = 3; + }; + + jails = { + # SSH brute force protection + sshd = { + enabled = true; + filter = "sshd"; + port = "2424"; + logpath = "/var/log/auth.log"; + maxretry = 3; + bantime = 3600; + }; + + # HTTP authentication failures + http-auth = { + enabled = true; + filter = "apache-auth"; + port = "80,443"; + logpath = "/var/log/traefik/access.log"; + maxretry = 5; + bantime = 3600; + }; + + # HTTP scanning/attacks + http-botsearch = { + enabled = true; + filter = "apache-botsearch"; + port = "80,443"; + logpath = [ "/var/log/traefik/access.log" ]; + maxretry = 2; + bantime = 7200; + }; + }; + }; + + # SSH hardening + services.openssh.settings = { + PermitRootLogin = "no"; + MaxAuthTries = 3; + MaxSessions = 5; + LoginGraceTime = 30; + ClientAliveInterval = 300; + ClientAliveCountMax = 2; + PermitEmptyPasswords = "no"; + ChallengeResponseAuthentication = "no"; + UsePAM = true; + LogLevel = "VERBOSE"; + X11Forwarding = false; + AllowTcpForwarding = "no"; + AllowAgentForwarding = "no"; + PermitTunnel = "no"; + }; + + # Kernel network hardening + boot.kernel.sysctl = { + # IP Spoofing protection + "net.ipv4.conf.all.rp_filter" = 1; + "net.ipv4.conf.default.rp_filter" = 1; + + # Ignore ICMP broadcasts + "net.ipv4.icmp_echo_ignore_broadcasts" = 1; + + # Disable source routing + "net.ipv4.conf.all.accept_source_route" = 0; + "net.ipv4.conf.default.accept_source_route" = 0; + "net.ipv6.conf.all.accept_source_route" = 0; + "net.ipv6.conf.default.accept_source_route" = 0; + + # Disable redirects + "net.ipv4.conf.all.send_redirects" = 0; + "net.ipv4.conf.default.send_redirects" = 0; + + # SYN flood protection + "net.ipv4.tcp_syncookies" = 1; + "net.ipv4.tcp_max_syn_backlog" = 2048; + "net.ipv4.tcp_synack_retries" = 2; + "net.ipv4.tcp_syn_retries" = 5; + + # Log martian packets + "net.ipv4.conf.all.log_martians" = 1; + "net.ipv4.conf.default.log_martians" = 1; + + # Ignore redirects + "net.ipv4.conf.all.accept_redirects" = 0; + "net.ipv4.conf.default.accept_redirects" = 0; + "net.ipv4.conf.all.secure_redirects" = 0; + "net.ipv4.conf.default.secure_redirects" = 0; + "net.ipv6.conf.all.accept_redirects" = 0; + "net.ipv6.conf.default.accept_redirects" = 0; + + # Connection tuning + "net.core.somaxconn" = 4096; + "net.core.netdev_max_backlog" = 65536; + "net.ipv4.tcp_max_orphans" = 65536; + "net.ipv4.tcp_fin_timeout" = 15; + "net.ipv4.tcp_keepalive_time" = 300; + "net.ipv4.tcp_keepalive_probes" = 5; + "net.ipv4.tcp_keepalive_intvl" = 15; + }; + + # Audit logging + services.auditd.enable = true; + + # Fail2ban log directory + systemd.tmpfiles.rules = [ + "d /var/log/fail2ban 0755 root root -" + "d /var/log/traefik 0755 root root -" + ]; + # Copy the NixOS configuration file and link it from the resulting system # (/run/current-system/configuration.nix). This is useful in case you # accidentally delete configuration.nix.