NixOS u Incus kontejneru: pravi uzrok i rješenje u jednoj liniji (security.nesting)


Kontekst: prethodni pokušaj nije radio

U prethodnom postu (“NixOS unutar Hetzner cloud instance na Ubuntu hostu s Incusom”) opisali smo mukotrpan put pokretanja NixOS-a 26.05 u Incus LXC kontejneru — osam problema, jedan za drugim, i na kraju “konačno rješenje” s ogromnim systemd.packages drop-in fajlom koji gasi desetak Protect*/Private* opcija, plus ručna aktivacija, privilegirani kontejner i pomoćni servis za /run/current-system.

Taj post je generisan modelom Claude Sonnet. I — rješenje zapravo nije radilo.

Kada smo se vratili na server (root@167.233.x.y), kontejner nixos-1 je bio u polomljenom stanju: bez IPv4 adrese, /run/current-system simbolički link je nestao, a “zakrpe” iz konfiguracije nikad nisu ni primijenjene jer nixos-rebuild nikad nije uspješno odradio switch. Konfiguracija je bila puna liječenja simptoma, a ne uzroka.

Ovaj post je analiza koju je odradio Claude Opus 4.8 — i pokazuje koliko je stvarno rješenje jednostavnije.


Metoda: ne nagađati, nego izolovati

Umjesto da nastavimo krpati nixos-1, pokrenuli smo čist, svjež kontejner iz iste slike i posmatrali šta tačno pada bez ijedne izmjene:

incus launch images:nixos/26.05 nixos-test -c security.privileged=true

Rezultat — systemctl is-system-runningdegraded, a pao je čitav niz core servisa:

systemd-journald.service                 failed
systemd-sysctl.service                   failed
systemd-tmpfiles-setup-dev-early.service failed
systemd-tmpfiles-setup.service           failed
systemd-networkd.service                 failed
systemd-resolved.service                 failed
systemd-logind.service                   failed
nscd.service                             failed
...

Ključno: ovo je bio privilegirani kontejner — upravo ono što je prethodni post tvrdio da rješava problem. Nije.


Pravi korijenski uzrok: 243/CREDENTIALS

Pogled u status pojedinačnog servisa otkrio je tačan exit kod:

Process: 218 ExecStart=systemd-tmpfiles --prefix=/dev --create --boot --graceful
         (code=exited, status=243/CREDENTIALS)

Exit kod 243 = EXIT_CREDENTIALS. systemd 256+ (NixOS 26.05 nosi systemd 260) dodao je ImportCredential=/LoadCredential= direktive u mnoge core jedinice. Prije nego što pokrene binarni fajl servisa, systemd montira ramfs za /run/credentials/<unit>.

Dokaz da problem nije u samom programu, nego u systemd-ovoj pripremi kredencijala: pokrenut ručno iz shell-a, isti systemd-tmpfiles prolazi bez greške:

$ systemd-tmpfiles --prefix=/dev --create --boot --graceful
$ echo $?
0          # ručno: prolazi

Kao systemd servis: 243/CREDENTIALS. Razlika je isključivo u tome što systemd kao servis-menadžer pokušava montirati credential ramfs — i taj mount Incusov podrazumijevani AppArmor profil zabranjuje. Otud kaskadni kvar: journald, sysctl, tmpfiles, networkd… svi padnu na istom koraku, prije nego što i počnu raditi.

Zašto je prethodni post mislio da nema 243 greške? Zato što je journald i sâm pao — pa nije bilo perzistentnog žurnala koji bi se mogao grep-ovati. Greška je sve vrijeme bila tu, samo nevidljiva u journalctl.


Rješenje: jedna Incus opcija

ramfs/tmpfs montiranja koja systemd radi za kredencijale (i za run-wrappers, dev-hugepages) dozvoljava upravo opcija namijenjena ugnježdavanju kontejnera:

incus config set nixos-1 security.nesting=true
incus restart nixos-1

Rezultat:

running=running
FAILED_COUNT=0
IPV4: 10.9*.*.97

Nula palih jedinica. Sistem je running, ne degraded. IPv4 stiže sâm (systemd-networkd sad radi). Nestale su i one dvije “bezopasne” greške iz prethodnog posta (dev-hugepages.mount, run-wrappers.mount) — jer su i one bile blokirani mountovi, ne neka inherentna LXC ograničenja.

