Files
cc-ci-orchestrator/nix/hosts/cc-ci-orchestrator-hetzner/configuration.nix
T
autonomic-botandClaude Sonnet 4.6 497bea8462 nix: add root SSH authorized key to cc-ci-orchestrator-hetzner config
nixos-rebuild removed the infect-provisioned authorized_keys — declare it
explicitly so rebuilds don't lock out root access.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 02:35:37 +00:00

135 lines
5.1 KiB
Nix

# cc-ci-orchestrator-hetzner — NixOS config for the Hetzner loops runtime host.
#
# Purpose: run the cc-ci Builder/Adversary/Watchdog loops + orchestrator/assistant sessions
# on a Hetzner cpx11 (2 vCPU / 2 GB dedicated AMD / 40 GB NVMe), replacing the slow b1 Incus VM.
#
# Provision with terraform/ then converge with: nixos-rebuild switch --flake .#cc-ci-orchestrator-hetzner
# See terraform/README.md for the full Stage 2 procedure.
{ config, pkgs, lib, ... }:
{
# hardware.nix is the nixos-infect generated hardware-configuration.nix (see README Stage 2a).
services.openssh = {
enable = true;
settings.PermitRootLogin = "yes";
};
# Root SSH access (the cc-ci-root-ed25519 key used by all loops tooling).
users.users.root.openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAcyTGb/wVgdhg5oBCZZvBaR1RuUQRY/3WHnOQpNDCsp claude-cc-ci-sandbox@20260526"
];
networking.useDHCP = true;
networking.nameservers = [ "1.1.1.1" "8.8.8.8" ];
networking.firewall = {
enable = true;
trustedInterfaces = [ "tailscale0" ];
allowedTCPPorts = [ 22 ];
};
nix.settings.experimental-features = [ "nix-command" "flakes" ];
system.stateVersion = "24.11";
# Tailscale — auth key at /etc/ts-auth-key (placed manually in Stage 2, not in git).
services.tailscale = {
enable = true;
authKeyFile = "/etc/ts-auth-key";
extraUpFlags = [ "--hostname=cc-ci-orchestrator" "--ssh" ];
};
# 4 GB disk swap — claude session memory safety net (2 GB RAM is tight for 3+ sessions).
swapDevices = [ { device = "/swapfile"; size = 4096; } ];
# nix-ld — lets the standalone Claude Code CLI (foreign dynamic ELF / Bun) run on NixOS.
programs.nix-ld.enable = true;
programs.nix-ld.libraries = with pkgs; [
stdenv.cc.cc.lib
zlib
openssl
curl
glibc
];
environment.systemPackages = with pkgs; [
git tmux python3 jq curl cacert
gnused gawk coreutils gnugrep findutils util-linux
nettools openssh
age sops # key management (same toolchain as cc-ci server)
];
# loops user — claude sessions run as non-root (--dangerously-skip-permissions blocked for root).
users.users.loops = {
isNormalUser = true;
home = "/home/loops";
shell = pkgs.bash;
extraGroups = [ "wheel" ];
};
security.sudo.wheelNeedsPassword = false;
security.sudo.extraRules = [{
users = [ "loops" ];
commands = [{ command = "ALL"; options = [ "NOPASSWD" ]; }];
}];
# Ensure /home/loops/.local/bin (claude) is on the loops user PATH.
environment.variables.PATH = lib.mkForce
"/home/loops/.local/bin:/run/current-system/sw/bin:/run/wrappers/bin:/usr/bin:/bin";
# SSH config for the loops user — points to the cc-ci Hetzner server via tailnet.
# HostName is updated post-cutover to the Hetzner cc-ci tailnet IP.
system.activationScripts.loopsSshConfig = ''
mkdir -p /home/loops/.ssh && chown loops:users /home/loops/.ssh && chmod 700 /home/loops/.ssh
# Only write if not already present (preserves manual customisation).
if [ ! -f /home/loops/.ssh/config ]; then
cat > /home/loops/.ssh/config <<'SSHCFG'
Host cc-ci
HostName REPLACE_WITH_CC_CI_HETZNER_TAILNET_IP
User root
IdentityFile /home/loops/.ssh/cc-ci-root-ed25519
IdentitiesOnly yes
StrictHostKeyChecking accept-new
ServerAliveInterval 30
SSHCFG
chmod 600 /home/loops/.ssh/config
chown loops:users /home/loops/.ssh/config
fi
'';
# claude-install — fetch the standalone Claude Code CLI for the loops user if missing.
systemd.services.claude-install = {
description = "Install Claude Code CLI for loops user (idempotent)";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
serviceConfig = {
Type = "oneshot"; RemainAfterExit = true;
User = "loops"; Group = "users";
};
environment = { HOME = "/home/loops"; };
path = [ pkgs.curl pkgs.bash pkgs.coreutils pkgs.gnutar pkgs.gzip ];
script = ''
if [ ! -x "$HOME/.local/bin/claude" ]; then
echo "installing Claude Code CLI for loops user..."
curl -fsSL https://claude.ai/install.sh | bash || echo "install failed — retry on next activation"
fi
'';
};
# cc-ci-loops supervisor — defined but NOT enabled until workspace is staged.
# Enable by adding wantedBy after staging (Stage 2e) for reboot-resilience.
systemd.services.cc-ci-loops = {
description = "cc-ci Builder/Adversary loops + watchdog (launch.sh start)";
# wantedBy = [ "multi-user.target" ]; # uncomment after workspace is staged
after = [ "network-online.target" "tailscaled.service" "claude-install.service" ];
wants = [ "network-online.target" ];
serviceConfig = {
Type = "oneshot"; RemainAfterExit = true;
User = "loops"; Group = "users";
WorkingDirectory = "/srv/cc-ci";
};
environment = { RESUME_PHASE = "1"; HOME = "/home/loops"; };
path = [ pkgs.bash pkgs.tmux pkgs.git pkgs.python3 pkgs.openssh pkgs.nettools ];
script = ''
[ -x /srv/cc-ci/cc-ci-plan/launch.sh ] && /srv/cc-ci/cc-ci-plan/launch.sh start || \
echo "workspace not staged yet — skipping loop start"
'';
};
}