diff --git a/configuration.nix b/configuration.nix index 3b0f4a1..cfde9f4 100644 --- a/configuration.nix +++ b/configuration.nix @@ -1,4 +1,4 @@ -{ pkgs, pkgs-unstable, beads, opencode, ... }: +{ pkgs, pkgs-unstable, opencode, ... }: let # ========================================================================== @@ -41,6 +41,12 @@ let text = builtins.readFile ./scripts/dev-remove.sh; }; + egress-status = pkgs.writeShellApplication { + name = "egress-status"; + runtimeInputs = with pkgs; [ systemd gnugrep gnused coreutils ]; + text = builtins.readFile ./scripts/egress-status; + }; + in { # Main NixOS configuration for ops-jrz1 server @@ -73,7 +79,6 @@ in direnv zig # AI coding tools (via flake inputs) - beads # Issue tracker (bd CLI) opencode # AI coding agent (opencode CLI) # For JS-based AI tools (gemini-cli, claude-cli): users run bun/npm install nodejs_22 @@ -90,6 +95,7 @@ in # Admin scripts (declarative deployment) dev-add dev-remove + egress-status ]; # Add ~/.local/bin and /usr/local/bin to PATH for manually installed tools @@ -142,29 +148,25 @@ in # UID range: 1000 (first regular user) to 65534 (nobody - excluded from controls) # This covers all dev accounts while excluding system services extraCommands = '' - # Log all new outbound connections from regular users + # Rate limit new outbound connections (150/min sustained, burst 300) + # High enough for npm install, low enough to prevent abuse + # DROP instead of REJECT so apps back off naturally iptables -A OUTPUT -m state --state NEW -m owner --uid-owner 1000:65534 \ - -j LOG --log-prefix "EGRESS: " --log-level info - - # Rate limit new outbound connections (30/min sustained, burst 60) - iptables -A OUTPUT -m state --state NEW -m owner --uid-owner 1000:65534 \ - -m limit --limit 30/min --limit-burst 60 -j ACCEPT + -m limit --limit 150/min --limit-burst 300 -j ACCEPT iptables -A OUTPUT -m state --state NEW -m owner --uid-owner 1000:65534 \ -j LOG --log-prefix "EGRESS-LIMIT: " --log-level warning iptables -A OUTPUT -m state --state NEW -m owner --uid-owner 1000:65534 \ - -j REJECT + -j DROP ''; # Clean up on stop extraStopCommands = '' iptables -D OUTPUT -m state --state NEW -m owner --uid-owner 1000:65534 \ - -j LOG --log-prefix "EGRESS: " --log-level info 2>/dev/null || true - iptables -D OUTPUT -m state --state NEW -m owner --uid-owner 1000:65534 \ - -m limit --limit 30/min --limit-burst 60 -j ACCEPT 2>/dev/null || true + -m limit --limit 150/min --limit-burst 300 -j ACCEPT 2>/dev/null || true iptables -D OUTPUT -m state --state NEW -m owner --uid-owner 1000:65534 \ -j LOG --log-prefix "EGRESS-LIMIT: " --log-level warning 2>/dev/null || true iptables -D OUTPUT -m state --state NEW -m owner --uid-owner 1000:65534 \ - -j REJECT 2>/dev/null || true + -j DROP 2>/dev/null || true ''; }; diff --git a/docs/dev-slack-direct.md b/docs/dev-slack-direct.md index d2dc86c..0ec0a33 100644 --- a/docs/dev-slack-direct.md +++ b/docs/dev-slack-direct.md @@ -21,20 +21,20 @@ Devs write Python bots using `slack-bolt` that connect directly to Slack via Soc ### Credentials -Shared Slack App tokens stored in `/etc/slack-dev.env`: +Shared Slack App tokens managed via sops-nix, deployed to `/run/secrets/`: -| Variable | Purpose | -|----------|---------| -| `SLACK_BOT_TOKEN` | Bot identity (xoxb-...) | -| `SLACK_APP_TOKEN` | Socket Mode connection (xapp-...) | +| Secret File | Purpose | +|-------------|---------| +| `/run/secrets/slack-bot-token` | Bot identity (xoxb-...) | +| `/run/secrets/slack-app-token` | Socket Mode connection (xapp-...) | These come from the existing mautrix-slack bridge login (Chochacho workspace, vlad's account). ### Access Control -- File owned by `root:devs`, mode `640` +- Files owned by `root:devs`, mode `0440` - Dev users added to `devs` group on creation -- `.bashrc` sources the env file on login +- Secrets encrypted in git, decrypted at boot via sops-nix ### Scripts @@ -58,8 +58,12 @@ ssh root@ops-jrz1 'dev-add.sh alice "ssh-ed25519 AAAA..."' ssh alice@ ``` -Tokens are available immediately: +Load tokens from secrets: ```bash +export SLACK_BOT_TOKEN=$(cat /run/secrets/slack-bot-token) +export SLACK_APP_TOKEN=$(cat /run/secrets/slack-app-token) + +# Verify they're set echo $SLACK_BOT_TOKEN # xoxb-... echo $SLACK_APP_TOKEN # xapp-... ``` @@ -72,7 +76,18 @@ from slack_bolt import App from slack_bolt.adapter.socket_mode import SocketModeHandler import os -app = App(token=os.environ["SLACK_BOT_TOKEN"]) +# Read tokens from sops-nix secrets (or fall back to env vars) +def read_secret(path, env_fallback): + try: + with open(path) as f: + return f.read().strip() + except FileNotFoundError: + return os.environ.get(env_fallback, "") + +BOT_TOKEN = read_secret("/run/secrets/slack-bot-token", "SLACK_BOT_TOKEN") +APP_TOKEN = read_secret("/run/secrets/slack-app-token", "SLACK_APP_TOKEN") + +app = App(token=BOT_TOKEN) @app.message("hello") def hello(message, say): @@ -81,11 +96,11 @@ def hello(message, say): @app.message("dice") def dice(message, say): import random - say(f"🎲 You rolled a {random.randint(1, 6)}!") + say(f"You rolled a {random.randint(1, 6)}!") if __name__ == "__main__": print("Bot starting...") - SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]).start() + SocketModeHandler(app, APP_TOKEN).start() ``` ### 4. Run It @@ -176,8 +191,8 @@ say(blocks=[ - Dedicated test channel in Slack - Supervisor/systemd for process management (not yet set up) -- Tokens in env vars, not code -- Can rotate tokens if leaked (re-login bridge) +- Tokens in sops-nix secrets, encrypted in git, decrypted only at runtime +- Can rotate tokens if leaked (re-login bridge, update secrets.yaml) ## Future Improvements diff --git a/flake.lock b/flake.lock index c44b49a..3513240 100644 --- a/flake.lock +++ b/flake.lock @@ -1,45 +1,5 @@ { "nodes": { - "beads": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": [ - "nixpkgs-unstable" - ] - }, - "locked": { - "lastModified": 1767403067, - "narHash": "sha256-HQ98HSZo/gJJmp3HrsGWSVbGYkXsnxjBsMof2j3Age0=", - "owner": "steveyegge", - "repo": "beads", - "rev": "32a4a9e0603957c467a03fbec65734ef91acaa0b", - "type": "github" - }, - "original": { - "owner": "steveyegge", - "repo": "beads", - "rev": "32a4a9e0603957c467a03fbec65734ef91acaa0b", - "type": "github" - } - }, - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, "nixpkgs": { "locked": { "lastModified": 1735563628, @@ -111,7 +71,6 @@ }, "root": { "inputs": { - "beads": "beads", "nixpkgs": "nixpkgs", "nixpkgs-unstable": "nixpkgs-unstable", "opencode": "opencode", @@ -139,21 +98,6 @@ "rev": "c2ea1186c0cbfa4d06d406ae50f3e4b085ddc9b3", "type": "github" } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 44e525d..44e6145 100644 --- a/flake.nix +++ b/flake.nix @@ -10,11 +10,6 @@ inputs.nixpkgs.follows = "nixpkgs"; }; - beads = { - url = "github:steveyegge/beads/32a4a9e0603957c467a03fbec65734ef91acaa0b"; - inputs.nixpkgs.follows = "nixpkgs-unstable"; - }; - opencode = { url = "github:sst/opencode/f6fe709f6ee75427ba64829af25b64d9a3111569"; inputs.nixpkgs.follows = "nixpkgs-unstable"; @@ -48,7 +43,6 @@ ]; }; }; - beads = inputs.beads.packages.x86_64-linux.default; opencode = inputs.opencode.packages.x86_64-linux.default; }; modules = [ @@ -71,7 +65,6 @@ ]; }; }; - beads = inputs.beads.packages.x86_64-linux.default; opencode = inputs.opencode.packages.x86_64-linux.default; }; modules = [ diff --git a/hosts/ops-jrz1.nix b/hosts/ops-jrz1.nix index c007323..dc811c7 100644 --- a/hosts/ops-jrz1.nix +++ b/hosts/ops-jrz1.nix @@ -35,11 +35,6 @@ mode = "0400"; }; - sops.secrets.acme-email = { - owner = "root"; - mode = "0444"; - }; - sops.secrets.maubot-admin-password = { # Maubot management interface admin password mode = "0400"; @@ -50,6 +45,19 @@ mode = "0400"; }; + # Slack dev tokens - shared with devs group for learner bot development + sops.secrets.slack-bot-token = { + owner = "root"; + group = "devs"; + mode = "0440"; + }; + + sops.secrets.slack-app-token = { + owner = "root"; + group = "devs"; + mode = "0440"; + }; + # Matrix homeserver configuration # NOTE: Disabled in favor of dev-platform.matrix which provides integrated # bridge coordination and systemd credential-based secrets management diff --git a/scripts/egress-status b/scripts/egress-status new file mode 100644 index 0000000..df5ff2b --- /dev/null +++ b/scripts/egress-status @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Check egress rate limit status for current user +# Users can run this to see if they're hitting connection limits + +set -euo pipefail + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo "=== Egress Rate Limit Status ===" +echo "" + +# Check recent limit hits (last 5 minutes) +RECENT_HITS=$(journalctl --since "5 minutes ago" -q 2>/dev/null | grep -c "EGRESS-LIMIT:" 2>/dev/null || true) +RECENT_HITS=${RECENT_HITS:-0} +RECENT_HITS=$(echo "$RECENT_HITS" | tr -d '[:space:]') + +if [ "$RECENT_HITS" -gt 0 ] 2>/dev/null; then + echo -e "${RED}⚠ Rate limit hit ${RECENT_HITS} times in the last 5 minutes${NC}" + echo "" + echo "Recent blocked connections:" + journalctl --since "5 minutes ago" -q 2>/dev/null | grep "EGRESS-LIMIT:" | tail -5 | \ + sed 's/.*DST=\([^ ]*\).*/ → \1/' || true + echo "" + echo -e "${YELLOW}Tip: Wait 1-2 minutes for the limit to reset, or run commands with fewer parallel connections.${NC}" +else + echo -e "${GREEN}✓ No rate limit hits in the last 5 minutes${NC}" +fi + +echo "" +echo "Current limits: 150 new connections/min, burst 300" +echo "Check logs: journalctl --since '1 hour ago' | grep EGRESS-LIMIT" diff --git a/secrets/secrets.yaml b/secrets/secrets.yaml index bcf86ec..5a085fc 100644 --- a/secrets/secrets.yaml +++ b/secrets/secrets.yaml @@ -1,9 +1,10 @@ matrix-registration-token: ENC[AES256_GCM,data:H7BgtpsDLOYcywjOHru+u7t6BCbqhFrmPS3YXJWnMVcppD4lVh6ewZB/ZPM2ck5OcBQe8gmCYNGKchzPf0aeRw==,iv:9b8gPuxQaJIGep/YHpA02/yJx13bJZ3r6WmKEXRGFDc=,tag:/NxCSqkwPxhEOeWM+/3Hhg==,type:str] acme-email: ENC[AES256_GCM,data:+tN+nRfn2kpGLdF3Vg==,iv:uZvSw4viBWCTT35C718cLOCrSLM1EnkmEZH644aVuPI=,tag:tf6+7ubiOLVj7k4rfNI3lQ==,type:str] -slack-oauth-token: "" -slack-app-token: "" +slack-oauth-token: null +slack-app-token: ENC[AES256_GCM,data:YRSu3h7xU9V6ymvOGa7lBRtUq794j/bh5gCOTBvNJXw+g6m3ypiJYOYVM6iN6hBLNQSPDtfxVnJiwiV00jcneYdTWN54RWzyU1O2yrVb10DA1GK2dOVFcfDjncdAVCsJwQ==,iv:wH6CTsLixT4kU5u8o0xd9Yyqy9wjomHIZoyg8aOP6ko=,tag:x9yh4qJolHzMMMramz+kjg==,type:str] maubot-admin-password: ENC[AES256_GCM,data:Omh6VFsnlLgS+UktM5qHjj3+VK84YmMgWcQCvkiMchfb621RV0LBg1ZB3tg=,iv:cINVFlHJJGkAcasK8BJr3Sd2zqkpQOyRgF+V0JhBJXE=,tag:PnS9TdtuR/87yQfttJTLow==,type:str] maubot-secret-key: ENC[AES256_GCM,data:krq8zjZelAYRNrFs+DYqh7j0bDd80YKRkro88hGiAxJOBCuFV6PdyyUKgqdSuGMhoFhZtMPmRKOQvAxKclOBEQ==,iv:PePSXEOcBKcReXYBzicDhGQ/yxJIZ/TNzARg4z9G7dA=,tag:ihVw9PAXScoZgrSzWkAMdQ==,type:str] +slack-bot-token: ENC[AES256_GCM,data:Ll4Ej2z8C810VsTtHNKdB+o4t43736dbNMBhbX9jFl3+l4N6xOpyHSi7EJE2B8Ce8mz1JsLeOvo=,iv:ct5yo21n4aBSVAIBgxzL+FG+P6gIMSP/f1UktVc7ya4=,tag:qOSkcR/m5KvpdwjxYVIxmQ==,type:str] sops: age: - recipient: age1vuxcwvdvzl2u7w6kudqvnnf45czrnhwv9aevjq9hyjjpa409jvkqhkz32q @@ -24,7 +25,7 @@ sops: TzI2NGdaVHd1RFZWRE50bjZ0cHhBOXMKRXVYFMNxNIX+8uVxf1X4hu+OfOKKs2TK A2qdAMJIfdy9f7SPVrPnrGMIwl/prxIkbSRwYC/UNK5NNkjMrGoSwg== -----END AGE ENCRYPTED FILE----- - lastmodified: "2025-10-27T04:21:51Z" - mac: ENC[AES256_GCM,data:k1aBVnSUnpgq1y+AQjZFB7AXmQe2r/SpSVl9xVsJku2/lehBfY6vRGZutRHV4iTaB3FmxwgGCOV29gPZ5NGUQDf9tg5hMacZOREJGd7lMWoSlZbCGjjkOQEvpKLq3kJNuV66Lb1LzKQtR6ws5k/EmnXneyDtjuEbFs4AZZi+WRE=,iv:zc58CMvJqPsKbANOCGLBuo+AiUnoF4Wx3Z33j6a+sfI=,tag:ENek+3uial24ladKBqW3sg==,type:str] + lastmodified: "2026-01-07T18:19:38Z" + mac: ENC[AES256_GCM,data:BkDv0FTRszUUyolWwhlK/hpVZHZvMmsvCZm2g7hDD5kPZGDR1lchsQ6x5rcWEecJ1HBXrFyAWslcuwLT+hBiWObVLi0Fp5VFHodBPHjgbwoFLgfWl9bpCc1TGSLozFxfOVlzZtm2paMl635hQROH1KJLSDXg5r9ZnQNjBgWn0LE=,iv:C1j7YBlagXl31CtNGCROnS0sTA8s1HijBrZ/bkDS+wA=,tag:zPIyXfRFDmDk7qof9C3jDg==,type:str] unencrypted_suffix: _unencrypted version: 3.10.2