I još bolje — security.privileged uopšte nije potreban. Dovoljan je neprivilegirani kontejner s nesting=true, što je sigurnije:

incus launch images:nixos/26.05 nixos-1 -c security.nesting=true

Minimalna, čista configuration.nix

Bez ijednog credential hacka, bez systemd.packages, bez ručne aktivacije, bez pomoćnih servisa za /run/current-system (koji sada radi sasvim normalno):

{ modulesPath, pkgs, ... }:

{
  imports = [
    "${modulesPath}/virtualisation/lxc-container.nix"
    ./incus.nix   # autogenerisan od Incusa: postavlja networking.hostName
  ];

  # Nix buildovi ne mogu koristiti kernel-namespace sandbox unutar LXC-a;
  # gasimo ga da `nixos-rebuild` radi.
  nix.settings.sandbox = false;

  services.openssh.enable = true;
  users.users.root.openssh.authorizedKeys.keys = [
    "ssh-rsa AAAA... vaš-kljuc"
  ];

  environment.systemPackages = with pkgs; [ vim git htop curl iproute2 net-tools ];

  time.timeZone = "Europe/Sarajevo";
  system.stateVersion = "26.05";
}

Jedina dvije NixOS postavke koje su zaista potrebne:

  • nix.settings.sandbox = false — da nixos-rebuild može da gradi unutar kontejnera (build sandbox koristi kernel namespace-ove kojih u LXC-u nema).
  • imports od lxc-container.nix — standardni NixOS profil za kontejnere.

Sve ostalo (SSH ključ, paketi, vremenska zona) je stvar ukusa. Pravi “fix” živi na strani Incusa, u jednoj liniji.


Ubacivanje konfiguracije sa Incus hosta: push → build → switch

Konfiguraciju ne moramo uređivati unutar kontejnera. Ugodnije je držati configuration.nix na Incus hostu (ili je tamo prebaciti sa laptopa), pa je jednom komandom ubaciti u kontejner i primijeniti. Cijeli ciklus ima tri koraka:

# 1) UBACI — kopiraj fajl sa hosta u kontejner
#    (incus file push <lokalni-fajl> <kontejner>/<putanja-u-kontejneru>)
incus file push ./configuration.nix nixos-1/etc/nixos/configuration.nix

# 2) BUILD + SWITCH — izgradi novu generaciju i pređi na nju, sve unutar kontejnera
incus exec nixos-1 -- nixos-rebuild switch

nixos-rebuild switch radi obje stvari atomarno: izgradi novu sistemsku generaciju i prebaci aktivni sistem na nju. Ako želimo prvo samo isprobati bez trajne promjene, nixos-rebuild test aktivira generaciju ali je ne čini default-om za sljedeći boot.

Caveat — $NIX_PATH u incus exec: “goli” incus exec ... -- pokreće ne-login shell, u kojem $NIX_PATH nije postavljen kao u interaktivnom bash -l. Tada nixos-rebuild ne nađe nixpkgs i javi file 'nixpkgs/nixos' was not found in the Nix search path. Dvije zaobilaznice:

# a) eksplicitno pokaži na kanal
incus exec nixos-1 -- nixos-rebuild switch \
  -I nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos

# b) ili pokreni kroz login shell (učita /etc/profile → postavi $NIX_PATH)
incus exec nixos-1 -- bash -lc "nixos-rebuild switch"

Prvi put eventualno treba i incus exec nixos-1 -- nix-channel --update da se kanal nixos-26.05 materijalizuje.

Provjera koja je generacija aktivna nakon switcha:

incus exec nixos-1 -- readlink /run/current-system
# /nix/store/<hash>-nixos-system-nixos-1-lxc-26.05...

Isti push → build → switch ciklus koristimo za sve izmjene u nastavku (paketi, SSH ključevi, mreža…).


Dodavanje alata: net-tools, vim, htop

Pakete dodajemo deklarativno, u environment.systemPackages. Recimo da želimo klasične mrežne alate (ifconfig, netstat, route) uz vim i htop:

  environment.systemPackages = with pkgs; [
    vim
    htop
    net-tools   # ifconfig, netstat, route
    git
    curl
    iproute2    # moderni `ip`
  ];

Izmjenu primjenjujemo jednom komandom unutar kontejnera:

incus exec nixos-1 -- nixos-rebuild switch

