-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstance-base.nix
More file actions
320 lines (281 loc) · 12 KB
/
Copy pathinstance-base.nix
File metadata and controls
320 lines (281 loc) · 12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
# Stripped NixOS profile for Seed instances running inside Kata VMs
#
# Kata provides the kernel, initrd, and virtio networking. This profile
# disables everything NixOS would normally configure for bare metal/VM boot.
# All settings use mkDefault so tenants can override if needed.
{ lib, pkgs, config, ... }:
let
# EST certificate enrollment script — obtains a SPIFFE identity cert from
# the platform EST endpoint using vTPM attestation.
#
# Flow:
# 1. Generate TPM-bound ECDSA P-256 key (private key never leaves the TPM)
# 2. Create CSR with SPIFFE URI SAN
# 3. Request age-encrypted challenge from controller (POST /est/challenge)
# 4. Decrypt challenge using age-plugin-tpm (proves vTPM possession)
# 5. Submit CSR + decrypted nonce (POST /est/enroll)
# 6. Write signed cert + CA + key handle to /seed/tls/
#
# The key.pem is a TSS2 PRIVATE KEY (TPM handle), not raw key material.
# Services use the tpm2-openssl provider for TLS operations.
seedCertEnroll = pkgs.writeShellScript "seed-cert-enroll" ''
set -euo pipefail
EST_URL="''${SEED_EST_URL:-}"
if [ -z "$EST_URL" ]; then
echo "SEED_EST_URL not set, skipping certificate enrollment"
exit 0
fi
NAMESPACE="''${SEED_NAMESPACE:?SEED_NAMESPACE must be set}"
INSTANCE="''${SEED_INSTANCE:?SEED_INSTANCE must be set}"
TLS_DIR="/seed/tls"
TPM_IDENTITY="/seed/tpm/age-identity"
export OPENSSL_MODULES="${pkgs.tpm2-openssl}/lib/ossl-modules"
mkdir -p "$TLS_DIR"
# Ensure TPM device exists (Kata VMs use tmpfs on /dev, nodes created by activation)
if [ ! -e /dev/tpmrm0 ]; then
echo "WARNING: /dev/tpmrm0 not found, running tpm-dev-create"
${tpmDevCreate}
fi
if [ ! -e /dev/tpmrm0 ]; then
echo "ERROR: /dev/tpmrm0 still not found after device creation"
exit 1
fi
# 1. Generate TPM-bound ECDSA P-256 key
# The output is a TSS2 PRIVATE KEY PEM — a TPM key handle, not raw material.
${pkgs.openssl}/bin/openssl genpkey \
-provider tpm2 -provider default \
-algorithm EC -pkeyopt group:P-256 \
-out "$TLS_DIR/key.pem"
# 2. Create CSR with SPIFFE URI SAN + DNS SANs for hostname verification
SPIFFE_URI="spiffe://seeds.loom.farm/$NAMESPACE/$INSTANCE"
SAN="URI:$SPIFFE_URI"
SAN="$SAN,DNS:$INSTANCE"
SAN="$SAN,DNS:$INSTANCE.$NAMESPACE.svc.cluster.local"
SAN="$SAN,DNS:$INSTANCE.$NAMESPACE.seed.loom.farm"
${pkgs.openssl}/bin/openssl req -new \
-provider tpm2 -provider default -propquery '?provider=tpm2' \
-key "$TLS_DIR/key.pem" \
-subj "/O=seeds.loom.farm/OU=$NAMESPACE/CN=$INSTANCE" \
-addext "subjectAltName=$SAN" \
-out "$TLS_DIR/csr.pem"
CSR_PEM=$(cat "$TLS_DIR/csr.pem")
# 3. Request challenge from EST endpoint
CHALLENGE_RESP=$(${pkgs.curl}/bin/curl -sf \
--cacert /etc/ssl/certs/ca-certificates.crt \
-X POST "$EST_URL/est/challenge" \
-H "Content-Type: application/json" \
-d "{\"namespace\":\"$NAMESPACE\",\"instance\":\"$INSTANCE\"}")
CHALLENGE_ID=$(echo "$CHALLENGE_RESP" | ${pkgs.jq}/bin/jq -r '.challengeId')
ENCRYPTED=$(echo "$CHALLENGE_RESP" | ${pkgs.jq}/bin/jq -r '.encrypted')
# 4. Decrypt challenge using age-plugin-tpm (proves vTPM possession)
# Timeout after 30s — age-plugin-tpm hangs if /dev/tpmrm0 is missing/broken.
NONCE=$(echo "$ENCRYPTED" | timeout 30 ${pkgs.age}/bin/age -d -i "$TPM_IDENTITY")
# 5. Submit CSR + decrypted nonce to EST enroll endpoint
ENROLL_BODY=$(${pkgs.jq}/bin/jq -n \
--arg cid "$CHALLENGE_ID" \
--arg nonce "$NONCE" \
--arg csr "$CSR_PEM" \
'{challengeId: $cid, nonce: $nonce, csr: $csr}')
${pkgs.curl}/bin/curl -sf \
--cacert /etc/ssl/certs/ca-certificates.crt \
-X POST "$EST_URL/est/enroll" \
-H "Content-Type: application/json" \
-d "$ENROLL_BODY" \
-o "$TLS_DIR/cert.pem"
# 6. Fetch CA cert
${pkgs.curl}/bin/curl -sf \
--cacert /etc/ssl/certs/ca-certificates.crt \
"$EST_URL/est/cacerts" \
-o "$TLS_DIR/ca.pem"
# key.pem is a TPM handle (not secret), but PostgreSQL checks permissions.
# 0640 root:tpm satisfies pg's check while allowing tpm group members to read.
chgrp tpm "$TLS_DIR/key.pem"
chmod 0640 "$TLS_DIR/key.pem"
chmod 0644 "$TLS_DIR/cert.pem" "$TLS_DIR/ca.pem"
# Remove CSR (no longer needed)
rm -f "$TLS_DIR/csr.pem"
'';
tpmDevCreate = pkgs.writeShellScript "tpm-dev-create" ''
for tpm in /sys/class/tpm/tpm*; do
[ -e "$tpm" ] || continue
name=$(basename "$tpm")
if [ ! -e "/dev/$name" ]; then
dev=$(cat "$tpm/dev" 2>/dev/null) || continue
major=''${dev%%:*}
minor=''${dev##*:}
mknod "/dev/$name" c "$major" "$minor"
fi
chgrp tpm "/dev/$name" 2>/dev/null || true
chmod 0660 "/dev/$name"
done
# Also create /dev/tpmrm* (resource manager interface)
for tpmrm in /sys/class/tpmrm/tpmrm*; do
[ -e "$tpmrm" ] || continue
name=$(basename "$tpmrm")
if [ ! -e "/dev/$name" ]; then
dev=$(cat "$tpmrm/dev" 2>/dev/null) || continue
major=''${dev%%:*}
minor=''${dev##*:}
mknod "/dev/$name" c "$major" "$minor"
fi
chgrp tpm "/dev/$name" 2>/dev/null || true
chmod 0660 "/dev/$name"
done
'';
in {
# boot.isContainer disables kernel, initrd, bootloader, and hardware scan.
# Kata VMs run real systemd (not container init), but isContainer gives us
# the right closure size. Services needing /run/* dirs should use RuntimeDirectory.
boot.isContainer = lib.mkDefault true;
# No documentation — smaller closure
documentation.enable = lib.mkDefault false;
# No nix daemon — instances are pre-built closures
nix.enable = lib.mkDefault false;
# No sudo — there's no interactive shell escalation in instances
security.sudo.enable = lib.mkDefault false;
# Immutable users — no passwd/shadow management
users.mutableUsers = lib.mkDefault false;
# No interactive login — instances are headless, managed by the controller
users.allowNoPasswordLogin = lib.mkDefault true;
# Kata handles networking via virtio-net + tc redirects
networking.useDHCP = lib.mkDefault false;
# k8s service DNS (CoreDNS) — enables service name resolution + external DNS
networking.nameservers = lib.mkDefault [ "10.43.0.10" ];
# tpm group — services that need TPM access (TLS with TPM-bound keys) join this group
users.groups.tpm = {};
# Minimal package set — just enough for systemd services to function,
# plus TPM/secrets tooling for sops-nix integration
environment.systemPackages = lib.mkDefault (with pkgs; [
coreutils
bashInteractive
util-linux
age
age-plugin-tpm
tpm2-tools
tpm2-openssl
sops
openssl
curl
jq
]);
# No polkit — headless instances don't need privilege negotiation
security.polkit.enable = lib.mkDefault false;
# No nscd/nsncd — instances use /etc/resolv.conf + /etc/passwd directly.
# nsncd fails in Kata VMs and cascades to nss-lookup.target failure,
# breaking DNS resolution and user lookups for all services.
services.nscd.enable = lib.mkDefault false;
system.nssModules = lib.mkForce [];
# Create TPM device nodes during NixOS activation, before sops-nix runs.
# Kata VMs use tmpfs on /dev (not devtmpfs), so the kernel doesn't
# auto-create device nodes. sops-nix's setupSecrets activation script runs
# before systemd starts, so we must create /dev/tpm* during activation too.
system.activationScripts.tpmDevNodes = {
deps = [];
text = ''
${tpmDevCreate}
'';
};
# Ensure sops-nix's setupSecrets runs after TPM device nodes exist.
# Provide a no-op default text so this works even when no secrets are defined.
system.activationScripts.setupSecrets = {
deps = [ "tpmDevNodes" ];
text = lib.mkDefault "";
};
# TPM identity provisioning — generates age-plugin-tpm identity on first boot.
# The identity file at /seed/tpm/age-identity contains the public key (recipient)
# on its first line, usable for encrypting sops secrets for this instance.
systemd.services.seed-tpm-init = {
description = "Generate age-plugin-tpm identity for sops-nix";
wantedBy = [ "multi-user.target" ];
before = lib.mkDefault [ "sops-nix.service" ];
unitConfig.ConditionPathExists = "!/seed/tpm/age-identity";
path = [ pkgs.coreutils ];
serviceConfig = {
Type = "oneshot";
ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p /seed/tpm";
ExecStart = "${pkgs.age-plugin-tpm}/bin/age-plugin-tpm --generate -o /seed/tpm/age-identity";
RemainAfterExit = true;
};
};
# TLS identity enrollment — obtains a SPIFFE identity certificate from the
# platform EST endpoint. Requires seed-tpm-init (age identity for attestation)
# and SEED_EST_URL (injected by the controller into all instance pods).
systemd.services.seed-cert-enroll = {
description = "Obtain SPIFFE identity certificate via EST";
wantedBy = [ "multi-user.target" ];
after = [ "seed-tpm-init.service" "network-online.target" ];
wants = [ "network-online.target" ];
requires = [ "seed-tpm-init.service" ];
path = [ pkgs.age-plugin-tpm ];
unitConfig = {
# Retry up to 5 times within 5 minutes on boot failures
StartLimitIntervalSec = 300;
StartLimitBurst = 5;
};
serviceConfig = {
Type = "oneshot";
EnvironmentFile = "/run/seed/env";
ExecStart = seedCertEnroll;
RemainAfterExit = true;
Restart = "on-failure";
RestartSec = "30s";
};
};
# Renewal timer — re-enroll at half the cert lifetime (12h for 24h certs).
# On failure, retries every 5 minutes.
systemd.timers.seed-cert-renew = {
description = "Renew SPIFFE identity certificate";
wantedBy = [ "timers.target" ];
timerConfig = {
OnActiveSec = "12h";
OnUnitActiveSec = "12h";
AccuracySec = "5m";
};
};
systemd.services.seed-cert-renew = {
description = "Renew SPIFFE identity certificate via EST";
after = [ "seed-cert-enroll.service" ];
requires = [ "seed-cert-enroll.service" ];
path = [ pkgs.age-plugin-tpm ];
serviceConfig = {
Type = "oneshot";
EnvironmentFile = "/run/seed/env";
ExecStart = seedCertEnroll;
Restart = "on-failure";
RestartSec = "5m";
};
};
# Default sops-nix to use the TPM-backed age identity provisioned by seed-tpm-init.
# Instances using sops.secrets.* will decrypt via their unique vTPM key automatically.
sops.age.keyFile = lib.mkDefault "/seed/tpm/age-identity";
sops.age.plugins = lib.mkDefault [ pkgs.age-plugin-tpm ];
# Trust the platform CA if mounted by the controller.
# NixOS creates /etc/ssl/certs/ca-certificates.crt as a symlink to the nix
# store CA bundle. We replace it with a combined bundle (mozilla roots +
# platform CA) so ALL programs trust it — including those in sanitized
# environments (sshd AuthorizedKeysCommand, git hooks, etc.) where
# SSL_CERT_FILE isn't available.
system.activationScripts.seedTrust = {
deps = [ "etc" ];
text = ''
if [ -f /etc/seed/ca/ca.crt ]; then
rm -f /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-bundle.crt /etc/pki/tls/certs/ca-bundle.crt
cat ${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt /etc/seed/ca/ca.crt > /etc/ssl/certs/ca-certificates.crt
ln -sf /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-bundle.crt
ln -sf /etc/ssl/certs/ca-certificates.crt /etc/pki/tls/certs/ca-bundle.crt
fi
'';
};
# Capture k8s-injected SEED_* environment variables for use by services.
# Kata VMs: systemd strips the inherited environment on startup, so
# PassEnvironment doesn't work. This activation script reads PID 1's
# original environment (preserved in /proc/1/environ) and writes SEED_*
# vars to /run/seed/env. Services use EnvironmentFile=/run/seed/env.
system.activationScripts.seedEnv = {
text = ''
mkdir -p /run/seed
${pkgs.coreutils}/bin/tr '\0' '\n' < /proc/1/environ | ${pkgs.gnugrep}/bin/grep '^SEED_' > /run/seed/env || true
'';
};
system.stateVersion = lib.mkDefault "25.11";
}