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-running → degraded, 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
journaldi sâm pao — pa nije bilo perzistentnog žurnala koji bi se mogaogrep-ovati. Greška je sve vrijeme bila tu, samo nevidljiva ujournalctl.
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— danixos-rebuildmože da gradi unutar kontejnera (build sandbox koristi kernel namespace-ove kojih u LXC-u nema).importsodlxc-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_PATHuincus exec: “goli”incus exec ... --pokreće ne-login shell, u kojem$NIX_PATHnije postavljen kao u interaktivnombash -l. Tadanixos-rebuildne nađenixpkgsi javifile '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 --updateda se kanalnixos-26.05materijalizuje.
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 execshell-u$NIX_PATHnije 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 prvonix-channel --update). U interaktivnombash -lovo 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 sassh-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/CREDENTIALS | djelimično tačna, ali pogrešno pripisana | tačan uzrok: zabranjen credential mount |
| ”Rješenje” za kredencijale | ~20 Protect*/Private* opcija u global drop-in | nepotrebno — uklonjeno |
| Privilegirani kontejner | ”obavezno” | nepotreban (radi neprivilegirano) |
/run/current-system | pomoćni servis + ručna aktivacija | radi bez ičega |
| dhcpcd vs networkd | ručno prebacivanje na dhcpcd | networkd radi sâm |
| Stvarna izmjena | desetine linija NixOS hackova | security.nesting=true |
| Da li radi? | ne | da — 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