Napomena: u “golom” incus exec shell-u $NIX_PATH nije postavljen kao u login shell-u, pa kanal treba pokazati eksplicitno: nixos-rebuild switch -I nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos (ili prvo nix-channel --update). U interaktivnom bash -l ovo nije potrebno.

Provjera da su alati stigli:

$ incus exec nixos-1 -- bash -lc 'command -v ifconfig netstat route htop vim'
/run/current-system/sw/bin/ifconfig
/run/current-system/sw/bin/netstat
/run/current-system/sw/bin/route
/run/current-system/sw/bin/htop
/run/current-system/sw/bin/vim

Ljepota deklarativnog pristupa: paketi nisu “instalirani” imperativno — oni su dio sistemske generacije. nixos-rebuild switch napravi novu generaciju i prebaci na nju atomarno; nixos-rebuild --rollback vraća na prethodnu ako nešto pođe po zlu.


Dodavanje novog SSH ključa

I SSH ključeve dodajemo deklarativno — ne diramo ~/.ssh/authorized_keys rukom, nego ih navedemo u konfiguraciji. Tako su ključevi dio sistemske generacije i prežive svaki rebuild i reboot:

  services.openssh.enable = true;

  users.users.root.openssh.authorizedKeys.keys = [
    # postojeći ključ (laptop)
    "ssh-rsa AAAA...redacted... korisnik@laptop"
    # novi ključ — npr. javni ključ Incus hosta da se sa hosta ulazi bez lozinke
    "ssh-ed25519 AAAA...redacted... root@incus-host"
  ];

Javni ključ koji želimo dodati pročitamo na izvoru (npr. na Incus hostu), pa ga samo zalijepimo u listu iznad:

# na izvornoj mašini — ispiše javni (ne privatni!) ključ
cat /root/.ssh/id_ed25519.pub
# ako ključ još ne postoji:
ssh-keygen -t ed25519 -C "root@incus-host"

Sigurnosna napomena: u listu ide isključivo javni ključ (*.pub, počinje sa ssh-ed25519/ssh-rsa). Privatni ključ nikad ne napušta mašinu na kojoj je generisan i ne stavlja se u konfiguraciju.

Primjena i provjera:

incus exec nixos-1 -- nixos-rebuild switch

# NixOS sklapa ključeve u read-only fajl (ne u ~/.ssh):
incus exec nixos-1 -- cat /etc/ssh/authorized_keys.d/root

# test s mašine čiji smo ključ dodali:
ssh root@<ip-kontejnera>     # → ulazi bez lozinke

Pošto je authorized_keys.d/root generisan iz konfiguracije, ručne izmjene tog fajla nemaju smisla — sljedeći nixos-rebuild bi ih pregazio. Jedini izvor istine je configuration.nix.


Provjera: reboot iz čista

Pravi test nije “radi dok ja gledam”, nego “preživi reboot”:

incus restart nixos-1
running=running
failed units: 0
eth0@if59  UP  10.9*.*.97/24
/run/current-system -> /nix/store/...-nixos-system-nixos-1-lxc-26.05...
sshd.socket=active

SSH radi (NixOS socket-aktivira sshd, pa je sshd.service “inactive” sve dok ne stigne konekcija — to je normalno). /run/current-system postoji. Nema nijedne pale jedinice.


Sažetak: Sonnet vs. Opus 4.8

Prethodno (Sonnet)Sada (Opus 4.8)
Dijagnoza 243/CREDENTIALSdjelimično tačna, ali pogrešno pripisanatačan uzrok: zabranjen credential mount
”Rješenje” za kredencijale~20 Protect*/Private* opcija u global drop-innepotrebno — uklonjeno
Privilegirani kontejner”obavezno”nepotreban (radi neprivilegirano)
/run/current-systempomoćni servis + ručna aktivacijaradi bez ičega
dhcpcd vs networkdručno prebacivanje na dhcpcdnetworkd radi sâm
Stvarna izmjenadesetine linija NixOS hackovasecurity.nesting=true
Da li radi?neda — 0 palih jedinica, preživi reboot

Pouka: kada se gomila simptoma “rješava” hrpom zakrpa, vrijedi se zaustaviti, pokrenuti čist kontejner i izolovati jedan korijenski uzrok. Često je rješenje jedna linija — a ne dvadeset.


Napomena

Generisano od strane Claude 🤖

Korijenska analiza i rješenje: Claude Opus 4.8. Prethodni (neispravni) pokušaj: Claude Sonnet.


Ernad Husremović, hernad@bring.out.ba