@celilo/e2e 0.9.3 → 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.
Files changed (52) hide show
  1. package/config/cpanel-host/cpanel-host-ca.crt +20 -0
  2. package/config/cpanel-host/docroot/.htaccess +14 -0
  3. package/config/cpanel-host/docroot/cms.html +4 -0
  4. package/config/cpanel-host/site-tls.crt +20 -0
  5. package/config/cpanel-host/site-tls.key +28 -0
  6. package/config/cpanel-host/site.conf +27 -0
  7. package/config/dns/com.zone +7 -0
  8. package/config/dns/knot-namecheap.conf +6 -0
  9. package/config/dns/tangohost.com.zone +14 -0
  10. package/config/resolver/unbound.conf +26 -0
  11. package/config/routing/management-routes.sh +9 -4
  12. package/config/routing/resolver-internal-routes.sh +10 -0
  13. package/docker/Dockerfile.cpanel-host-sim +70 -0
  14. package/docker/Dockerfile.firewall +12 -0
  15. package/docker/Dockerfile.management +6 -0
  16. package/docker/Dockerfile.observer +11 -0
  17. package/docker/Dockerfile.signal-cli +60 -0
  18. package/docker/Dockerfile.signal-release +36 -0
  19. package/docker/Dockerfile.signal-sim +17 -0
  20. package/docker/Dockerfile.target-machine +9 -0
  21. package/package.json +3 -3
  22. package/registry-server/package.json +17 -0
  23. package/registry-server/src/auth.test.ts +76 -0
  24. package/registry-server/src/auth.ts +105 -0
  25. package/registry-server/src/bootstrap-packaging.test.ts +71 -0
  26. package/registry-server/src/bootstrap.ts +246 -0
  27. package/registry-server/src/index.ts +16 -0
  28. package/registry-server/src/introspection.test.ts +247 -0
  29. package/registry-server/src/introspection.ts +204 -0
  30. package/registry-server/src/landing.ts +90 -0
  31. package/registry-server/src/module-owner-store.test.ts +85 -0
  32. package/registry-server/src/module-owner-store.ts +108 -0
  33. package/registry-server/src/rate-limit.test.ts +62 -0
  34. package/registry-server/src/rate-limit.ts +83 -0
  35. package/registry-server/src/scoped-token-store.test.ts +93 -0
  36. package/registry-server/src/scoped-token-store.ts +110 -0
  37. package/registry-server/src/server.test.ts +771 -0
  38. package/registry-server/src/server.ts +701 -0
  39. package/registry-server/src/storage.test.ts +148 -0
  40. package/registry-server/src/storage.ts +150 -0
  41. package/registry-server/src/validation.test.ts +86 -0
  42. package/registry-server/src/validation.ts +60 -0
  43. package/registry-server/tsconfig.json +15 -0
  44. package/scripts/stage-libsignal.ts +116 -0
  45. package/simulators/signal-cli/server.ts +272 -0
  46. package/src/cli/build.ts +7 -0
  47. package/src/cli/index.ts +6 -0
  48. package/src/container-manager.ts +5 -1
  49. package/src/docker-compose-generator.ts +108 -4
  50. package/src/network-builder.ts +52 -0
  51. package/src/simulator-ips.ts +9 -0
  52. package/src/types.ts +34 -0
