@le-space/rootfs 0.9.9 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +10 -1
- package/index.js +22 -0
- package/package.json +1 -1
- package/reference/confidential/README.md +37 -0
- package/reference/confidential/confidential-init.sh +52 -0
- package/reference/confidential/confidential-systemctl.py +205 -0
- package/reference/confidential/make-confidential-rootfs.sh +142 -0
- package/reference/orbitdb-relay/contract.json +1 -1
- package/reference/orbitdb-relay/rootfs/attest-relay.service +27 -0
- package/reference/orbitdb-relay/rootfs/build-rootfs-image.sh +24 -0
- package/reference/orbitdb-relay/rootfs/orbitdb-relay-configure.sh +7 -0
- package/reference/uc-go-peer/rootfs/__pycache__/uc-go-peer-autotls-refresh.cpython-312.pyc +0 -0
- package/reference/uc-go-peer/rootfs/__pycache__/uc-go-peer-describe.cpython-312.pyc +0 -0
package/index.d.ts
CHANGED
|
@@ -166,6 +166,8 @@ interface RootfsExecutionPlan {
|
|
|
166
166
|
mode: "host" | "docker";
|
|
167
167
|
reason: string;
|
|
168
168
|
referenceRootfsDir: string;
|
|
169
|
+
/** Run on the host first: files the image needs, built into the reference dir (all a Docker build sees). */
|
|
170
|
+
assetCommands?: RootfsCommandPlan[];
|
|
169
171
|
prepareCommand?: RootfsCommandPlan;
|
|
170
172
|
runCommand: RootfsCommandPlan;
|
|
171
173
|
}
|
|
@@ -176,6 +178,13 @@ interface RootfsExecutionPlanOptions {
|
|
|
176
178
|
orbitdbRelayMountPath?: string;
|
|
177
179
|
dockerImageTag?: string;
|
|
178
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* The orbitdb-relay image carries the attest relay (packages/attest-relay) as
|
|
183
|
+
* one bundled file, so browsers can reach a confidential guest's RA-TLS port
|
|
184
|
+
* at wss://<relay host>/attest. Built from this workspace when it is here; an
|
|
185
|
+
* image built without it has no attest relay (build-rootfs-image.sh says so).
|
|
186
|
+
*/
|
|
187
|
+
declare function profileAssetCommands(contract: RootfsContract, referenceRootfsDir: string): RootfsCommandPlan[];
|
|
179
188
|
declare function createHostRootfsExecutionPlan(plan: RootfsBuildPlan, options?: RootfsExecutionPlanOptions): RootfsExecutionPlan;
|
|
180
189
|
declare function createDockerRootfsExecutionPlan(plan: RootfsBuildPlan, options?: RootfsExecutionPlanOptions): RootfsExecutionPlan;
|
|
181
190
|
declare function selectRootfsExecutionPlan(plan: RootfsBuildPlan, availability: RootfsToolchainAvailability, options?: RootfsExecutionPlanOptions): RootfsExecutionPlan;
|
|
@@ -261,4 +270,4 @@ declare function createRootfsScriptCommand(buildPlan: RootfsBuildPlan, reference
|
|
|
261
270
|
declare function buildRootfs(buildPlan: RootfsBuildPlan, deps: RootfsCommandRunner, availability: RootfsToolchainAvailability, options?: RootfsExecutionPlanOptions): Promise<RootfsExecutionResult>;
|
|
262
271
|
declare function publishRootfs(buildPlan: RootfsBuildPlan, deps: RootfsExecutorDependencies, options?: RootfsPublishOptions): Promise<RootfsPublishExecutionResult>;
|
|
263
272
|
|
|
264
|
-
export { ITEM_HASH_RE, type RootfsBuildDriver, type RootfsBuildOptions, type RootfsBuildPipeline, type RootfsBuildPlan, type RootfsCommandPlan, type RootfsCommandRunner, type RootfsContract, type RootfsContractManifest, type RootfsContractPort, type RootfsContractRootfs, type RootfsContractServices, type RootfsContractSource, type RootfsContractState, type RootfsExecutedCommand, type RootfsExecutionPlan, type RootfsExecutionPlanOptions, type RootfsExecutionResult, type RootfsExecutorDependencies, type RootfsFileReader, type RootfsFinalizeOptions, type RootfsFinalizeResult, type RootfsIpfsAddEntry, type RootfsManifest, type RootfsManifestOptions, type RootfsManifestOutputPaths, type RootfsManifestState, type RootfsPublicationArtifacts, type RootfsPublicationResult, type RootfsPublishExecutionResult, type RootfsPublishOptions, type RootfsStoreMessageResponse, type RootfsStoreMessageStatus, type RootfsToolchainAvailability, buildRootfs, contractShellEnv, createDockerRootfsBuildPipeline, createDockerRootfsExecutionPlan, createHostRootfsBuildPipeline, createHostRootfsExecutionPlan, createRootfsBuildPipeline, createRootfsBuildPlan, createRootfsManifest, createRootfsPublicationResult, createRootfsScriptCommand, deriveRootfsVersion, extractRootfsCid, extractRootfsSourceSizeBytes, finalizeRootfsBuildPipeline, parseIpfsAddResponse, parseRootfsContract, parseStoreMessageResponse, parseStoreMessageStatus, publicationArtifacts, publishRootfs, readRootfsContractFile, referenceProfileContractPath, referenceProfileRoot, referenceProfileRootfsDir, resolveRootfsManifestOutputPaths, rootfsBuildShellEnv, rootfsSourceSizeBytesFromIpfsAddResponse, selectRootfsExecutionPlan, serializeRootfsManifest, summarizeStoreMessageFailure, validateRootfsContract, validateRootfsManifest };
|
|
273
|
+
export { ITEM_HASH_RE, type RootfsBuildDriver, type RootfsBuildOptions, type RootfsBuildPipeline, type RootfsBuildPlan, type RootfsCommandPlan, type RootfsCommandRunner, type RootfsContract, type RootfsContractManifest, type RootfsContractPort, type RootfsContractRootfs, type RootfsContractServices, type RootfsContractSource, type RootfsContractState, type RootfsExecutedCommand, type RootfsExecutionPlan, type RootfsExecutionPlanOptions, type RootfsExecutionResult, type RootfsExecutorDependencies, type RootfsFileReader, type RootfsFinalizeOptions, type RootfsFinalizeResult, type RootfsIpfsAddEntry, type RootfsManifest, type RootfsManifestOptions, type RootfsManifestOutputPaths, type RootfsManifestState, type RootfsPublicationArtifacts, type RootfsPublicationResult, type RootfsPublishExecutionResult, type RootfsPublishOptions, type RootfsStoreMessageResponse, type RootfsStoreMessageStatus, type RootfsToolchainAvailability, buildRootfs, contractShellEnv, createDockerRootfsBuildPipeline, createDockerRootfsExecutionPlan, createHostRootfsBuildPipeline, createHostRootfsExecutionPlan, createRootfsBuildPipeline, createRootfsBuildPlan, createRootfsManifest, createRootfsPublicationResult, createRootfsScriptCommand, deriveRootfsVersion, extractRootfsCid, extractRootfsSourceSizeBytes, finalizeRootfsBuildPipeline, parseIpfsAddResponse, parseRootfsContract, parseStoreMessageResponse, parseStoreMessageStatus, profileAssetCommands, publicationArtifacts, publishRootfs, readRootfsContractFile, referenceProfileContractPath, referenceProfileRoot, referenceProfileRootfsDir, resolveRootfsManifestOutputPaths, rootfsBuildShellEnv, rootfsSourceSizeBytesFromIpfsAddResponse, selectRootfsExecutionPlan, serializeRootfsManifest, summarizeStoreMessageFailure, validateRootfsContract, validateRootfsManifest };
|
package/index.js
CHANGED
|
@@ -385,7 +385,9 @@ function serializeRootfsManifest(manifest) {
|
|
|
385
385
|
}
|
|
386
386
|
|
|
387
387
|
// src/execution-plan.ts
|
|
388
|
+
import { existsSync as existsSync2 } from "fs";
|
|
388
389
|
import path2 from "path";
|
|
390
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
389
391
|
function ensurePathWithin(parent, child, label) {
|
|
390
392
|
const relative = path2.relative(parent, child);
|
|
391
393
|
if (relative.startsWith("..") || path2.isAbsolute(relative)) {
|
|
@@ -411,12 +413,25 @@ function resolveReferenceRootfsDir(contract, override) {
|
|
|
411
413
|
}
|
|
412
414
|
return referenceProfileRootfsDir(contract.id);
|
|
413
415
|
}
|
|
416
|
+
function profileAssetCommands(contract, referenceRootfsDir) {
|
|
417
|
+
if (contract.rootfs.profile !== "orbitdb-relay") return [];
|
|
418
|
+
const bundler = fileURLToPath2(new URL("../../attest-relay/scripts/bundle.mjs", import.meta.url));
|
|
419
|
+
if (!existsSync2(bundler)) return [];
|
|
420
|
+
return [
|
|
421
|
+
{
|
|
422
|
+
command: process.execPath,
|
|
423
|
+
args: [bundler, path2.join(referenceRootfsDir, "attest-relay.cjs")],
|
|
424
|
+
workdir: path2.dirname(bundler)
|
|
425
|
+
}
|
|
426
|
+
];
|
|
427
|
+
}
|
|
414
428
|
function createHostRootfsExecutionPlan(plan, options = {}) {
|
|
415
429
|
const referenceRootfsDir = resolveReferenceRootfsDir(plan.contract, options.referenceRootfsDir);
|
|
416
430
|
return {
|
|
417
431
|
mode: "host",
|
|
418
432
|
reason: "Using host virt-customize/qemu-img toolchain.",
|
|
419
433
|
referenceRootfsDir,
|
|
434
|
+
assetCommands: profileAssetCommands(plan.contract, referenceRootfsDir),
|
|
420
435
|
runCommand: {
|
|
421
436
|
command: "/bin/bash",
|
|
422
437
|
args: [path2.join(referenceRootfsDir, "build-rootfs-image.sh")],
|
|
@@ -462,6 +477,7 @@ function createDockerRootfsExecutionPlan(plan, options = {}) {
|
|
|
462
477
|
mode: "docker",
|
|
463
478
|
reason: "Using Dockerized Debian/libguestfs builder.",
|
|
464
479
|
referenceRootfsDir,
|
|
480
|
+
assetCommands: profileAssetCommands(plan.contract, referenceRootfsDir),
|
|
465
481
|
prepareCommand: {
|
|
466
482
|
command: "docker",
|
|
467
483
|
args: [
|
|
@@ -662,6 +678,11 @@ function createRootfsScriptCommand(buildPlan, referenceRootfsDir) {
|
|
|
662
678
|
async function buildRootfs(buildPlan, deps, availability, options = {}) {
|
|
663
679
|
const pipeline = createRootfsBuildPipeline(buildPlan, availability, options);
|
|
664
680
|
const executedCommands = [];
|
|
681
|
+
for (const asset of pipeline.executionPlan.assetCommands ?? []) {
|
|
682
|
+
const assetCommand = { command: asset.command, args: asset.args, workdir: asset.workdir, env: asset.env };
|
|
683
|
+
await deps.run(assetCommand);
|
|
684
|
+
executedCommands.push(assetCommand);
|
|
685
|
+
}
|
|
665
686
|
if (pipeline.executionPlan.prepareCommand) {
|
|
666
687
|
const prepareCommand = {
|
|
667
688
|
command: pipeline.executionPlan.prepareCommand.command,
|
|
@@ -746,6 +767,7 @@ export {
|
|
|
746
767
|
parseRootfsContract,
|
|
747
768
|
parseStoreMessageResponse,
|
|
748
769
|
parseStoreMessageStatus,
|
|
770
|
+
profileAssetCommands,
|
|
749
771
|
publicationArtifacts,
|
|
750
772
|
publishRootfs,
|
|
751
773
|
readRootfsContractFile,
|
package/package.json
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Confidential relay rootfs
|
|
2
|
+
|
|
3
|
+
A confidential (AMD SEV-SNP) instance on Aleph does not boot a rootfs the way a
|
|
4
|
+
normal instance does. Aleph's measured init waits for the owner to attest the
|
|
5
|
+
guest and inject the LUKS passphrase, unlocks the rootfs, **chroots** into it and
|
|
6
|
+
runs its `/sbin/init`. That init is not PID 1, nothing provisions the image from
|
|
7
|
+
outside (no cloud-init), and the encrypted image is uploaded in full.
|
|
8
|
+
|
|
9
|
+
`make-confidential-rootfs.sh` turns the image a relay profile build makes into
|
|
10
|
+
a plain ext4 that `aleph instance create --confidential --encrypt-rootfs` (or
|
|
11
|
+
`--image`, see below) can boot:
|
|
12
|
+
|
|
13
|
+
| Change | Why |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| `/sbin/init` → `confidential-init.sh` | a regular file that stays in the foreground: mounts `/run`, devpts and cgroup2, grows the filesystem (`EXT4_IOC_RESIZE_FS`, since `resize2fs` refuses in the chroot), makes SSH host keys on first boot, then runs `systemctl init` |
|
|
16
|
+
| `systemctl`, `journalctl` → [docker-systemctl-replacement](https://github.com/gdraheim/docker-systemctl-replacement) (pinned, checksummed) behind `confidential-systemctl.py` | starts the enabled units and keeps answering the `systemctl` / `journalctl` calls the profile scripts make, so the profile itself does not change; the wrapper runs `.timer` units (OnBootSec / OnActiveSec, then OnUnitActiveSec, with RandomizedDelaySec), which the replacement does not |
|
|
17
|
+
| cloud-init, networkd, resolved, timesyncd, getty, … off | the measured init already set up network and DNS |
|
|
18
|
+
| `Type=notify` units run as `Type=simple`; the init sets `net.ipv4.ip_unprivileged_port_start=0` | the replacement receives no sd_notify readiness and applies no `AmbientCapabilities`, so Caddy (WSS on 443) would not start (#168) |
|
|
19
|
+
| root's `authorized_keys` from the caller, plus any injected with the passphrase; host keys removed | an encrypted rootfs cannot be provisioned from the INSTANCE message; the widget sends the deployer's key (with its bootstrap token) as the `authorized_keys` secret over the attested channel, and the init adds its key lines before the units start; host keys exist only inside it |
|
|
20
|
+
| no kernel, modules, docs, locales, apt caches | the guest runs the runtime's measured kernel; the image should stay small |
|
|
21
|
+
|
|
22
|
+
## Image size
|
|
23
|
+
|
|
24
|
+
`aleph instance create --encrypt-rootfs` uploads through the CCN, which takes at
|
|
25
|
+
most 1 GiB per file. Above ~900 MiB plain, encrypt the image yourself (LUKS2,
|
|
26
|
+
`--cipher aes-xts-plain64`, whole device, ext4 inside: the measured init
|
|
27
|
+
rejects any other cipher), add it through `ipfs.aleph.cloud`, reference it from
|
|
28
|
+
a STORE message and start it with `aleph instance create --confidential --image
|
|
29
|
+
<store hash>`. The **Aleph Confidential Relay Smoke** workflow does both.
|
|
30
|
+
orbitdb-relay is 1.7 GiB plain today; slimming it is #161.
|
|
31
|
+
|
|
32
|
+
## Known gaps
|
|
33
|
+
|
|
34
|
+
- Timers are approximated: no OnCalendar, no Persistent catch-up across a
|
|
35
|
+
reboot (an encrypted instance re-waits for its passphrase after one anyway).
|
|
36
|
+
- `aleph-vm-deploy` takes `confidential: true` with an encrypted rootfs from
|
|
37
|
+
`.github/actions/aleph-confidential-rootfs`.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# /sbin/init of a confidential (SEV-SNP) relay rootfs.
|
|
3
|
+
#
|
|
4
|
+
# Aleph's measured init unlocks the LUKS rootfs, chroots into it and runs this
|
|
5
|
+
# file. It is not PID 1, so systemd cannot run; docker-systemctl-replacement
|
|
6
|
+
# starts the enabled units instead and keeps answering the `systemctl` and
|
|
7
|
+
# `journalctl` calls the profile scripts make; confidential-systemctl.py
|
|
8
|
+
# (/usr/bin/systemctl) adds the .timer units it does not run.
|
|
9
|
+
# This script must stay in the foreground: when it exits, the VM powers off.
|
|
10
|
+
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
11
|
+
|
|
12
|
+
mkdir -p /run /dev/pts /sys/fs/cgroup /var/log
|
|
13
|
+
mount -t tmpfs -o mode=755 tmpfs /run
|
|
14
|
+
mkdir -p /run/sshd /run/lock
|
|
15
|
+
mount -t devpts -o gid=5,mode=620,ptmxmode=666 devpts /dev/pts 2>/dev/null
|
|
16
|
+
mountpoint -q /sys/fs/cgroup || mount -t cgroup2 none /sys/fs/cgroup 2>/dev/null
|
|
17
|
+
[ -s /etc/hostname ] && hostname -F /etc/hostname 2>/dev/null
|
|
18
|
+
# Units that bind low ports rely on AmbientCapabilities=CAP_NET_BIND_SERVICE,
|
|
19
|
+
# which the systemctl replacement does not apply (relay-button#168).
|
|
20
|
+
echo 0 > /proc/sys/net/ipv4/ip_unprivileged_port_start 2>/dev/null
|
|
21
|
+
|
|
22
|
+
# The CRN grows the disk, not the filesystem inside the LUKS volume, and
|
|
23
|
+
# resize2fs refuses in the chroot (it does not see / as mounted), so ask the
|
|
24
|
+
# kernel to grow it online (EXT4_IOC_RESIZE_FS).
|
|
25
|
+
if [ -b /dev/mapper/cryptroot ]; then
|
|
26
|
+
bs=$(tune2fs -l /dev/mapper/cryptroot 2>/dev/null | sed -n 's/^Block size: *//p')
|
|
27
|
+
if [ -n "$bs" ]; then
|
|
28
|
+
blocks=$(( $(blockdev --getsize64 /dev/mapper/cryptroot) / bs ))
|
|
29
|
+
perl -e 'sysopen(my $f, "/", 0) or die "open: $!"; ioctl($f, 0x40086610, pack("Q", $ARGV[0])) or die "EXT4_IOC_RESIZE_FS: $!\n"' "$blocks" \
|
|
30
|
+
> /var/log/confidential-resize.log 2>&1
|
|
31
|
+
fi
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Host keys are made on the first boot, inside the encrypted disk, so they
|
|
35
|
+
# never exist outside it.
|
|
36
|
+
ls /etc/ssh/ssh_host_*_key >/dev/null 2>&1 || ssh-keygen -A > /dev/null 2>&1
|
|
37
|
+
|
|
38
|
+
# Keys injected with the passphrase over the attested channel (the widget
|
|
39
|
+
# sends the deployer's key with its bootstrap token there, since nothing reads
|
|
40
|
+
# the INSTANCE message's authorized_keys here). Key lines only, no options.
|
|
41
|
+
if [ -s /tmp/secrets/authorized_keys ]; then
|
|
42
|
+
mkdir -p -m 700 /root/.ssh
|
|
43
|
+
touch /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys
|
|
44
|
+
grep -E '^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp(256|384|521)|sk-(ssh-ed25519|ecdsa-sha2-nistp256)@openssh\.com) [A-Za-z0-9+/]+={0,3}( [^[:cntrl:]]*)?$' /tmp/secrets/authorized_keys \
|
|
45
|
+
| while read -r line; do
|
|
46
|
+
grep -qxF "$line" /root/.ssh/authorized_keys || printf '%s\n' "$line" >> /root/.ssh/authorized_keys
|
|
47
|
+
done
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
echo "confidential-init: $(uname -r), starting the enabled units" > /dev/console
|
|
51
|
+
/usr/bin/python3 /usr/bin/systemctl __start-enabled-timers
|
|
52
|
+
exec /usr/bin/python3 /usr/lib/confidential/systemctl3.py init
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""systemctl for a confidential rootfs: timers here, everything else in
|
|
3
|
+
docker-systemctl-replacement.
|
|
4
|
+
|
|
5
|
+
docker-systemctl-replacement does not run .timer units ("start not
|
|
6
|
+
implemented for unit type"), but the relay profiles start and enable timers in
|
|
7
|
+
their configure scripts, under `set -e`. This wrapper handles start, restart,
|
|
8
|
+
stop, enable, disable, is-active, is-enabled and status for .timer units: each
|
|
9
|
+
started timer is a small background loop that starts its service after
|
|
10
|
+
OnBootSec / OnActiveSec and then every OnUnitActiveSec, with
|
|
11
|
+
RandomizedDelaySec. All other calls go to the replacement unchanged, except
|
|
12
|
+
that --no-block is honoured: the replacement ignores it and would wait for a
|
|
13
|
+
oneshot unit to finish (the AutoTLS refresh waits for its certificate), so the
|
|
14
|
+
call is run in the background and returns at once, as it does under systemd.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import random
|
|
19
|
+
import re
|
|
20
|
+
import signal
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
import time
|
|
24
|
+
|
|
25
|
+
REAL = "/usr/lib/confidential/systemctl3.py"
|
|
26
|
+
UNIT_DIRS = ["/etc/systemd/system", "/run/systemd/system", "/lib/systemd/system", "/usr/lib/systemd/system"]
|
|
27
|
+
WANTS = "/etc/systemd/system/timers.target.wants"
|
|
28
|
+
RUN_DIR = "/run/confidential-timers"
|
|
29
|
+
UNITS = {"us": 1e-6, "ms": 1e-3, "s": 1, "sec": 1, "second": 1, "seconds": 1, "m": 60, "min": 60,
|
|
30
|
+
"minute": 60, "minutes": 60, "h": 3600, "hr": 3600, "hour": 3600, "hours": 3600,
|
|
31
|
+
"d": 86400, "day": 86400, "days": 86400, "w": 604800, "week": 604800, "weeks": 604800}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def seconds(value):
|
|
35
|
+
"""systemd time span: "2min", "6h", "1h 30min", "90" (seconds)."""
|
|
36
|
+
value = (value or "").strip()
|
|
37
|
+
if not value:
|
|
38
|
+
return None
|
|
39
|
+
if re.fullmatch(r"\d+(\.\d+)?", value):
|
|
40
|
+
return float(value)
|
|
41
|
+
total = 0.0
|
|
42
|
+
for number, unit in re.findall(r"(\d+(?:\.\d+)?)\s*([a-z]+)", value):
|
|
43
|
+
total += float(number) * UNITS.get(unit, 0)
|
|
44
|
+
return total
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def unit_file(name):
|
|
48
|
+
for directory in UNIT_DIRS:
|
|
49
|
+
path = os.path.join(directory, name)
|
|
50
|
+
if os.path.exists(path):
|
|
51
|
+
return path
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def timer_settings(name):
|
|
56
|
+
path = unit_file(name)
|
|
57
|
+
if not path:
|
|
58
|
+
return None
|
|
59
|
+
settings, section = {}, None
|
|
60
|
+
with open(path) as handle:
|
|
61
|
+
for line in handle:
|
|
62
|
+
line = line.strip()
|
|
63
|
+
if line.startswith("["):
|
|
64
|
+
section = line.strip("[]")
|
|
65
|
+
elif section == "Timer" and "=" in line and not line.startswith("#"):
|
|
66
|
+
key, value = line.split("=", 1)
|
|
67
|
+
settings[key.strip()] = value.strip()
|
|
68
|
+
return settings
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def pid_file(name):
|
|
72
|
+
return os.path.join(RUN_DIR, name + ".pid")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def running_pid(name):
|
|
76
|
+
try:
|
|
77
|
+
pid = int(open(pid_file(name)).read())
|
|
78
|
+
os.kill(pid, 0)
|
|
79
|
+
return pid
|
|
80
|
+
except (OSError, ValueError):
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def run_loop(name):
|
|
85
|
+
"""The timer itself; runs detached until stopped."""
|
|
86
|
+
settings = timer_settings(name) or {}
|
|
87
|
+
service = settings.get("Unit") or name[: -len(".timer")] + ".service"
|
|
88
|
+
first = seconds(settings.get("OnActiveSec")) or seconds(settings.get("OnBootSec")) or 0
|
|
89
|
+
period = seconds(settings.get("OnUnitActiveSec")) or seconds(settings.get("OnUnitInactiveSec"))
|
|
90
|
+
jitter = seconds(settings.get("RandomizedDelaySec")) or 0
|
|
91
|
+
time.sleep(first + random.uniform(0, jitter))
|
|
92
|
+
while True:
|
|
93
|
+
subprocess.call([sys.executable, REAL, "start", service])
|
|
94
|
+
if not period:
|
|
95
|
+
return
|
|
96
|
+
time.sleep(period + random.uniform(0, jitter))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def start(name):
|
|
100
|
+
if not timer_settings(name):
|
|
101
|
+
print(f"Unit {name} not found.", file=sys.stderr)
|
|
102
|
+
return 5
|
|
103
|
+
if running_pid(name):
|
|
104
|
+
return 0
|
|
105
|
+
os.makedirs(RUN_DIR, exist_ok=True)
|
|
106
|
+
child = subprocess.Popen([sys.executable, os.path.abspath(__file__), "__run-timer", name],
|
|
107
|
+
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
108
|
+
start_new_session=True)
|
|
109
|
+
with open(pid_file(name), "w") as handle:
|
|
110
|
+
handle.write(str(child.pid))
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def stop(name):
|
|
115
|
+
pid = running_pid(name)
|
|
116
|
+
if pid:
|
|
117
|
+
os.kill(pid, signal.SIGTERM)
|
|
118
|
+
try:
|
|
119
|
+
os.remove(pid_file(name))
|
|
120
|
+
except OSError:
|
|
121
|
+
pass
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def enable(name):
|
|
126
|
+
path = unit_file(name)
|
|
127
|
+
if not path:
|
|
128
|
+
return 5
|
|
129
|
+
os.makedirs(WANTS, exist_ok=True)
|
|
130
|
+
link = os.path.join(WANTS, name)
|
|
131
|
+
if not os.path.lexists(link):
|
|
132
|
+
os.symlink(path, link)
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def disable(name):
|
|
137
|
+
link = os.path.join(WANTS, name)
|
|
138
|
+
if os.path.lexists(link):
|
|
139
|
+
os.remove(link)
|
|
140
|
+
return 0
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def handle_timers(command, names, now):
|
|
144
|
+
code = 0
|
|
145
|
+
for name in names:
|
|
146
|
+
if command == "start":
|
|
147
|
+
code |= start(name)
|
|
148
|
+
elif command == "restart":
|
|
149
|
+
stop(name)
|
|
150
|
+
code |= start(name)
|
|
151
|
+
elif command == "stop":
|
|
152
|
+
code |= stop(name)
|
|
153
|
+
elif command == "enable":
|
|
154
|
+
code |= enable(name)
|
|
155
|
+
if now:
|
|
156
|
+
code |= start(name)
|
|
157
|
+
elif command == "disable":
|
|
158
|
+
code |= disable(name)
|
|
159
|
+
if now:
|
|
160
|
+
stop(name)
|
|
161
|
+
elif command == "is-active":
|
|
162
|
+
active = running_pid(name) is not None
|
|
163
|
+
print("active" if active else "inactive")
|
|
164
|
+
code |= 0 if active else 3
|
|
165
|
+
elif command == "is-enabled":
|
|
166
|
+
enabled = os.path.lexists(os.path.join(WANTS, name))
|
|
167
|
+
print("enabled" if enabled else "disabled")
|
|
168
|
+
code |= 0 if enabled else 1
|
|
169
|
+
elif command == "status":
|
|
170
|
+
pid = running_pid(name)
|
|
171
|
+
print(f"{name} - {'active (waiting), pid ' + str(pid) if pid else 'inactive (dead)'}")
|
|
172
|
+
code |= 0 if pid else 3
|
|
173
|
+
return code
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def main(argv):
|
|
177
|
+
if len(argv) >= 2 and argv[0] == "__run-timer":
|
|
178
|
+
run_loop(argv[1])
|
|
179
|
+
return 0
|
|
180
|
+
if len(argv) >= 1 and argv[0] == "__start-enabled-timers":
|
|
181
|
+
for name in sorted(os.listdir(WANTS)) if os.path.isdir(WANTS) else []:
|
|
182
|
+
if name.endswith(".timer"):
|
|
183
|
+
start(name)
|
|
184
|
+
return 0
|
|
185
|
+
words = [arg for arg in argv if not arg.startswith("-")]
|
|
186
|
+
command = words[0] if words else ""
|
|
187
|
+
timers = [word for word in words[1:] if word.endswith(".timer")]
|
|
188
|
+
others = [word for word in words[1:] if not word.endswith(".timer")]
|
|
189
|
+
if "--no-block" in argv:
|
|
190
|
+
rest = [arg for arg in argv if arg != "--no-block"]
|
|
191
|
+
subprocess.Popen([sys.executable, os.path.abspath(__file__)] + rest,
|
|
192
|
+
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
193
|
+
start_new_session=True)
|
|
194
|
+
return 0
|
|
195
|
+
if not timers:
|
|
196
|
+
os.execv(sys.executable, [sys.executable, REAL] + argv)
|
|
197
|
+
code = handle_timers(command, timers, "--now" in argv)
|
|
198
|
+
if others:
|
|
199
|
+
flags = [arg for arg in argv if arg.startswith("-")]
|
|
200
|
+
code |= subprocess.call([sys.executable, REAL] + flags + [command] + others)
|
|
201
|
+
return code
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
if __name__ == "__main__":
|
|
205
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Turn a relay profile image (the qcow2 or raw disk the profile build makes)
|
|
3
|
+
# into a plain ext4 rootfs that `aleph instance create --confidential
|
|
4
|
+
# --encrypt-rootfs` can encrypt and boot on an SEV-SNP node.
|
|
5
|
+
#
|
|
6
|
+
# make-confidential-rootfs.sh <image> <out.ext4> <authorized_keys>
|
|
7
|
+
#
|
|
8
|
+
# What changes in the image, and why:
|
|
9
|
+
# - /sbin/init becomes confidential-init.sh (a regular file, as the measured
|
|
10
|
+
# init requires): systemd cannot be PID 1 in the chroot.
|
|
11
|
+
# - /usr/bin/systemctl and /usr/bin/journalctl become docker-systemctl-
|
|
12
|
+
# replacement, pinned below, behind confidential-systemctl.py, which runs
|
|
13
|
+
# .timer units; the profile's units and scripts keep working.
|
|
14
|
+
# - cloud-init and the systemd services that manage what the measured init
|
|
15
|
+
# already did (network, DNS, getty, timesyncd) are no longer started.
|
|
16
|
+
# - Type=notify units run as Type=simple, and the init lets any user bind
|
|
17
|
+
# ports below 1024 (the replacement has neither sd_notify nor capabilities).
|
|
18
|
+
# - root's authorized_keys come from the caller: nothing provisions an
|
|
19
|
+
# encrypted rootfs from outside. Host keys are removed and made on first boot.
|
|
20
|
+
# - Kernel, modules, docs and caches go: the guest runs the runtime's measured
|
|
21
|
+
# kernel, and the encrypted image must stay below the CCN's 1 GiB upload limit.
|
|
22
|
+
#
|
|
23
|
+
# Needs root (qemu-nbd, mount), qemu-utils, rsync, e2fsprogs and curl.
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
|
|
26
|
+
SYSTEMCTL_REPLACEMENT_COMMIT=8bd65bec50650fcc740e2d55c32162ba7bafdfc8 # v1.7.1097
|
|
27
|
+
SYSTEMCTL3_SHA256=5f5a47f321c7a8881dfd01f774106dc4012d664bee238eada86a7112674fea93
|
|
28
|
+
JOURNALCTL3_SHA256=16a52fe484d7c0fca464c82a56de58980440362942482c0d1fbc424e69f8f833
|
|
29
|
+
MAX_PLAIN_MIB="${MAX_PLAIN_MIB:-900}"
|
|
30
|
+
|
|
31
|
+
image="${1:?usage: make-confidential-rootfs.sh <image> <out.ext4> <authorized_keys>}"
|
|
32
|
+
out="${2:?missing output path}"
|
|
33
|
+
keys="${3:?missing authorized_keys file}"
|
|
34
|
+
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
35
|
+
[ "$(id -u)" = 0 ] || { echo "run as root" >&2; exit 1; }
|
|
36
|
+
[ -s "$keys" ] || { echo "authorized_keys file is empty: $keys" >&2; exit 1; }
|
|
37
|
+
|
|
38
|
+
work="$(mktemp -d)"
|
|
39
|
+
mnt="$work/mnt"
|
|
40
|
+
root="$work/root"
|
|
41
|
+
nbd=""
|
|
42
|
+
cleanup() {
|
|
43
|
+
mountpoint -q "$mnt" && umount "$mnt"
|
|
44
|
+
[ -n "$nbd" ] && qemu-nbd -d "$nbd" >/dev/null 2>&1
|
|
45
|
+
rm -rf "$work"
|
|
46
|
+
}
|
|
47
|
+
trap cleanup EXIT
|
|
48
|
+
mkdir -p "$mnt" "$root"
|
|
49
|
+
|
|
50
|
+
# 1. Mount the image's root filesystem (the largest ext4 partition).
|
|
51
|
+
modprobe nbd max_part=16
|
|
52
|
+
nbd=/dev/nbd0
|
|
53
|
+
qemu-nbd -r -c "$nbd" "$image"
|
|
54
|
+
udevadm settle
|
|
55
|
+
part="$(lsblk -lnbo NAME,FSTYPE,SIZE "$nbd" | awk '$2 == "ext4" { print $3, $1 }' | sort -n | tail -1 | cut -d' ' -f2)"
|
|
56
|
+
[ -n "$part" ] || { echo "no ext4 partition in $image" >&2; exit 1; }
|
|
57
|
+
mount -o ro "/dev/$part" "$mnt"
|
|
58
|
+
|
|
59
|
+
# 2. Copy it, leaving out what a confidential guest never uses.
|
|
60
|
+
rsync -aHAX --numeric-ids \
|
|
61
|
+
--exclude=/boot/ --exclude=/usr/lib/modules/ --exclude=/lib/modules/ \
|
|
62
|
+
--exclude=/usr/share/doc/ --exclude=/usr/share/man/ --exclude=/usr/share/info/ \
|
|
63
|
+
--exclude=/usr/share/locale/ --exclude=/var/cache/apt/ --exclude=/var/lib/apt/lists/ \
|
|
64
|
+
--exclude=/var/log/ --exclude=/var/lib/cloud/ --exclude=/tmp/ \
|
|
65
|
+
"$mnt/" "$root/"
|
|
66
|
+
mkdir -p "$root"/{boot,var/log,tmp,var/cache/apt,var/lib/apt/lists}
|
|
67
|
+
chmod 1777 "$root/tmp"
|
|
68
|
+
umount "$mnt"
|
|
69
|
+
|
|
70
|
+
# 3. The systemd replacement.
|
|
71
|
+
fetch() {
|
|
72
|
+
curl -fsSL -o "$2" "https://raw.githubusercontent.com/gdraheim/docker-systemctl-replacement/${SYSTEMCTL_REPLACEMENT_COMMIT}/files/docker/$1"
|
|
73
|
+
echo "$3 $2" | sha256sum -c - >/dev/null
|
|
74
|
+
}
|
|
75
|
+
fetch systemctl3.py "$work/systemctl3.py" "$SYSTEMCTL3_SHA256"
|
|
76
|
+
fetch journalctl3.py "$work/journalctl3.py" "$JOURNALCTL3_SHA256"
|
|
77
|
+
for bin in systemctl journalctl; do
|
|
78
|
+
for dir in usr/bin bin; do
|
|
79
|
+
[ -e "$root/$dir/$bin" ] && [ ! -L "$root/$dir/$bin" ] && mv "$root/$dir/$bin" "$root/$dir/$bin.systemd"
|
|
80
|
+
done
|
|
81
|
+
done
|
|
82
|
+
install -d "$root/usr/lib/confidential"
|
|
83
|
+
install -m 755 "$work/systemctl3.py" "$root/usr/lib/confidential/systemctl3.py"
|
|
84
|
+
# The replacement does not run .timer units; this wrapper does, and passes
|
|
85
|
+
# everything else on.
|
|
86
|
+
install -m 755 "$here/confidential-systemctl.py" "$root/usr/bin/systemctl"
|
|
87
|
+
install -m 755 "$work/journalctl3.py" "$root/usr/bin/journalctl"
|
|
88
|
+
# journalctl3.py calls systemctl3.py from its own directory.
|
|
89
|
+
ln -sf /usr/lib/confidential/systemctl3.py "$root/usr/bin/systemctl3.py"
|
|
90
|
+
[ -x "$root/usr/bin/python3" ] || { echo "the image has no python3, which the systemctl replacement needs" >&2; exit 1; }
|
|
91
|
+
|
|
92
|
+
# 4. Stop starting what the measured init already did, or what has no place here.
|
|
93
|
+
wants="$root/etc/systemd/system"
|
|
94
|
+
find "$wants" -path '*.wants/*' \( \
|
|
95
|
+
-name 'cloud-*' -o -name 'systemd-networkd*' -o -name 'systemd-resolved*' \
|
|
96
|
+
-o -name 'systemd-timesyncd*' -o -name 'networking.service' -o -name 'getty@*' \
|
|
97
|
+
-o -name 'serial-getty@*' -o -name 'e2scrub*' -o -name 'fstrim*' -o -name 'apt-daily*' \
|
|
98
|
+
-o -name 'man-db*' -o -name 'unattended-upgrades*' -o -name 'systemd-pstore*' \
|
|
99
|
+
-o -name 'qemu-guest-agent*' -o -name 'remote-fs.target' \) -delete
|
|
100
|
+
# cloud-init is also pulled in from units under /lib; nothing may start it.
|
|
101
|
+
find "$root/etc/systemd" "$root/lib/systemd/system" "$root/usr/lib/systemd/system" \
|
|
102
|
+
-name 'cloud-*' \( -type l -o -type f \) -delete 2>/dev/null || true
|
|
103
|
+
rm -f "$root/usr/bin/cloud-init" "$root/usr/bin/cloud-id"
|
|
104
|
+
[ -s "$root/etc/hostname" ] || echo relay > "$root/etc/hostname"
|
|
105
|
+
|
|
106
|
+
# 4b. Units the replacement cannot run as written (relay-button#168): it does
|
|
107
|
+
# not receive sd_notify readiness, so Type=notify becomes Type=simple (a
|
|
108
|
+
# reverse proxy such as Caddy needs no readiness handshake here); and it does
|
|
109
|
+
# not apply AmbientCapabilities, so low ports are opened to every user by the
|
|
110
|
+
# init (net.ipv4.ip_unprivileged_port_start=0) instead.
|
|
111
|
+
for dir in "$root/etc/systemd/system" "$root/lib/systemd/system" "$root/usr/lib/systemd/system"; do
|
|
112
|
+
[ -d "$dir" ] || continue
|
|
113
|
+
# grep exits 1 when a directory has none; under pipefail that would end the script.
|
|
114
|
+
{ grep -rlE '^Type=notify(-reload)?$' "$dir" --include='*.service' 2>/dev/null || true; } | while read -r unit; do
|
|
115
|
+
sed -i -E 's/^Type=notify(-reload)?$/Type=simple/; /^NotifyAccess=/d' "$unit"
|
|
116
|
+
echo "Type=notify -> simple: ${unit#$root}"
|
|
117
|
+
done
|
|
118
|
+
done
|
|
119
|
+
|
|
120
|
+
# 5. Init, DNS placeholder, SSH.
|
|
121
|
+
rm -f "$root/sbin/init"
|
|
122
|
+
install -m 755 "$here/confidential-init.sh" "$root/sbin/init"
|
|
123
|
+
rm -f "$root/etc/resolv.conf"
|
|
124
|
+
touch "$root/etc/resolv.conf"
|
|
125
|
+
rm -f "$root"/etc/ssh/ssh_host_*
|
|
126
|
+
install -d -m 700 "$root/root/.ssh"
|
|
127
|
+
install -m 600 "$keys" "$root/root/.ssh/authorized_keys"
|
|
128
|
+
ln -sf /lib/systemd/system/ssh.service "$wants/multi-user.target.wants/ssh.service" 2>/dev/null || true
|
|
129
|
+
|
|
130
|
+
# 6. A tight ext4: the CRN grows the disk, the init grows the filesystem.
|
|
131
|
+
size_mib=$(( $(du -sm "$root" | cut -f1) * 110 / 100 + 64 ))
|
|
132
|
+
echo "rootfs content: $(du -sh "$root" | cut -f1), plain image ${size_mib} MiB"
|
|
133
|
+
[ "$size_mib" -le "$MAX_PLAIN_MIB" ] || {
|
|
134
|
+
echo "largest directories:" >&2
|
|
135
|
+
du -xm --max-depth=4 "$root" 2>/dev/null | sort -n | tail -30 | sed "s|$root||" >&2
|
|
136
|
+
echo "the plain image (${size_mib} MiB) is above ${MAX_PLAIN_MIB} MiB and would exceed the 1 GiB upload limit once encrypted" >&2
|
|
137
|
+
exit 1
|
|
138
|
+
}
|
|
139
|
+
rm -f "$out"
|
|
140
|
+
truncate -s "${size_mib}M" "$out"
|
|
141
|
+
mkfs.ext4 -q -b 4096 -T default -d "$root" "$out"
|
|
142
|
+
echo "wrote $out"
|
|
@@ -71,6 +71,6 @@
|
|
|
71
71
|
],
|
|
72
72
|
"manifest": {
|
|
73
73
|
"copyTarget": "public/rootfs-manifest.json",
|
|
74
|
-
"notes": "The OrbitDB relay image prebakes Node.js, production dependencies, and Caddy into the qcow2, delays first relay start until Aleph host port mappings are known, exposes a temporary setup endpoint on port 80, and uses Caddy on port 443 to front both HTTPS helper routes and secure libp2p WSS transport for the configured proxy hostname."
|
|
74
|
+
"notes": "The OrbitDB relay image prebakes Node.js, production dependencies, and Caddy into the qcow2, delays first relay start until Aleph host port mappings are known, exposes a temporary setup endpoint on port 80, and uses Caddy on port 443 to front both HTTPS helper routes and secure libp2p WSS transport for the configured proxy hostname, and serves the attest relay (wss://<proxy hostname>/attest) that browsers use to attest and unlock confidential instances."
|
|
75
75
|
}
|
|
76
76
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Attest relay: WebSocket to an SEV-SNP guest's attestation port, behind Caddy at /attest
|
|
3
|
+
After=network-online.target
|
|
4
|
+
Wants=network-online.target
|
|
5
|
+
|
|
6
|
+
[Service]
|
|
7
|
+
Type=simple
|
|
8
|
+
# Loopback only: Caddy terminates TLS and proxies wss://<host>/attest here.
|
|
9
|
+
Environment=ATTEST_RELAY_HOST=127.0.0.1
|
|
10
|
+
Environment=ATTEST_RELAY_PORT=8787
|
|
11
|
+
Environment=ATTEST_RELAY_TRUST_PROXY=1
|
|
12
|
+
# Optional overrides (ATTEST_RELAY_ORIGINS, limits) for the operator.
|
|
13
|
+
EnvironmentFile=-/etc/default/attest-relay
|
|
14
|
+
ExecStart=/usr/bin/node /usr/local/lib/attest-relay/attest-relay.cjs
|
|
15
|
+
User=nobody
|
|
16
|
+
Group=nogroup
|
|
17
|
+
NoNewPrivileges=yes
|
|
18
|
+
ProtectSystem=strict
|
|
19
|
+
ProtectHome=yes
|
|
20
|
+
PrivateTmp=yes
|
|
21
|
+
Restart=on-failure
|
|
22
|
+
RestartSec=5
|
|
23
|
+
StandardOutput=journal
|
|
24
|
+
StandardError=journal
|
|
25
|
+
|
|
26
|
+
[Install]
|
|
27
|
+
WantedBy=multi-user.target
|
|
@@ -228,6 +228,29 @@ case "${ROOTFS_PROFILE}" in
|
|
|
228
228
|
LICENSE \
|
|
229
229
|
.env.example
|
|
230
230
|
|
|
231
|
+
# The attest relay (packages/attest-relay) as one file, so browsers reach
|
|
232
|
+
# a confidential guest's RA-TLS port through wss://<relay host>/attest.
|
|
233
|
+
# The rootfs executor bundles it into this directory before the build
|
|
234
|
+
# (a Docker build sees only this directory); a host build in the
|
|
235
|
+
# workspace can bundle it here.
|
|
236
|
+
ATTEST_RELAY_BUNDLE="${ATTEST_RELAY_BUNDLE:-${SCRIPT_DIR}/attest-relay.cjs}"
|
|
237
|
+
attest_relay_bundler="${SCRIPT_DIR}/../../../../attest-relay/scripts/bundle.mjs"
|
|
238
|
+
if [ ! -s "${ATTEST_RELAY_BUNDLE}" ] && [ -f "${attest_relay_bundler}" ]; then
|
|
239
|
+
node "${attest_relay_bundler}" "${ATTEST_RELAY_BUNDLE}" || true
|
|
240
|
+
fi
|
|
241
|
+
attest_relay_args=()
|
|
242
|
+
if [ -s "${ATTEST_RELAY_BUNDLE}" ]; then
|
|
243
|
+
attest_relay_args=(
|
|
244
|
+
--mkdir /usr/local/lib/attest-relay
|
|
245
|
+
--copy-in "${ATTEST_RELAY_BUNDLE}:/usr/local/lib/attest-relay"
|
|
246
|
+
--copy-in "${SCRIPT_DIR}/attest-relay.service:/etc/systemd/system"
|
|
247
|
+
--run-command "cd /usr/local/lib/attest-relay && [ -f attest-relay.cjs ] || mv '$(basename "${ATTEST_RELAY_BUNDLE}")' attest-relay.cjs"
|
|
248
|
+
--run-command "systemctl enable attest-relay.service"
|
|
249
|
+
)
|
|
250
|
+
else
|
|
251
|
+
echo "WARNING: no attest relay bundle at ${ATTEST_RELAY_BUNDLE}; this image will not serve /attest." >&2
|
|
252
|
+
fi
|
|
253
|
+
|
|
231
254
|
orbitdb_customize_args=(
|
|
232
255
|
-a "${IMAGE}"
|
|
233
256
|
--mkdir /opt/orbitdb-relay
|
|
@@ -262,6 +285,7 @@ case "${ROOTFS_PROFILE}" in
|
|
|
262
285
|
--run-command "systemctl enable orbitdb-relay-bootstrap.service"
|
|
263
286
|
--run-command "systemctl enable orbitdb-relay-autotls-refresh.timer"
|
|
264
287
|
--run-command "systemctl enable orbitdb-relay.service"
|
|
288
|
+
"${attest_relay_args[@]}"
|
|
265
289
|
--run-command "rm -f /opt/$(basename "${APP_TAR}")"
|
|
266
290
|
)
|
|
267
291
|
|
|
@@ -13,6 +13,7 @@ CADDYFILE="${CADDYFILE:-/etc/caddy/Caddyfile}"
|
|
|
13
13
|
CADDY_UPSTREAM_WSS_PORT="${CADDY_UPSTREAM_WSS_PORT:-9092}"
|
|
14
14
|
CADDY_UPSTREAM_HOST="${CADDY_UPSTREAM_HOST:-127.0.0.1}"
|
|
15
15
|
CADDY_UPSTREAM_METRICS_PORT="${CADDY_UPSTREAM_METRICS_PORT:-9090}"
|
|
16
|
+
CADDY_UPSTREAM_ATTEST_PORT="${CADDY_UPSTREAM_ATTEST_PORT:-8787}"
|
|
16
17
|
# Internal libp2p listen ports. On the Aleph VM, IPv4 is NAT/port-mapped (so it
|
|
17
18
|
# is announced on the externally assigned host ports), but IPv6 is globally
|
|
18
19
|
# routed with NO NAT — the VM is reachable directly on the real listen ports,
|
|
@@ -158,6 +159,12 @@ https://${hostname} {
|
|
|
158
159
|
reverse_proxy 127.0.0.1:${CADDY_UPSTREAM_METRICS_PORT}
|
|
159
160
|
}
|
|
160
161
|
|
|
162
|
+
# The attest relay (attest-relay.service): a browser's RA-TLS records to a
|
|
163
|
+
# confidential guest's attestation port, which it cannot dial itself.
|
|
164
|
+
handle /attest {
|
|
165
|
+
reverse_proxy 127.0.0.1:${CADDY_UPSTREAM_ATTEST_PORT}
|
|
166
|
+
}
|
|
167
|
+
|
|
161
168
|
reverse_proxy ${CADDY_UPSTREAM_HOST}:${CADDY_UPSTREAM_WSS_PORT}
|
|
162
169
|
}
|
|
163
170
|
EOF
|
|
Binary file
|