# vultr-security.nix - Hardened security configuration for VPS hosting # # This config implements defense-in-depth for a public XMPP server: # - SSH key-only authentication (no passwords) # - Fail2ban for brute-force prevention # - Firewall with strict rules (iptables via NixOS) # - Disabled root login # - Automatic security updates # - Minimal attack surface # # Usage: # 1. Generate SSH key locally: ssh-keygen -t ed25519 -C "your@email.com" # 2. Add your public key to 'authorizedKeys' below # 3. Import this module in configuration.nix # 4. nixos-rebuild switch # 5. IMPORTANT: Test SSH works BEFORE logging out! (ssh -i ~/.ssh/id_ed25519 youruser@server) # { config, pkgs, lib, ... }: { # ============================================================================ # SSH Configuration - Key-only, no root, no passwords # ============================================================================ services.openssh = { enable = true; ports = [ 22 ]; # Change to non-standard port if you want (e.g., 2222) settings = { # Disable password authentication entirely PasswordAuthentication = false; KbdInteractiveAuthentication = false; # No keyboard-interactive auth ChallengeResponseAuthentication = false; # Disable root login (force use of sudo from normal user) PermitRootLogin = "no"; # Only allow public key authentication PubkeyAuthentication = true; # Additional hardening PermitEmptyPasswords = false; X11Forwarding = false; # Disable X11 forwarding (not needed) PrintMotd = false; # Prevent SSH from being used as a tunnel AllowTcpForwarding = false; AllowStreamLocalForwarding = false; GatewayPorts = "no"; # Limit login attempts MaxAuthTries = 3; MaxSessions = 2; # Use strong ciphers only Ciphers = [ "chacha20-poly1305@openssh.com" "aes256-gcm@openssh.com" "aes128-gcm@openssh.com" "aes256-ctr" "aes192-ctr" "aes128-ctr" ]; KexAlgorithms = [ "curve25519-sha256" "curve25519-sha256@libssh.org" "diffie-hellman-group16-sha512" "diffie-hellman-group18-sha512" "diffie-hellman-group-exchange-sha256" ]; Macs = [ "hmac-sha2-512-etm@openssh.com" "hmac-sha2-256-etm@openssh.com" "umac-128-etm@openssh.com" ]; }; }; # ============================================================================ # User Configuration # ============================================================================ users.users.tony = { # CHANGE THIS to your desired username isNormalUser = true; description = "Tony (Admin)"; # Add user to wheel group for sudo access extraGroups = [ "wheel" "networkmanager" ]; # SSH public keys (ADD YOUR KEY HERE!) openssh.authorizedKeys.keys = [ # Add your public key from ~/.ssh/id_ed25519.pub # Example format: # "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx your@email.com" # IMPORTANT: Replace this with your actual public key! "ssh-ed25519 REPLACE_THIS_WITH_YOUR_PUBLIC_KEY your@email.com" ]; # Set a strong password as backup (in case SSH key is lost) # Generate hash with: mkpasswd -m sha-512 # hashedPassword = "$6$rounds=500000$..."; # Uncomment and add hashed password }; # Disable root password (force sudo from normal user) users.users.root.hashedPassword = "!"; # "!" means locked account # ============================================================================ # Sudo Configuration - Secure but usable # ============================================================================ # NOTE: We keep sudo enabled because you need it to manage the server # Disabling sudo would lock you out from system administration security.sudo = { enable = true; # Require password for sudo (even with key-based SSH) execWheelOnly = true; # Only wheel group can sudo wheelNeedsPassword = true; # Require password for sudo commands extraConfig = '' # Require password every time (no timeout) Defaults timestamp_timeout=0 # Log all sudo commands Defaults logfile=/var/log/sudo.log Defaults log_input, log_output # Require password for ALL commands (no exceptions) Defaults passwd_tries=3 Defaults badpass_message="Authentication failed. This incident will be logged." # Prevent sudo -i or sudo su (force explicit commands) # Uncomment these if you want to be extra strict: # Defaults !rootpw # Defaults !runaspw ''; }; # Disable 'su' command (force sudo for all privilege escalation) security.sudo.extraConfig = '' # Block 'su' by preventing root password authentication # Users must use 'sudo' for specific commands instead of 'su' ''; # Remove 'su' from system entirely (optional - uncomment if desired) # environment.systemPackages = lib.mkForce (builtins.filter (pkg: pkg.pname or "" != "shadow") config.environment.systemPackages); # ============================================================================ # Firewall - Only allow necessary ports # ============================================================================ networking.firewall = { enable = true; # Allowed ports (ONLY what's needed) allowedTCPPorts = [ 22 # SSH (change if you modified SSH port above) 80 # HTTP (for Let's Encrypt ACME challenge) 443 # HTTPS (for Let's Encrypt ACME challenge) 5222 # XMPP client-to-server (C2S) 5269 # XMPP server-to-server (S2S) - remove if no federation 5281 # XMPP HTTPS (file uploads) ]; # Block ping (optional - makes server invisible to ping sweeps) allowPing = false; # Advanced firewall rules using iptables extraCommands = '' # Rate-limit SSH connections (prevent brute-force) iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --set --name SSH iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 60 --hitcount 4 --name SSH -j DROP # Rate-limit XMPP connections (prevent abuse) iptables -A INPUT -p tcp --dport 5222 -m state --state NEW -m recent --set --name XMPP iptables -A INPUT -p tcp --dport 5222 -m state --state NEW -m recent --update --seconds 60 --hitcount 10 --name XMPP -j DROP # Drop invalid packets iptables -A INPUT -m state --state INVALID -j DROP # Allow established connections iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT # Log dropped packets (for debugging - comment out in production) # iptables -A INPUT -j LOG --log-prefix "iptables-dropped: " --log-level 4 ''; extraStopCommands = '' iptables -D INPUT -p tcp --dport 22 -m state --state NEW -m recent --set --name SSH 2>/dev/null || true iptables -D INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 60 --hitcount 4 --name SSH -j DROP 2>/dev/null || true iptables -D INPUT -p tcp --dport 5222 -m state --state NEW -m recent --set --name XMPP 2>/dev/null || true iptables -D INPUT -p tcp --dport 5222 -m state --state NEW -m recent --update --seconds 60 --hitcount 10 --name XMPP -j DROP 2>/dev/null || true ''; }; # ============================================================================ # Fail2Ban - Automatic IP banning for repeated failed attempts # ============================================================================ services.fail2ban = { enable = true; # Ban for 1 hour after 3 failed attempts within 10 minutes maxretry = 3; bantime = "1h"; findtime = "10m"; jails = { # SSH brute-force protection sshd = '' enabled = true filter = sshd action = iptables[name=SSH, port=22, protocol=tcp] maxretry = 3 findtime = 600 bantime = 3600 ''; # XMPP brute-force protection # (Custom filter - see below) prosody = '' enabled = true filter = prosody-auth action = iptables[name=XMPP, port=5222, protocol=tcp] logpath = /var/log/prosody/prosody.log maxretry = 5 findtime = 600 bantime = 3600 ''; }; }; # Custom fail2ban filter for Prosody auth failures environment.etc."fail2ban/filter.d/prosody-auth.conf".text = '' [Definition] failregex = ^.* \[error\] .* (authentication failure|certificate expired) for .* from ignoreregex = ''; # ============================================================================ # Automatic Security Updates (Optional - Recommended) # ============================================================================ system.autoUpgrade = { enable = true; dates = "weekly"; # Update once per week allowReboot = false; # Don't auto-reboot (manual intervention) channel = "https://nixos.org/channels/nixos-24.05"; # Stable channel }; # ============================================================================ # Additional Hardening # ============================================================================ # Disable unnecessary services services.avahi.enable = false; # No mDNS/Bonjour services.printing.enable = false; # No CUPS printing # Kernel hardening boot.kernel.sysctl = { # Prevent IP spoofing "net.ipv4.conf.all.rp_filter" = 1; "net.ipv4.conf.default.rp_filter" = 1; # Ignore ICMP redirects "net.ipv4.conf.all.accept_redirects" = 0; "net.ipv4.conf.default.accept_redirects" = 0; "net.ipv6.conf.all.accept_redirects" = 0; "net.ipv6.conf.default.accept_redirects" = 0; # Ignore ICMP echo requests (disable ping response) "net.ipv4.icmp_echo_ignore_all" = 1; # Disable IPv6 if not needed (optional) # "net.ipv6.conf.all.disable_ipv6" = 1; # Enable TCP SYN cookies (prevent SYN flood attacks) "net.ipv4.tcp_syncookies" = 1; # Disable source routing "net.ipv4.conf.all.accept_source_route" = 0; "net.ipv6.conf.all.accept_source_route" = 0; # Log suspicious packets "net.ipv4.conf.all.log_martians" = 1; }; # ============================================================================ # Logging and Monitoring # ============================================================================ services.journald.extraConfig = '' MaxRetentionSec=1month SystemMaxUse=1G ''; # Log failed login attempts security.pam.loginLimits = [ { domain = "*"; type = "hard"; item = "maxlogins"; value = "3"; } ]; # ============================================================================ # Minimal Package Set (Reduce attack surface) # ============================================================================ environment.systemPackages = with pkgs; [ # Essential tools only vim wget curl htop tmux git # Security tools fail2ban iptables # Prosody admin tools # (prosodyctl is included with services.prosody) ]; # Disable documentation to save space and reduce attack surface documentation.enable = false; documentation.man.enable = false; documentation.nixos.enable = false; }