@@ -0,0 +1,20 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDMzCCAhugAwIBAgIUNwg2dEjTZomtYfUlMacwvA348c4wDQYJKoZIhvcNAQEL
3
+ BQAwKDEmMCQGA1UEAwwdY2VsaWxvIGUyZSBjUGFuZWwtaG9zdCBzaW0gQ0EwIBcN
4
+ MjYwNzI5MTI1MDM3WhgPMjEyNjA3MDUxMjUwMzdaMCgxJjAkBgNVBAMMHWNlbGls
5
+ byBlMmUgY1BhbmVsLWhvc3Qgc2ltIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
6
+ MIIBCgKCAQEAqCdcXJ1jCLtUBVTH/Tu9VMhGnVjWy6UL5s99zprxxH09u23UWwAa
7
+ Ya48BIBbS25bv9YinLfWo5BtL7fOtZYNw3LpWSYi6NRAyUldt9mDOQ0jufQRZs5E
8
+ 3HiRRIdHKF5ABQRTM7toMWFW6X7kTSH4ySJqQUtXThf/9C4bca8dDLNwIt+3HMfq
9
+ 9vLP7uobTtNkasNZhmpKePtU5U90igwF04bOKF51nvTBABpuK/MrSOVXFjpabsrl
10
+ LHy7dG0I2J3C//N++qeBpXqEO1O7r9rcGN97FV7s5IM1TF0s/JPnztqt7mgiXYQf
11
+ bFAQ91Wrog2ROSecAxaXG/T2SrpTzVkrKQIDAQABo1MwUTAdBgNVHQ4EFgQUkib0
12
+ DPU9hPj3xiY5hHjHcgq/bNcwHwYDVR0jBBgwFoAUkib0DPU9hPj3xiY5hHjHcgq/
13
+ bNcwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAdbGBHyoCoaAC
14
+ mIM8VMjWBUwC6lPiPsrKXidym6AH1Sz446ZUje+q/h1NSeNx6hRpW92gY8AYylmN
15
+ cnzs6fB2cibToxxIQM1IgdGdjRBHvyJKjDOC9oWrRxuzEqoDtM3IFIO0rWmW8ubF
16
+ ouxakB1EAm9Lg+I7ZfljGjK7lhdPOnUPcciNR4TguDyeNFzUIvI9Ajc4HDgnom41
17
+ 8lJ7kgYFiyVsThKXGalO2RphjGJ9mdF4UzQnukJQZSe+tsFhREsZtUFFAApZy6QF
18
+ XdC33NNtDAOPH/iYL1E+t2E50t1NfC6z7keiBPFzfWVeNGr6dGko7xZlr5YkSCym
19
+ ++fPX7Ivrg==
20
+ -----END CERTIFICATE-----
@@ -0,0 +1,14 @@
1
+ # BEGIN WordPress
2
+ # The stock WordPress front-controller rules, verbatim in shape: any request
3
+ # that is NOT a real file and NOT a real directory is handed to the CMS. A
4
+ # published subfolder (e.g. /nexus/) IS a real directory, so it is served from
5
+ # disk and never reaches the CMS — which is exactly what external_web relies on.
6
+ <IfModule mod_rewrite.c>
7
+ RewriteEngine On
8
+ RewriteBase /
9
+ RewriteRule ^index\.php$ - [L]
10
+ RewriteCond %{REQUEST_FILENAME} !-f
11
+ RewriteCond %{REQUEST_FILENAME} !-d
12
+ RewriteRule . /cms.html [L]
13
+ </IfModule>
14
+ # END WordPress
@@ -0,0 +1,4 @@
1
+ <!doctype html>
2
+ <title>Tango Host CMS</title>
3
+ <h1>CELILO_E2E_CMS_MARKER</h1>
4
+ <p>PHP-free stand-in for the host's own WordPress front controller.</p>
@@ -0,0 +1,20 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDPzCCAiegAwIBAgIUBUnN9NoJfptkeic1uhSMmvr/KskwDQYJKoZIhvcNAQEL
3
+ BQAwKDEmMCQGA1UEAwwdY2VsaWxvIGUyZSBjUGFuZWwtaG9zdCBzaW0gQ0EwIBcN
4
+ MjYwNzI5MTI1MDM3WhgPMjEyNjA3MDUxMjUwMzdaMBgxFjAUBgNVBAMMDXRhbmdv
5
+ aG9zdC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDXAn3dqIFu
6
+ MjApQp1HL+V3O6rTxgH5Z+0x3PqFegGDD1sPR1FNj9X5WfG7cipZgvQsjVI8xE/b
7
+ 8j9BDLyUdhyIiDiDGnz4goGE45t6n3kN9llaOLHEvPliZSx/XuCseu6LbzwMGJc6
8
+ bdB6PFmKr0FYXZX67KwFyN/s/IXEpaCfJIEjPF7VJDXW+p4XMGkPuIgGki7+8u9p
9
+ ZfhP5rZPPxEyWxjruS72nxfPC97G7LhSh40i5KGAQIYaSYuPuhUr+3xEdF8DVmA1
10
+ x+JtXBkIBfrIfC64qDwVk1QTUn0FhYsBBL8L4eynED/pRaToOWPggZSmie1nYAtE
11
+ pzNgAACOIO13AgMBAAGjbzBtMCsGA1UdEQQkMCKCDXRhbmdvaG9zdC5jb22CEXd3
12
+ dy50YW5nb2hvc3QuY29tMB0GA1UdDgQWBBS7FibNCjP8A4HR4SrlP5taWQfPmjAf
13
+ BgNVHSMEGDAWgBSSJvQM9T2E+PfGJjmEeMdyCr9s1zANBgkqhkiG9w0BAQsFAAOC
14
+ AQEAQPTJ95Lu7RhObp8ONTfbt275GsO7osRqPX8EoWsySu1Vhs7BWvOgs0zlRgo2
15
+ QSAyXuW5ZmHEVvq1OOfsA/HAkZn0H1h6z6Fz7RHlgYLFhkd6IAE8JIg705a9w2Zo
16
+ denA9PV2SFCsEEIa7hQ/BG4YgvBvf7pCOtdHg3WlUyhLMvkENJh2m/x4AATmICqs
17
+ uNCu0HuxpaitaiqtTyXcAy1yRITPoZYfKt66j6MmEaPB/e0Rs3GcQuY2VsTb3YYS
18
+ q5cEjxxOhDZL50d2F4UTwoy9244lBgIO/T1MCMlnLuqMmRDxvF4drTmt2QjShJ6n
19
+ z/x7eUUMGyBbHqBE73ZgG26eEQ==
20
+ -----END CERTIFICATE-----
@@ -0,0 +1,28 @@
1
+ -----BEGIN PRIVATE KEY-----
2
+ MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDXAn3dqIFuMjAp
3
+ Qp1HL+V3O6rTxgH5Z+0x3PqFegGDD1sPR1FNj9X5WfG7cipZgvQsjVI8xE/b8j9B
4
+ DLyUdhyIiDiDGnz4goGE45t6n3kN9llaOLHEvPliZSx/XuCseu6LbzwMGJc6bdB6
5
+ PFmKr0FYXZX67KwFyN/s/IXEpaCfJIEjPF7VJDXW+p4XMGkPuIgGki7+8u9pZfhP
6
+ 5rZPPxEyWxjruS72nxfPC97G7LhSh40i5KGAQIYaSYuPuhUr+3xEdF8DVmA1x+Jt
7
+ XBkIBfrIfC64qDwVk1QTUn0FhYsBBL8L4eynED/pRaToOWPggZSmie1nYAtEpzNg
8
+ AACOIO13AgMBAAECggEADjLz7Pd71LMryw3DJThchwtBtMqWxsm+OFTHlpkAEI+r
9
+ C89B50VgdB8kf6SCd3bWeTq7+5dbTsMoujsFGSjTqZCVatMM/bxgoU4GroppjCqA
10
+ rByiYMO6FG1qCt9RJxLXPg4SgRn+orUt12S05Ru9zQAhL3kBw0AIybXDf8EQYnH/
11
+ tbRhx3/YHcaIgRjzd/zTBmfgmiYNGInNO8q2O6UssuHpdFC2YPHkdHhLZNWX5gBz
12
+ 0VHK++ff4ZkpzOOZVlS//UoKMMtwez4dutu+ZX7jGCeXkKEwuNyXmPBRj5vpeGoy
13
+ J/CaGArR+7ut/dOqZv7bEF5bp6dZWXxjdHtaga+OwQKBgQDw2efq+In79MfRgdJt
14
+ duk0wkuGress648qzPczGfLu7LzwY7Q+QUVPgrziUmkEgw/Xvw+TarLi1KaBizYt
15
+ yP2kB3HP7Z4Xufe7M3N/98S9VcOELH8/MT4L5uxhkqeaJ0n4ev3alEih4HKCL4Bs
16
+ NKqdWPds6EE+hOoVsTJLS17JowKBgQDkiH8SJ0WL4zmGknEdI3CB7BbsVlCtR/Wx
17
+ Qw6fsn5Fwian7SfMLJSc3FpZ2E0BM8PJKNLoXU9sxGRgQT0HczGFyAKW9ifh4K/r
18
+ EovG4+9Xh9s56c8AFTs6mMQgL1lhsFgyzUwNxscGOTqp949QAHe7QLY6G5IzYi3q
19
+ /8p0m5jyHQKBgDrYQn16M9xHNfrOCSeNgfTN8734CBl1hAklAtCNanlNhewFsCf1
20
+ soV2n8d78wSTMO+5Uojl7MbWxceox5fwH2hQGVJ/77xER26JZ5LAamUH6zkD1iMO
21
+ WOZ3iRFdz20uZDg9dIBO8l/6gn+kM3A8feLrtaf6JtarirW6EVusEoCZAoGAOoRM
22
+ 7UwK/ce/+hpTRl/Fti7Kq9dqLwSG/82o1/FvbdNJ7KPIFfqwTv4JUIoXJaComECQ
23
+ yJjj9njPLuplgxvW6WeOajM7gRX0VZ/ZFKNWrLYHx2t76oyFLvrg9VDKdT4fR1rU
24
+ B48HdSuAPvc5xgVooxTt1DEtHWUdVtgKchNJdW0CgYBdYIBZr/tafno7kXJAGl2Q
25
+ /I7hNXvtAsw8OMU8mls6DXc2RXxO1J4V67IjBoyqyFoBZD10N5Si7vxgCdIv9Vhc
26
+ b/wQAHC9b2C4qX2i6UlUL60XQmNlGchh/fLL8R9dOE0+rhvkkK+CyF2vRUuGxchE
27
+ JZdQ89koDLYQMtdzeEGnOg==
28
+ -----END PRIVATE KEY-----
@@ -0,0 +1,27 @@
1
+ # The off-fleet host's own vhost. celilo does NOT manage this file — the point
2
+ # of external_web is that the host owns its web server, TLS, and DNS, and
3
+ # celilo only drops files into the document root.
4
+ <VirtualHost *:80>
5
+ ServerName tangohost.com
6
+ ServerAlias www.tangohost.com
7
+ DocumentRoot /home/theglo18/tangohost.com
8
+ <Directory /home/theglo18/tangohost.com>
9
+ Options -Indexes +FollowSymLinks
10
+ AllowOverride All
11
+ Require all granted
12
+ </Directory>
13
+ </VirtualHost>
14
+
15
+ <VirtualHost *:443>
16
+ ServerName tangohost.com
17
+ ServerAlias www.tangohost.com
18
+ DocumentRoot /home/theglo18/tangohost.com
19
+ SSLEngine on
20
+ SSLCertificateFile /etc/ssl/cpanel-sim/site-tls.crt
21
+ SSLCertificateKeyFile /etc/ssl/cpanel-sim/site-tls.key
22
+ <Directory /home/theglo18/tangohost.com>
23
+ Options -Indexes +FollowSymLinks
24
+ AllowOverride All
25
+ Require all granted
26
+ </Directory>
27
+ </VirtualHost>
@@ -9,3 +9,10 @@ com. IN NS tld-dns.lab.
9
9
 
