diff --git a/hosts/box/default.nix b/hosts/box/default.nix index f664638..e5120bf 100644 --- a/hosts/box/default.nix +++ b/hosts/box/default.nix @@ -30,6 +30,7 @@ ../profiles/gpodder ../profiles/transmission ../profiles/raven + ../profiles/radicle-node # ../profiles/postgres_upgrade_script # one-time use ]; @@ -58,6 +59,7 @@ "/tank/postgres" # postgres data "/tank/media/photos" "/tank/media/music" + "/var/lib/radicle" ]; }; diff --git a/hosts/helix/default.nix b/hosts/helix/default.nix index 62bf183..18f933f 100644 --- a/hosts/helix/default.nix +++ b/hosts/helix/default.nix @@ -1,18 +1,25 @@ -{ self, profiles, suites, pkgs, ... }: +{ + self, + profiles, + suites, + pkgs, + ... +}: { imports = [ ./configuration.nix ../profiles/core ../profiles/server # ../profiles/metrics - ../profiles/gitea + # ../profiles/gitea # Replaced by radicle + ../profiles/radicle-seed # ../profiles/woodpecker-server ../profiles/rss-bridge # ../profiles/mount-mossnet ../profiles/freshrss ../profiles/microbin ../profiles/site - + # ../profiles/postgres_upgrade_script ]; @@ -50,7 +57,7 @@ services.postgresqlBackup = { # TODO needs working wireguard to box enable = false; - databases = [ "gitea" "freshrss" ]; # "woodpecker" + databases = [ "freshrss" ]; # gitea removed (now using radicle) location = "/mnt/two/postgres"; }; @@ -58,7 +65,8 @@ enable = true; name = "helix"; paths = [ - "/var/lib/gitea" + # "/var/lib/gitea" # Replaced by radicle + "/var/lib/radicle" "/var/lib/freshrss" # "/var/lib/woodpecker" "/var/lib/microbin" diff --git a/hosts/profiles/dns/default.nix b/hosts/profiles/dns/default.nix index a5eb662..f81f4a1 100644 --- a/hosts/profiles/dns/default.nix +++ b/hosts/profiles/dns/default.nix @@ -1,14 +1,22 @@ -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: let adblockLocalZones = pkgs.stdenv.mkDerivation { name = "unbound-zones-adblock"; - src = (pkgs.fetchFromGitHub { - owner = "StevenBlack"; - repo = "hosts"; - rev = "3.12.21"; - sha256 = "Yzr6PY/zqQE+AHH0J6ioHTsgkikM+dz4aelbGpQJa1s="; - } + "/hosts"); + src = ( + pkgs.fetchFromGitHub { + owner = "StevenBlack"; + repo = "hosts"; + rev = "3.12.21"; + sha256 = "Yzr6PY/zqQE+AHH0J6ioHTsgkikM+dz4aelbGpQJa1s="; + } + + "/hosts" + ); phases = [ "installPhase" ]; @@ -40,9 +48,11 @@ let "photos.mossnet.lan" "pod.mossnet.lan" "mast.mossnet.lan" + "rad.mossnet.lan" ]; -in { +in +{ services.unbound = { enable = true; settings = { @@ -53,9 +63,17 @@ in { # private-address = "192.168.1.0/24"; cache-min-ttl = 0; serve-expired = "yes"; - interface = [ "0.0.0.0" "::" ]; - access-control = - [ "127.0.0.0/8 allow" "192.168.1.0/24 allow" "10.0.69.0/24 allow" "::1 allow" "fd7d:587a:4300:1::/64 allow" ]; + interface = [ + "0.0.0.0" + "::" + ]; + access-control = [ + "127.0.0.0/8 allow" + "192.168.1.0/24 allow" + "10.0.69.0/24 allow" + "::1 allow" + "fd7d:587a:4300:1::/64 allow" + ]; access-control-view = "10.0.69.0/24 wireguard"; # so-reuseport = "yes"; tls-upstream = "yes"; @@ -63,15 +81,17 @@ in { local-zone = ''"mossnet.lan." redirect''; local-data = ''"mossnet.lan. IN A ${mossnet}"''; }; - forward-zone = [{ - name = "."; - forward-addr = [ - "45.90.28.0#6939b9.dns.nextdns.io" - "1.1.1.1@853#cloudflare-dns.com" - ]; - # non-tls - # forward-addr = ["45.90.30.49" "45.90.28.49" "1.1.1.1" "8.8.8.8"] - }]; + forward-zone = [ + { + name = "."; + forward-addr = [ + "45.90.28.0#6939b9.dns.nextdns.io" + "1.1.1.1@853#cloudflare-dns.com" + ]; + # non-tls + # forward-addr = ["45.90.30.49" "45.90.28.49" "1.1.1.1" "8.8.8.8"] + } + ]; view = { name = "wireguard"; local-zone = ''"mossnet.lan." redirect''; diff --git a/hosts/profiles/radicle-node/default.nix b/hosts/profiles/radicle-node/default.nix new file mode 100644 index 0000000..e07c035 --- /dev/null +++ b/hosts/profiles/radicle-node/default.nix @@ -0,0 +1,85 @@ +{ + self, + config, + pkgs, + ... +}: +let + # Custom radicle-explorer configured for local box node + localExplorer = pkgs.radicle-explorer.withConfig { + preferredSeeds = [ + { + hostname = "rad.mossnet.lan"; + port = 80; + scheme = "http"; + } + ]; + }; +in +{ + age.secrets.radicle-box-key.file = "${self}/secrets/radicle-box-key.age"; + age.secrets.radicle-box-key.owner = "radicle"; + + services.radicle = { + enable = true; + privateKeyFile = config.age.secrets.radicle-box-key.path; + publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII2QC5AbaTHCRVzGluWgXUlyBNFDxcLiIeViv81f3TYw mossnet.lan"; + + node = { + listenAddress = "0.0.0.0"; # Listen on all interfaces for local LAN access + listenPort = 8776; + openFirewall = true; + }; + + settings = { + node = { + alias = "mossnet.lan"; + connect = [ "z6MkfPhJnbrHbB4FNcub7weT8CRcqFgfJinDfSYjPwK9tSXy@10.0.69.5:8776" ]; + seedingPolicy.default = "block"; + }; + }; + + # HTTP API for local web access + httpd = { + enable = true; + listenAddress = "127.0.0.1"; + listenPort = 8888; + }; + }; + + # Nginx to serve radicle-explorer + proxy API + services.nginx = { + enable = true; + recommendedProxySettings = true; + + virtualHosts."rad.mossnet.lan" = { + root = localExplorer; + + locations."/" = { + tryFiles = "$uri $uri/ /index.html"; + index = "index.html"; + }; + + # Proxy API requests to radicle-httpd + locations."/api" = { + proxyPass = "http://127.0.0.1:8888"; + }; + + # Proxy raw file access to radicle-httpd + locations."/raw" = { + proxyPass = "http://127.0.0.1:8888"; + }; + + # Proxy git protocol requests (rad:xxx) to radicle-httpd + locations."~ ^/rad:" = { + proxyPass = "http://127.0.0.1:8888"; + }; + }; + }; + + # Open firewall for nginx + networking.firewall.allowedTCPPorts = [ 80 ]; + + # rad CLI for interactive use + environment.systemPackages = [ pkgs.radicle-node ]; +} diff --git a/hosts/profiles/radicle-seed/default.nix b/hosts/profiles/radicle-seed/default.nix new file mode 100644 index 0000000..6ddba53 --- /dev/null +++ b/hosts/profiles/radicle-seed/default.nix @@ -0,0 +1,85 @@ +{ + self, + config, + pkgs, + lib, + ... +}: +let + # Custom radicle-explorer with our seed as preferred + customExplorer = pkgs.radicle-explorer.withConfig { + preferredSeeds = [ + { + hostname = "git.sealight.xyz"; + port = 443; + scheme = "https"; + } + ]; + }; +in +{ + age.secrets.radicle-helix-key.file = "${self}/secrets/radicle-helix-key.age"; + age.secrets.radicle-helix-key.owner = "radicle"; + + services.radicle = { + enable = true; + privateKeyFile = config.age.secrets.radicle-helix-key.path; + publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA3x7XH24gEr8xHnt1qKQx38Se2AoXiUnb48/VwfL8/A git.sealight.xyz"; + + node = { + listenAddress = "0.0.0.0"; + listenPort = 8776; + openFirewall = true; + }; + + settings = { + node = { + alias = "git.sealight.xyz"; + externalAddresses = [ "git.sealight.xyz:8776" ]; + connect = [ "z6MkoyrvcRdeGU5PyB2SbHj9mNj3nb5p34rZamkEz64GX1c3@10.0.69.4:8776" ]; + seedingPolicy.default = "block"; + }; + }; + + httpd = { + enable = true; + listenAddress = "127.0.0.1"; + listenPort = 8080; + # Don't use the module's nginx integration - we'll configure it manually + nginx = null; + }; + }; + + # Configure nginx manually for radicle-explorer + httpd API + services.nginx.virtualHosts."git.sealight.xyz" = { + enableACME = true; + forceSSL = true; + + # Serve radicle-explorer static files at root + root = customExplorer; + + locations."/" = { + tryFiles = "$uri $uri/ /index.html"; + index = "index.html"; + }; + + # Proxy API requests to radicle-httpd + locations."/api" = { + proxyPass = "http://127.0.0.1:8080"; + recommendedProxySettings = true; + }; + + # Proxy raw file access to radicle-httpd + locations."/raw" = { + proxyPass = "http://127.0.0.1:8080"; + recommendedProxySettings = true; + }; + + # Proxy git protocol requests (rad:xxx) to radicle-httpd + # These are requests to /:rid/* where rid starts with "rad:" + locations."~ ^/rad:" = { + proxyPass = "http://127.0.0.1:8080"; + recommendedProxySettings = true; + }; + }; +} diff --git a/scripts/migrate-forgejo-to-radicle.sh b/scripts/migrate-forgejo-to-radicle.sh new file mode 100755 index 0000000..7f88776 --- /dev/null +++ b/scripts/migrate-forgejo-to-radicle.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Migration script: Forgejo bare repos -> Radicle +# +# Usage: +# 1. First rsync repos from helix: +# rsync -avz git.sealight.xyz:/var/lib/gitea/repositories/aynish/ /tmp/forgejo-migration/ +# +# 2. Run this script: +# sudo -u radicle ./migrate-forgejo-to-radicle.sh /tmp/forgejo-migration +# +# Or run as root: +# ./migrate-forgejo-to-radicle.sh /tmp/forgejo-migration + +SOURCE_DIR="${1:-/tmp/forgejo-migration}" +WORK_DIR="/var/lib/radicle/migration" +LOG_FILE="/var/lib/radicle/migration.log" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log() { echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"; } +log_success() { log "${GREEN}✓ $1${NC}"; } +log_warn() { log "${YELLOW}⚠ $1${NC}"; } +log_error() { log "${RED}✗ $1${NC}"; } + +# Use rad directly with proper environment (rad-system can't access files outside sandbox) +export RAD_HOME=/var/lib/radicle +export HOME=/var/lib/radicle +RAD_CMD="rad" + +# Ensure keys are in place (agenix stores them in /run/agenix/) +if [[ -f /run/agenix/radicle-box-key ]] && [[ ! -s /var/lib/radicle/keys/radicle ]]; then + log "Setting up radicle keys..." + cp /run/agenix/radicle-box-key /var/lib/radicle/keys/radicle + chmod 600 /var/lib/radicle/keys/radicle + chown radicle:radicle /var/lib/radicle/keys/radicle +fi + +# Get public key from the config if not present +if [[ ! -s /var/lib/radicle/keys/radicle.pub ]]; then + log "Generating public key..." + ssh-keygen -y -f /var/lib/radicle/keys/radicle > /var/lib/radicle/keys/radicle.pub 2>/dev/null || true + chown radicle:radicle /var/lib/radicle/keys/radicle.pub +fi + +# Check source directory +if [[ ! -d "$SOURCE_DIR" ]]; then + log_error "Source directory not found: $SOURCE_DIR" + echo "" + echo "First rsync repos from helix:" + echo " rsync -avz git.sealight.xyz:/var/lib/gitea/repositories/aynish/ /tmp/forgejo-migration/" + exit 1 +fi + +# Create work directory +mkdir -p "$WORK_DIR" +cd "$WORK_DIR" + +log "Starting Forgejo -> Radicle migration" +log "Source: $SOURCE_DIR" +log "Work dir: $WORK_DIR" +log "Using: $RAD_CMD" + +# Count repos +REPOS=$(find "$SOURCE_DIR" -maxdepth 1 -name "*.git" -type d | sort) +TOTAL=$(echo "$REPOS" | grep -c . || echo 0) + +if [[ "$TOTAL" -eq 0 ]]; then + log_error "No .git directories found in $SOURCE_DIR" + exit 1 +fi + +log "Found $TOTAL repositories to migrate" +echo "" + +MIGRATED=0 +SKIPPED=0 +FAILED=0 + +for BARE_REPO in $REPOS; do + # Extract repo name (remove .git suffix) + REPO_NAME=$(basename "$BARE_REPO" .git) + + log "Processing: $REPO_NAME" + + CLONE_DIR="$WORK_DIR/$REPO_NAME" + + # Clone bare repo to working directory + if [[ -d "$CLONE_DIR/.git" ]]; then + log " Directory exists, skipping clone" + else + log " Cloning from bare repo..." + if ! git clone --quiet "$BARE_REPO" "$CLONE_DIR" 2>>"$LOG_FILE"; then + log_error " Failed to clone $REPO_NAME" + ((FAILED++)) || true + continue + fi + fi + + cd "$CLONE_DIR" + + # Check if already initialized in Radicle + if $RAD_CMD . >/dev/null 2>&1; then + log_warn " Already initialized in Radicle, skipping" + cd "$WORK_DIR" + ((SKIPPED++)) || true + continue + fi + + # Get current branch + CURRENT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "main") + + # Normalize to main if needed + if [[ "$CURRENT_BRANCH" != "main" ]]; then + log " Normalizing branch: $CURRENT_BRANCH -> main" + git branch -m "$CURRENT_BRANCH" main 2>>"$LOG_FILE" || true + fi + + # Get description from git config if available + DESCRIPTION=$(git config --get gitweb.description 2>/dev/null || echo "Migrated from Forgejo") + + # Truncate description if too long + if [[ ${#DESCRIPTION} -gt 255 ]]; then + DESCRIPTION="${DESCRIPTION:0:252}..." + fi + + # Initialize in Radicle + log " Initializing in Radicle..." + if $RAD_CMD init \ + --name "$REPO_NAME" \ + --description "$DESCRIPTION" \ + --default-branch main \ + --public 2>>"$LOG_FILE"; then + + # Push to radicle + log " Pushing to Radicle..." + if git push rad main 2>>"$LOG_FILE"; then + RID=$($RAD_CMD . 2>/dev/null || echo "unknown") + log_success " Migrated: $REPO_NAME -> $RID" + ((MIGRATED++)) || true + else + log_error " Failed to push $REPO_NAME" + ((FAILED++)) || true + fi + else + log_error " Failed to init $REPO_NAME in Radicle" + ((FAILED++)) || true + fi + + cd "$WORK_DIR" +done + +echo "" +log "========================================" +log "Migration Summary:" +log " Total repos: $TOTAL" +log " Migrated: $MIGRATED" +log " Skipped: $SKIPPED" +log " Failed: $FAILED" +log "========================================" +echo "" +log "Verify with: $RAD_CMD ls" +log "Configure helix to follow: rad-system follow --alias mossnet" diff --git a/secrets/radicle-box-key.age b/secrets/radicle-box-key.age new file mode 100644 index 0000000..0b2213e Binary files /dev/null and b/secrets/radicle-box-key.age differ diff --git a/secrets/radicle-helix-key.age b/secrets/radicle-helix-key.age new file mode 100644 index 0000000..04b6ede --- /dev/null +++ b/secrets/radicle-helix-key.age @@ -0,0 +1,9 @@ +age-encryption.org/v1 +-> ssh-ed25519 kgCrKA WzvIX43OcJw/qxPgTw3iUjorQ0ahFHYZEmnghnUz/xc +HtNx7XqWFZDNpjNiquxfRrnP/TcXxoPn6mMB0BMjlJw +-> ssh-ed25519 6DaCrw dZ9etYRaGuAO0gcA3Krdgvcg9vkwctAtJzKevgjE3wc +gbWQHzO//bGp3Jl513GIgzaM4FzJSiA5thOSWWsx7ik +--- QeO3vdOBbisSZaBECB1Wa603H6P7lXDJwxerJ8gH4+M +lGX.$lp^^ 0zK88QuQq;Y85RM4I:gqpܺ,OdPkRrqUM!=Eu2 [_!%CE +"o 3wl" :T6T[[PnqB 7'F8:6Eͳʽ\ #1ĞKj.Ь)VBuN "S,^Dŝ&Ʃh%*gvN^-2PBb'? LjF,-b:,x#į UY_Qp+j!U;Aјr5ӏ0A|<=jߏsq;b!BW'ݴ8As#= i`|N٬Ko̰H)ް +?" 7pWҏ \ No newline at end of file diff --git a/secrets/secrets.nix b/secrets/secrets.nix index da8fae8..6ce9c7d 100644 --- a/secrets/secrets.nix +++ b/secrets/secrets.nix @@ -7,9 +7,21 @@ let helix = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAKrL6IDHNnHmxi0q9nzu87NOyidPm3HpE7klU368lEf root@helix"; helix2 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK2G81z1E51ioJQGLHnTJEjgSdBqLM6mb72Z+0atE6Bf root@helix"; work = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHOnfDvR2D2nGnC+DZYDUXiokzz+eLfZwkp+O8WjWutp anishlakhwara@Anishs-MacBook-Pro.local"; - curve = [ system user ]; - allUserKeys = [ system user mossnet ]; - systemOnly = [ system mossnet lituus helix ]; + curve = [ + system + user + ]; + allUserKeys = [ + system + user + mossnet + ]; + systemOnly = [ + system + mossnet + lituus + helix + ]; in { "taskwarrior-private.age".publicKeys = curve; @@ -20,10 +32,22 @@ in "borg-password.age".publicKeys = systemOnly; "borg-key.age".publicKeys = systemOnly; - "helix-wg.age".publicKeys = [ helix2 helix ]; - "freshrss-dbpass.age".publicKeys = [ helix2 helix ]; - "woodpecker-server-secrets.age".publicKeys = [ helix2 helix ]; - "gitea-dbpass.age".publicKeys = [ helix helix2 ]; + "helix-wg.age".publicKeys = [ + helix2 + helix + ]; + "freshrss-dbpass.age".publicKeys = [ + helix2 + helix + ]; + "woodpecker-server-secrets.age".publicKeys = [ + helix2 + helix + ]; + "gitea-dbpass.age".publicKeys = [ + helix + helix2 + ]; "synapse-config.age".publicKeys = [ lituus ]; "synapse-database-password.age".publicKeys = [ lituus ]; @@ -31,12 +55,37 @@ in "telegram-matrix-env.age".publicKeys = [ lituus ]; "wallabag.age".publicKeys = [ mossnet ]; - "woodpecker-agent-secret.age".publicKeys = [ mossnet helix helix2 ]; + "woodpecker-agent-secret.age".publicKeys = [ + mossnet + helix + helix2 + ]; "box-wg.age".publicKeys = [ mossnet ]; "wallabag-password.age".publicKeys = [ mossnet ]; "wallabag-secret.age".publicKeys = [ mossnet ]; - "work-wg.age".publicKeys = [ work user system ]; - "github-token.age".publicKeys = [ work user system mossnet ]; - "anthropicToken.age".publicKeys = [ work user system mossnet ]; + "work-wg.age".publicKeys = [ + work + user + system + ]; + "github-token.age".publicKeys = [ + work + user + system + mossnet + ]; + "anthropicToken.age".publicKeys = [ + work + user + system + mossnet + ]; + + # Radicle node keys + "radicle-box-key.age".publicKeys = [ mossnet ]; + "radicle-helix-key.age".publicKeys = [ + helix + helix2 + ]; }