10
10
  park-your-domain IN NS ns1.park-your-domain.com.
11
11
  ns1.park-your-domain.com. IN A 100.64.0.55
12
+
13
+ ; The OFF-FLEET web host (external_web). Unlike every customer name, this one
14
+ ; resolves to the host's OWN public address (100.64.0.63), NOT the customer
15
+ ; firewall — because the host genuinely is a third party out on the internet,
16
+ ; not a service behind the customer's NAT. No RFC1918 leak: .62 is public.
17
+ tangohost IN NS ns1.tangohost.com.
18
+ ns1.tangohost.com. IN A 100.64.0.55
@@ -14,6 +14,12 @@ zone:
14
14
  file: "/config/example.net.zone"
15
15
  storage: "/var/lib/knot"
16
16
 
17
+ # The OFF-FLEET web host's own zone. Served here only because this is the
18
+ # simulator's authoritative nameserver; celilo never writes to it.
19
+ - domain: "tangohost.com"
20
+ file: "/config/tangohost.com.zone"
21
+ storage: "/var/lib/knot"
22
+
17
23
  - domain: "park-your-domain.com"
18
24
  file: "/config/park-your-domain.com.zone"
19
25
  storage: "/var/lib/knot"
@@ -0,0 +1,14 @@
1
+ $ORIGIN tangohost.com.
2
+ $TTL 300
3
+
4
+ @ IN SOA ns1.tangohost.com. admin.tangohost.com. (
5
+ 2026072901 300 60 604800 300
6
+ )
7
+
8
+ ; The OFF-FLEET host's own public DNS — it owns this zone, celilo does not.
9
+ ; Both names point at the host's own public address, NOT the customer
10
+ ; firewall (100.100.0.100): this host lives out on the simulated internet.
11
+ @ IN NS ns1.tangohost.com.
12
+ ns1 IN A 100.64.0.55
13
+ @ IN A 100.64.0.63
14
+ www IN A 100.64.0.63
@@ -23,6 +23,21 @@ server:
23
23
  local-data: "npm-registry.lab. IN A 100.64.0.59"
24
24
  # MinIO S3 simulator — backup/restore target. See SIMULATOR_IPS.MINIO.
25
25
  local-data: "minio.lab. IN A 100.64.0.61"
26
+ # apt-repo simulator. Also needed HERE, not only in the internal resolver:
27
+ # module target hosts resolve through the PUBLIC resolver, and in production
28
+ # apt.celilo.computer is a publicly-resolvable name — so a host fetching a
29
+ # package from it is the faithful case, not a shortcut.
30
+ local-data: "apt.celilo.lab. IN A 100.64.0.60"
31
+ # signal-cli daemon (real, unlinked) and its simulator counterpart. Both
32
+ # sit on the `internal` network — celilo-mgr reaches a notification
33
+ # transport locally; a transport needing public ingress could not tell
34
+ # you the ingress was broken.
35
+ local-data: "signal-cli.lab. IN A 192.168.0.90"
36
+ local-data: "signal-sim.lab. IN A 192.168.0.91"
37
+ # signal-cli release host — the sim internet's stand-in for GitHub
38
+ # releases, so the signal module's deploy-time download resolves inside
39
+ # the sealed network. See SIMULATOR_IPS.SIGNAL_RELEASE.
40
+ local-data: "signal-release.lab. IN A 100.64.0.62"
26
41
  # External website prober simulator — without this override, isitup.org
27
42
  # falls through the "forward-zone: ." below and resolves to the REAL
28
43
  # Namecheap-parked domain on the public internet.
@@ -50,6 +65,17 @@ stub-zone:
50
65
  name: "park-your-domain.com."
51
66
  stub-addr: 100.64.0.55
52
67
 
68
+ # The OFF-FLEET web host's domain (external_web). Like every other simulated
69
+ # domain it needs a stub, or it falls through to the real internet — and
70
+ # tangohost.com is a REAL registered name out there, so the leak resolves to a
71
+ # real address and the SSH bootstrap times out against a stranger's server.
72
+ # namecheap-dns is authoritative for the zone; the zone points at the host's own
73
+ # public address (100.64.0.63), NOT the customer firewall, because the host
74
+ # genuinely is a third party on the internet.
75
+ stub-zone:
76
+ name: "tangohost.com."
77
+ stub-addr: 100.64.0.55
78
+
53
79
  # Everything else → the real internet, via Docker's embedded resolver.
54
80
  #
55
81
  # 127.0.0.11 is Docker's per-container DNS; it relays to whatever the HOST is
@@ -22,17 +22,22 @@ chmod 600 /root/.ssh/config
22
22
  # Routing — DEFAULT_GATEWAY comes from docker-compose environment
23
23
  # default: 192.168.0.1 (fw-isp), direct-internet: 192.168.0.254 (fw-main)
24
24
  GATEWAY="${DEFAULT_GATEWAY:-192.168.0.1}"
25
+ # Next hop into the segmented zones. On the default topology celilo-mgr is on the
26
+ # internal LAN and reaches them via fw-main's internal leg (192.168.0.254). When
27
+ # celilo-mgr lives on its OWN control-plane network (managementZone secure-mgmt),
28
+ # it reaches them via fw-main's leg on THAT network instead.
29
+ FW_MAIN="${FW_MAIN_HOP:-192.168.0.254}"
25
30
  ip route del default 2>/dev/null || true
26
31
  ip route add default via "$GATEWAY"
27
- ip route add 10.0.10.0/24 via 192.168.0.254 # dmz via fw-main
28
- ip route add 10.0.20.0/24 via 192.168.0.254 # app via fw-main
29
- ip route add 10.0.30.0/24 via 192.168.0.254 # secure via fw-main
32
+ ip route add 10.0.10.0/24 via "$FW_MAIN" # dmz via fw-main
33
+ ip route add 10.0.20.0/24 via "$FW_MAIN" # app via fw-main
34
+ ip route add 10.0.30.0/24 via "$FW_MAIN" # secure via fw-main
30
35
 
31
36
  # DNS — Technitium internal resolver (split-horizon) with public fallback.
32
37
  # Technitium at 192.168.0.10 is authoritative for iamtheinternet.org internally,
33
38
  # resolving www/auth to Caddy's DMZ IP (10.0.10.10) rather than the public WAN IP.
34
39
  # Fall back to the simulated public resolver if Technitium is not yet up.
35
- printf 'nameserver 192.168.0.10\nnameserver 100.100.0.1\n' > /etc/resolv.conf
40
+ printf 'nameserver %s\nnameserver 100.100.0.1\n' "${INTERNAL_RESOLVER:-192.168.0.10}" > /etc/resolv.conf
36
41
 
37
42
  # Fetch Pebble's runtime ACME root CA (Pebble generates a fresh root at each
38
43
  # startup, unlike the static /config/pebble/pebble-ca.crt baked in at build
@@ -57,6 +57,16 @@ $(printf "${SPLIT_HORIZON_BLOCK}")
57
57
  # MinIO S3 simulator — backup/restore target for the migration e2e.
58
58
  # See SIMULATOR_IPS.MINIO.
59
59
  local-data: "minio.lab. IN A 100.64.0.61"
60
+ # signal-cli daemon (real, unlinked) and its simulator counterpart. Both
61
+ # sit on the `internal` network — celilo-mgr reaches a notification
62
+ # transport locally; a transport needing public ingress could not tell
63
+ # you the ingress was broken.
64
+ local-data: "signal-cli.lab. IN A 192.168.0.90"
65
+ local-data: "signal-sim.lab. IN A 192.168.0.91"
66
+ # signal-cli release host — the sim internet's stand-in for GitHub
67
+ # releases, so the signal module's deploy-time download resolves inside
68
+ # the sealed network. See SIMULATOR_IPS.SIGNAL_RELEASE.
69
+ local-data: "signal-release.lab. IN A 100.64.0.62"
60
70
 
61
71
  forward-zone:
62
72
  name: "."
@@ -0,0 +1,70 @@
1
+ # An OFF-FLEET web host: a third-party cPanel/SSH shared account that celilo
2
+ # publishes to but does NOT govern. It is a PUBLIC PEER on `internet-external`
3
+ # alongside namecheap-dns and pebble — NOT behind the customer firewall — so
4
+ # public DNS resolving its domain to its own address bends no RFC1918 rule.
5
+ #
6
+ # Faithful to what we verified against the live A2/CloudLinux account:
7
+ # - sshd on 7822, an unprivileged account (no root, no systemd control)
8
+ # - Apache + mod_rewrite serving the stock WordPress .htaccess, so a real
9
+ # directory under the docroot is served BEFORE the CMS front controller
10
+ # - the domain's TLS certificate pre-installed (as AutoSSL would leave it),
11
+ # not obtained via ACME — external_web never owns TLS
12
+ # - ~/.cpanel/userdata/<domain> carrying the authoritative documentroot
13
+ #
14
+ # No systemd: sshd + apache are all this host runs, so a two-process entrypoint
15
+ # is enough and boots in a fraction of the time.
16
+ FROM ubuntu:22.04
17
+
18
+ ENV DEBIAN_FRONTEND=noninteractive
19
+
20
+ COPY config/apt/99retries.conf /etc/apt/apt.conf.d/99retries.conf
21
+
22
+ RUN apt-get update && apt-get install -y \
23
+ openssh-server \
24
+ apache2 \
25
+ ca-certificates \
26
+ iproute2 \
27
+ iputils-ping \
28
+ curl \
29
+ && rm -rf /var/lib/apt/lists/*
30
+
31
+ RUN a2enmod rewrite ssl && a2dissite 000-default
32
+
33
+ # sshd on 7822 with password auth ON — the bootstrap's whole job is to log in
34
+ # with the account password once and install celilo's key, so the account must
35
+ # accept a password until that happens (as a real shared host does).
36
+ RUN mkdir -p /run/sshd && \
37
+ sed -i 's/^#\?Port .*/Port 7822/' /etc/ssh/sshd_config && \
38
+ sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config && \
39
+ sed -i 's/^#\?PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config && \
40
+ sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
41
+
42
+ # The account. Unprivileged and NOT in sudo — celilo cannot run a daemon, bind
43
+ # a port, or touch the firewall here. That inability is what "off-fleet" means.
44
+ RUN useradd -m -s /bin/bash theglo18 && \
45
+ echo 'theglo18:tango-e2e-password' | chpasswd
46
+
47
+ # The host's own site, in the account's home the way cPanel lays an addon
48
+ # domain out, plus the userdata file external_web reads the docroot from.
49
+ COPY config/cpanel-host/docroot/ /home/theglo18/tangohost.com/
50
+ RUN mkdir -p /home/theglo18/.cpanel/userdata && \
51
+ printf 'documentroot: /home/theglo18/tangohost.com\nservername: tangohost.com\nuser: theglo18\n' \
52
+ > /home/theglo18/.cpanel/userdata/tangohost.com && \
53
+ chown -R theglo18:theglo18 /home/theglo18
54
+
55
+ # Apache runs as www-data but must read/serve files the account writes.
56
+ RUN usermod -aG theglo18 www-data && chmod 755 /home/theglo18
57
+
58
+ # Pre-installed TLS for the domain (a committed sim CA + leaf, so the cert is
59
+ # deterministic). mgmt trusts this CA — see Dockerfile.management.
60
+ COPY config/cpanel-host/site-tls.crt config/cpanel-host/site-tls.key /etc/ssl/cpanel-sim/
61
+ COPY config/cpanel-host/site.conf /etc/apache2/sites-available/tangohost.conf
62
+ RUN a2ensite tangohost && chmod 600 /etc/ssl/cpanel-sim/site-tls.key
63
+
64
+ EXPOSE 7822 80 443
65
+
66
+ # apache2 is PID 1; sshd runs beside it. If either dies the container exits,
67
+ # which is what we want from a fixture host. The runtime/lock dirs live under
68
+ # /var/run, which the container wipes at start, so recreate them before apache
69
+ # reads DefaultRuntimeDir.
70
+ CMD ["/bin/sh", "-c", "mkdir -p /var/run/apache2 /var/lock/apache2 /run/sshd && /usr/sbin/sshd && . /etc/apache2/envvars && exec apache2 -D FOREGROUND"]
@@ -12,8 +12,20 @@ RUN apt-get update && apt-get install -y \
12
12
  dnsutils \
13
13
  procps \
14
14
  python3 \
15
+ wireguard-tools \
16
+ wireguard-go \
15
17
  && rm -rf /var/lib/apt/lists/*
16
18
 
19
+ # Containers have no wireguard kernel module to load, and a test must not depend
20
+ # on whether the Docker host happens to have one. wg-quick falls back to this
21
+ # userspace implementation when `ip link add type wireguard` fails, so the tunnel
22
+ # behaves the same on any host — it just needs /dev/net/tun + NET_ADMIN.
23
+ # NOTE the value: Ubuntu's `wireguard-go` package installs its binary as
24
+ # /usr/bin/wireguard, NOT `wireguard-go`. Naming the binary wrong here fails
25
+ # only on a host WITHOUT the kernel module — i.e. it passes locally and
26
+ # breaks in CI, which is the worst possible way for this to be wrong.
27
+ ENV WG_QUICK_USERSPACE_IMPLEMENTATION=wireguard
28
+
17
29
  RUN mkdir -p /run/sshd && \
18
30
  sed -i 's/#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config && \
19
31
  sed -i 's/#PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config
@@ -44,6 +44,12 @@ RUN chsh -s /bin/zsh root
44
44
  COPY config/proxy/squid-ca.crt /usr/local/share/ca-certificates/e2e-proxy-ca.crt
45
45
  # Trust Pebble's test CA (so bun fetch() trusts certs issued by the simulated Let's Encrypt)
46
46
  COPY config/pebble/pebble-ca.crt /usr/local/share/ca-certificates/pebble-ca.crt
47
+ # Trust the OFF-FLEET cPanel host sim's CA. That host's cert is PRE-INSTALLED
48
+ # (as a real shared host's AutoSSL cert is) rather than issued by Pebble —
49
+ # external_web never owns TLS — so its CA is a separate, static trust anchor.
50
+ # external_web's authoritative post-publish check HTTPS-GETs the route from
51
+ # here, which is why mgmt in particular must trust it.
52
+ COPY config/cpanel-host/cpanel-host-ca.crt /usr/local/share/ca-certificates/cpanel-host-ca.crt
47
53
  RUN update-ca-certificates
48
54
 
49
55
  # Tell bun (and Node-compat code) to merge the system CA bundle with
@@ -18,8 +18,19 @@ RUN apt-get update && apt-get install -y \
18
18
  iputils-ping \
19
19
  iproute2 \
20
20
  ca-certificates \
21
+ wireguard-tools \
22
+ wireguard-go \
21
23
  && rm -rf /var/lib/apt/lists/*
22
24
 
25
+ # A vantage may need to dial a VPN and probe from INSIDE the tunnel — the only
26
+ # way to assert VPN reach by real signal rather than by reading a rule string.
27
+ # Userspace implementation so the test doesn't depend on the Docker host's kernel.
28
+ # NOTE the value: Ubuntu's `wireguard-go` package installs its binary as
29
+ # /usr/bin/wireguard, NOT `wireguard-go`. Naming the binary wrong here fails
30
+ # only on a host WITHOUT the kernel module — i.e. it passes locally and
31
+ # breaks in CI, which is the worst possible way for this to be wrong.
32
+ ENV WG_QUICK_USERSPACE_IMPLEMENTATION=wireguard
33
+
23
34
  COPY config/routing/observer-setup.sh /startup.sh
24
35
  RUN chmod +x /startup.sh
25
36
 
@@ -0,0 +1,60 @@
1
+ # Real signal-cli, for verifying celilo's client against the actual JSON-RPC
2
+ # interface rather than against a simulator that encodes our assumptions.
3
+ #
4
+ # The account is never linked. That is deliberate and sufficient: signal-cli's
5
+ # HTTP interface is served locally and does not touch Signal's network, so the
6
+ # endpoint paths, envelope shapes, error shapes, SSE receive stream, and the
7
+ # Host/Content-Type guards are all verifiable without an account.
8
+ #
9
+ # What an unlinked daemon CANNOT verify is that a linked account delivers a
10
+ # message to a phone. That needs Signal's real network. DNS is ours in e2e and
11
+ # even the TLS trust store is a swappable classpath resource (`whisper.store`,
12
+ # password "whisper" — see StagingConfig.java), but CDSI/SVR2 use SGX remote
13
+ # attestation whose whole purpose is to make substitution impossible. That
14
+ # limit is real; everything short of it is not.
15
+ #
16
+ # Three facts learned by RUNNING this rather than reading about it:
17
+ # - signal-cli 0.14.6 ships class-file version 69, so it needs a Java 25 JRE.
18
+ # A 21 base dies at startup with UnsupportedClassVersionError.
19
+ # - it needs the native libsignal-client (signal_jni), shipped x86_64 only —
20
+ # as is the "Linux-native" build. Hence the amd64 pin.
21
+ # - extracting the tarball inside the amd64 stage fails under qemu on an
22
+ # arm64 host ("Cannot mkdir: Function not implemented"), so the fetch runs
23
+ # on the builder's own architecture and only the files cross over.
24
+
25
+ ARG SIGNAL_CLI_VERSION=0.14.6
26
+
27
+ FROM --platform=$BUILDPLATFORM debian:bookworm-slim AS fetch
28
+ ARG SIGNAL_CLI_VERSION
29
+ RUN apt-get update \
30
+ && apt-get install -y --no-install-recommends curl ca-certificates \
31
+ && rm -rf /var/lib/apt/lists/*
32
+ # Fetched at BUILD time — the e2e network is sealed at run time.
33
+ RUN curl -fsSL \
34
+ "https://github.com/AsamK/signal-cli/releases/download/v${SIGNAL_CLI_VERSION}/signal-cli-${SIGNAL_CLI_VERSION}.tar.gz" \
35
+ -o /tmp/signal-cli.tar.gz \
36
+ && mkdir -p /out \
37
+ && tar -xzf /tmp/signal-cli.tar.gz -C /out \
38
+ && mv "/out/signal-cli-${SIGNAL_CLI_VERSION}" /out/signal-cli
39
+ # The published JSON schemas are the authoritative description of the JSON-RPC
40
+ # surface — a test can assert against them rather than against prose.
41
+ RUN curl -fsSL \
42
+ "https://github.com/AsamK/signal-cli/releases/download/v${SIGNAL_CLI_VERSION}/signal-cli-${SIGNAL_CLI_VERSION}-json-schemas.tar.gz" \
43
+ -o /tmp/schemas.tar.gz \
44
+ && mkdir -p /out/schemas \
45
+ && tar -xzf /tmp/schemas.tar.gz -C /out/schemas
46
+
47
+ FROM --platform=linux/amd64 eclipse-temurin:25-jre
48
+ COPY --from=fetch /out/signal-cli /opt/signal-cli
49
+ COPY --from=fetch /out/schemas /opt/signal-cli-schemas
50
+
51
+ ENV PATH="/opt/signal-cli/bin:${PATH}"
52
+ ENV SIGNAL_CLI_DATA=/var/lib/signal-cli
53
+ RUN mkdir -p "${SIGNAL_CLI_DATA}"
54
+
55
+ EXPOSE 8080
56
+
57
+ # Bound to 0.0.0.0 so the Host-header pin is skipped for in-network callers
58
+ # (HttpServerHandler.isHostAllowed). The container only ever sits on the sealed
59
+ # e2e network.
60
+ CMD ["signal-cli", "--config", "/var/lib/signal-cli", "daemon", "--http", "0.0.0.0:8080"]
@@ -0,0 +1,36 @@
1
+ # The signal-cli release host — the sim internet's stand-in for the GitHub
2
+ # releases the signal module downloads at deploy time.
3
+ #
4
+ # Same reason npm-registry-sim and apt-repo-sim exist: a module that fetches an
5
+ # artifact from the internet must still be deployable inside a sealed network,
6
+ # and the honest way to test that is to SERVE the artifact rather than to skip
7
+ # the download. The module points at this host through its `release_base_url`
8
+ # config, exactly as caddy points at pebble through the ACME directory URL.
9
+ #
10
+ # The tarball is fetched at BUILD time on the builder's own architecture (the
11
+ # e2e network is sealed at run time, and extracting under qemu on an arm64 host
12
+ # fails — see Dockerfile.signal-cli for the full account).
13
+
14
+ ARG SIGNAL_CLI_VERSION=0.14.6
15
+
16
+ FROM --platform=$BUILDPLATFORM debian:bookworm-slim AS fetch
17
+ ARG SIGNAL_CLI_VERSION
18
+ RUN apt-get update \
19
+ && apt-get install -y --no-install-recommends curl ca-certificates \
20
+ && rm -rf /var/lib/apt/lists/*
21
+ # The portable JVM distribution, NOT the "-Linux-native" build: that one is a
22
+ # compiled x86_64 binary and cannot run on aarch64 at all. The JAR plus a JRE
23
+ # plus our own libsignal native works on both architectures.
24
+ #
25
+ # Served at the path BELOW the base URL, not GitHub's full path: the
26
+ # production default (`.../AsamK/signal-cli/releases/download`) already carries
27
+ # that prefix, so the module appends only `/v<version>/<file>`. Mirroring
28
+ # GitHub's full layout here would 404 for exactly that reason.
29
+ RUN mkdir -p "/out/v${SIGNAL_CLI_VERSION}" \
30
+ && curl -fsSL \
31
+ "https://github.com/AsamK/signal-cli/releases/download/v${SIGNAL_CLI_VERSION}/signal-cli-${SIGNAL_CLI_VERSION}.tar.gz" \
32
+ -o "/out/v${SIGNAL_CLI_VERSION}/signal-cli-${SIGNAL_CLI_VERSION}.tar.gz"
33
+
34
+ FROM nginx:alpine
35
+ COPY --from=fetch /out /usr/share/nginx/html
36
+ EXPOSE 80
@@ -0,0 +1,17 @@
1
+ # The signal-cli SIMULATOR — not the real daemon.
2
+ #
3
+ # Dockerfile.signal-cli runs the actual signal-cli binary and exists to keep
4
+ # our understanding of its JSON-RPC surface honest (the pebble pattern). This
5
+ # image is the fast, drivable stand-in used by tests that need to control what
6
+ # arrives: it can queue an inbound reply, fail a recipient inside a success,
7
+ # and revoke the device link on demand — none of which the real unlinked
8
+ # daemon can be made to do.
9
+ FROM oven/bun:1
10
+
11
+ WORKDIR /app
12
+ COPY simulators/signal-cli/server.ts ./server.ts
13
+
14
+ ENV SIGNAL_RPC_PORT=8080
15
+
16
+ EXPOSE 8080
17
+ CMD ["bun", "run", "server.ts"]
@@ -73,7 +73,16 @@ RUN apt-get update && apt-get install -y \
73
73
  debian-keyring \
74
74
  debian-archive-keyring \
75
75
  apt-transport-https \
76
+ wireguard-tools \
77
+ wireguard-go \
76
78
  && rm -rf /var/lib/apt/lists/*
79
+
80
+ # A provisioned box may host the `wireguard` module (the VPN does not have to
81
+ # terminate on the firewall). The module's validate_config requires wg to be
82
+ # present and fails loudly when it is not — so the stand-in for an operator's
83
+ # box carries it, exactly as a real one would. Userspace implementation named
84
+ # for the binary Ubuntu actually installs (/usr/bin/wireguard), not the package.
85
+ ENV WG_QUICK_USERSPACE_IMPLEMENTATION=wireguard
77
86
  RUN curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | \
78
87
  gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
79
88
  RUN curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | \
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celilo/e2e",
3
- "version": "0.9.3",
3
+ "version": "0.10.0",
4
4
  "description": "E2E test infrastructure for Celilo-deployed applications. Provides a simulated internet with DNS hierarchy, ACME server, firewalls, and target machines in Docker.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -37,8 +37,8 @@
37
37
  "README.md"
38
38
  ],
39
39
  "dependencies": {
40
- "@celilo/cli-display": "workspace:^",
41
- "@celilo/event-bus": "workspace:^",
40
+ "@celilo/cli-display": "^0.1.9",
41
+ "@celilo/event-bus": "^0.1.8",
42
42
  "yaml": "^2.8.0",
43
43
  "zod": "^3.24.1"
44
44
  },
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@celilo/registry-server",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "scripts": {
8
+ "start": "bun run src/index.ts",
9
+ "dev": "bun --watch src/index.ts",
10
+ "build:binary": "bun build --compile --target=bun-linux-x64 --outfile=dist/celilo-registry-server src/index.ts",
11
+ "test": "bun test"
12
+ },
13
+ "devDependencies": {
14
+ "bun-types": "latest",
15
+ "typescript": "^5"
16
+ }
17
+ }
@@ -0,0 +1,76 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { ADMIN_SCOPE, TokenAuth, hashToken } from './auth';
3
+
4
+ describe('TokenAuth (package-scoped — ISS-0140)', () => {
5
+ test('empty specs → no tokens; nothing authorizes', () => {
6
+ const auth = new TokenAuth([]);
7
+ expect(auth.hasTokens()).toBe(false);
8
+ expect(auth.authorize('anything', 'caddy')).toBe(false);
9
+ expect(auth.isAdmin('anything')).toBe(false);
10
+ });
11
+
12
+ test('admin-scoped token publishes ANY package', () => {
13
+ const auth = new TokenAuth([{ token: 'admin-tok', scope: ADMIN_SCOPE }]);
14
+ expect(auth.authorize('admin-tok', 'caddy')).toBe(true);
15
+ expect(auth.authorize('admin-tok', 'lunacycle')).toBe(true);
16
+ expect(auth.isAdmin('admin-tok')).toBe(true);
17
+ });
18
+
19
+ test('package-scoped token publishes ONLY its package', () => {
20
+ const auth = new TokenAuth([{ token: 'luna-tok', scope: 'lunacycle' }]);
21
+ expect(auth.authorize('luna-tok', 'lunacycle')).toBe(true);
22
+ expect(auth.authorize('luna-tok', 'caddy')).toBe(false);
23
+ // A scoped token is NOT an admin token — it cannot mint.
24
+ expect(auth.isAdmin('luna-tok')).toBe(false);
25
+ });
26
+
27
+ test('unknown token never authorizes', () => {
28
+ const auth = new TokenAuth([{ token: 'known', scope: 'lunacycle' }]);
29
+ expect(auth.authorize('unknown', 'lunacycle')).toBe(false);
30
+ });
31
+
32
+ test('tokens are stored hashed, not in cleartext', () => {
33
+ const auth = new TokenAuth([{ token: 'secret-token', scope: ADMIN_SCOPE }]);
34
+ expect(JSON.stringify(auth)).not.toContain('secret-token');
35
+ });
36
+
37
+ test('tolerates a Bearer prefix and surrounding whitespace', () => {
38
+ const auth = new TokenAuth([{ token: 'clean', scope: ADMIN_SCOPE }]);
39
+ expect(auth.authorize(' clean ', 'caddy')).toBe(true);
40
+ expect(auth.authorize('Bearer clean', 'caddy')).toBe(true);
41
+ });
42
+
43
+ test('empty header rejected', () => {
44
+ const auth = new TokenAuth([{ token: 'secret', scope: ADMIN_SCOPE }]);
45
+ expect(auth.authorize('', 'caddy')).toBe(false);
46
+ expect(auth.scopeOf('')).toBe(null);
47
+ });
48
+
49
+ test('addHashed / removeHashed manage minted tokens at runtime', () => {
50
+ const auth = new TokenAuth([]);
51
+ const hash = hashToken('minted-tok');
52
+ auth.addHashed(hash, 'lunacycle');
53
+ expect(auth.authorize('minted-tok', 'lunacycle')).toBe(true);
54
+ auth.removeHashed(hash);
55
+ expect(auth.authorize('minted-tok', 'lunacycle')).toBe(false);
56
+ });
57
+ });
58
+
59
+ describe('TokenAuth.fromEnv (PUBLISH_TOKENS format)', () => {
60
+ test('bare line → admin scope; "token pkg" line → scoped', () => {
61
+ process.env.PUBLISH_TOKENS = 'admin-tok\nluna-tok lunacycle\n';
62
+ const auth = TokenAuth.fromEnv();
63
+ expect(auth.isAdmin('admin-tok')).toBe(true);
64
+ expect(auth.authorize('luna-tok', 'lunacycle')).toBe(true);
65
+ expect(auth.authorize('luna-tok', 'caddy')).toBe(false);
66
+ process.env.PUBLISH_TOKENS = '';
67
+ });
68
+
69
+ test('blank lines ignored', () => {
70
+ process.env.PUBLISH_TOKENS = '\n \nt1\n';
71
+ const auth = TokenAuth.fromEnv();
72
+ expect(auth.hasTokens()).toBe(true);
73
+ expect(auth.isAdmin('t1')).toBe(true);
74
+ process.env.PUBLISH_TOKENS = '';
75
+ });
76
+ });