empeira 0.1.0.pre.alpha.4 → 0.1.0.pre.alpha.7

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/config/defaults.yaml +11 -4
  3. data/docs/addons.md +10 -6
  4. data/docs/architecture.md +18 -0
  5. data/docs/configuration.md +50 -23
  6. data/docs/hiera-and-eyaml.md +2 -2
  7. data/docs/networking.md +16 -7
  8. data/docs/nodes.md +9 -3
  9. data/docs/proxy.md +114 -19
  10. data/docs/troubleshooting.md +2 -2
  11. data/lib/empeira/build.json +3 -3
  12. data/lib/empeira/cli/proxy.rb +2 -2
  13. data/lib/empeira/configuration/schema.rb +28 -2
  14. data/lib/empeira/control_plane/controller.rb +16 -4
  15. data/lib/empeira/control_plane/files.rb +1 -8
  16. data/lib/empeira/control_plane/health.rb +11 -0
  17. data/lib/empeira/control_plane/plan.rb +17 -15
  18. data/lib/empeira/control_plane/startup_failure.rb +43 -0
  19. data/lib/empeira/infrastructure/cleanup.rb +6 -0
  20. data/lib/empeira/infrastructure/session_lock.rb +60 -0
  21. data/lib/empeira/network/direct_egress.rb +0 -7
  22. data/lib/empeira/network/gateway.rb +9 -1
  23. data/lib/empeira/network/host_policy.rb +1 -1
  24. data/lib/empeira/network/peer/layout.rb +8 -0
  25. data/lib/empeira/network/proxy_bindings.rb +27 -10
  26. data/lib/empeira/network/proxy_policy.rb +1 -1
  27. data/lib/empeira/node/container_lifecycle.rb +41 -23
  28. data/lib/empeira/node/runtime_proxy.rb +2 -2
  29. data/lib/empeira/node/session_lock.rb +19 -0
  30. data/lib/empeira/runtime/service_arguments.rb +7 -1
  31. data/lib/empeira/runtime/service_inspection.rb +5 -1
  32. data/lib/empeira/vm/session_lock.rb +2 -46
  33. data/lib/empeira.rb +3 -0
  34. data/resources/gateway/configure.rb +21 -11
  35. data/resources/nodes/management/guest_agent.py +4 -0
  36. metadata +4 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4e4e7094658b9e3d8f4ee0edbafa3e4e45be28921900cce8eb4af7d66096cee6
4
- data.tar.gz: 7f062dc35a9cef14ad52f55362beba6c4a5a0ab1b57225ce27a191123dc01fc9
3
+ metadata.gz: 3f0b0c2558d0799e27a8985ab84de156e8de49741ec078965338d0c1638ab0d3
4
+ data.tar.gz: 5607b7c52a5fb130b7a8534318aecb89c0093709ea7d9b65fcccef86da745fab
5
5
  SHA512:
6
- metadata.gz: 8410139aeffa1ccaab90f3ddac57a809a86420e385f1354f801f8181c936ecf5653762cc4dbade0178d6df99b2dad1cc8b68cb8d537013132ec09e2ed65aefa1
7
- data.tar.gz: 52ba2b37546bc90263d9834193b5080440e2a66425c9f9403b19800e791bf5f26b895d4c4e1af4bc218951b3a0e433437c07c550f493f4577e4e8dfd156a06bb
6
+ metadata.gz: 5a0eaa2c61a33c12d49632cfa4690f9e369bbae26a1d4cf4f9b1c5ae0d3178d6aed0f14f28721d1a83e1332319b02cf5a899f8f31287b88816b2e2e7ba3e8366
7
+ data.tar.gz: ed8ab5306f5948ed1f81c8e1b00121cb6360490e35daf915b95c441a42174552320b08fcc37a193eb2e1011b694eb1f85932739df3ee0d2274b182fded0b6957
data/config/defaults.yaml CHANGED
@@ -50,12 +50,19 @@ puppetdb:
50
50
  bootstrap_config: /etc/puppetlabs/puppetdb/bootstrap.cfg
51
51
  data: /opt/puppetlabs/server/data/puppetdb
52
52
  network:
53
- egress: []
53
+ egress:
54
+ nodes: []
55
+ infrastructure: []
54
56
  redirects: []
55
57
  proxy:
56
- enabled: false
57
- global: []
58
- rules: []
58
+ memory: 512
59
+ nodes:
60
+ enabled: false
61
+ global: []
62
+ rules: []
63
+ infrastructure:
64
+ enabled: false
65
+ allow: []
59
66
  vm:
60
67
  disk: 30
61
68
  interfaces: []
data/docs/addons.md CHANGED
@@ -208,8 +208,12 @@ Array elements can be mappings, arrays or scalars. For example:
208
208
 
209
209
  ```yaml
210
210
  proxy:
211
- enabled: true
212
- global: [forge.puppet.com, github.com]
211
+ nodes:
212
+ enabled: true
213
+ global: [forge.puppet.com, github.com]
214
+ infrastructure:
215
+ enabled: false
216
+ allow: []
213
217
  server:
214
218
  mounts:
215
219
  - source: /home/user/data
@@ -217,10 +221,10 @@ server:
217
221
  readonly: true
218
222
  ```
219
223
 
220
- produces `EMPEIRA_CONFIG_PROXY_ENABLED=true`,
221
- `EMPEIRA_CONFIG_PROXY_GLOBAL_COUNT=2`,
222
- `EMPEIRA_CONFIG_PROXY_GLOBAL_0=forge.puppet.com`,
223
- `EMPEIRA_CONFIG_PROXY_GLOBAL_1=github.com`,
224
+ produces `EMPEIRA_CONFIG_PROXY_NODES_ENABLED=true`,
225
+ `EMPEIRA_CONFIG_PROXY_NODES_GLOBAL_COUNT=2`,
226
+ `EMPEIRA_CONFIG_PROXY_NODES_GLOBAL_0=forge.puppet.com`,
227
+ `EMPEIRA_CONFIG_PROXY_NODES_GLOBAL_1=github.com`,
224
228
  `EMPEIRA_CONFIG_SERVER_MOUNTS_COUNT=1`, and the variables
225
229
  `EMPEIRA_CONFIG_SERVER_MOUNTS_0_SOURCE`, `_TARGET`, `_READONLY`.
226
230
 
data/docs/architecture.md CHANGED
@@ -93,6 +93,24 @@ system SSH. Guest commands use argument arrays and owned process groups; stdout
93
93
  and stderr stream in bounded frames without truncation. Timeouts, interrupts and
94
94
  host disconnects terminate those groups. Binary uploads are SHA-256 verified,
95
95
  staged privately and atomically installed in the real guest filesystem.
96
+ The management supervisor and workers retain `umask 077`, private 0700 staging and
97
+ explicit upload modes. Only executed commands (including Puppet and its children)
98
+ and PTY shells start with `umask 022`. Previously the worker's private mask leaked
99
+ into these children, causing ordinary file/directory creation to yield 0600/0700;
100
+ APT could not read public keys created by commands that omitted an explicit mode.
101
+ Commands still run as numeric root, and explicit private modes remain private.
102
+ Empeira's native APT acquisition already copies its own signing key with mode 0644
103
+ under `/var/tmp`; it does not create project repository keys under
104
+ `/usr/share/keyrings` or `/etc/apt/keyrings`.
105
+
106
+ This correction applies to newly seeded VMs. Recreate disposable existing VMs to
107
+ receive the updated adapter; `up` does not replace seeded management code or repair
108
+ existing files. No directory-wide chmod or post-Puppet keyring rewrite is performed.
109
+ A catalog explicitly setting mode 0600, or a tool using private temporary files,
110
+ still needs correction at its owning resource. Project-managed public APT keys
111
+ should specify root:root and 0644, with searchable parent directories. The reported
112
+ Docker/Elastic/project key names are absent from Empeira's source; their exact
113
+ catalog resources require inspection in the affected project.
96
114
 
97
115
  The QEMU monitor verifies workspace/node/instance identity before management.
98
116
  A 0700 host directory and 0600 Unix socket restrict access; recorded device/inode
@@ -107,9 +107,15 @@ runtime:
107
107
  server:
108
108
  environment: production
109
109
  network:
110
- egress: []
110
+ egress:
111
+ nodes: []
112
+ infrastructure: []
111
113
  proxy:
112
- enabled: false
114
+ nodes:
115
+ enabled: false
116
+ infrastructure:
117
+ enabled: false
118
+ allow: []
113
119
  node_defaults:
114
120
  os: ubuntu
115
121
  version: "24.04"
@@ -150,11 +156,15 @@ release version. A partial project mapping may omit it and inherit the default.
150
156
  | `server.timeout` | Positive readiness timeout in seconds per component, `240` |
151
157
  | `server.mounts` | Additional server-only host bind mounts, `[]`; `source`, `target`, optional boolean `readonly` (default `true`) |
152
158
  | `puppetdb.memory` | Positive integer MiB, `768` |
153
- | `network.egress` | Direct TCP proxy-bypass host/port entries, `[]`; a project array replaces the default array |
159
+ | `network.egress.nodes` | Node-only direct TCP host/port grants, `[]`; arrays replace |
160
+ | `network.egress.infrastructure` | Infrastructure-only direct TCP host/port grants, `[]`; arrays replace |
154
161
  | `network.redirects` | Exact IPv4/protocol/port endpoints redirected to enabled internal services, `[]` |
155
- | `proxy.enabled` | Boolean, `false`; creates the normal HTTP/HTTPS policy proxy |
156
- | `proxy.global` | DNS destination array, `[]` |
157
- | `proxy.rules` | Additive hostname-glob rules with `hosts` and `allow` arrays, `[]` |
162
+ | `proxy.memory` | Positive integer MiB, `512` per Squid container; shared regular proxy and separate temporary bootstrap proxy each use this limit; does not enable either proxy |
163
+ | `proxy.nodes.enabled` | Boolean, `false`; enables normal node HTTP/HTTPS proxy access |
164
+ | `proxy.nodes.global` | DNS destination array, `[]` |
165
+ | `proxy.nodes.rules` | Additive node hostname-glob rules with `hosts` and `allow` arrays, `[]` |
166
+ | `proxy.infrastructure.enabled` | Boolean, `false`; enables infrastructure HTTP/HTTPS proxy access |
167
+ | `proxy.infrastructure.allow` | Independent infrastructure DNS destination array, `[]` |
158
168
  | `vm.disk` | New VM virtual disk capacity in GiB, integer `1`–`2048`, default `30`; must accommodate the base image |
159
169
  | `vm.interfaces` | VM-only hostname rules defining dummy/VLAN devices with explicit IPv4 CIDR host addresses, `[]` |
160
170
  | `vm.console.root_password` | Initial console password, `empeira`; a string overrides it and `null` disables it |
@@ -488,14 +498,20 @@ The numeric gem packaging fallback is never used to bypass this check.
488
498
  ```yaml
489
499
  network:
490
500
  egress:
491
- - host: api.example.com
492
- ports: [443]
501
+ nodes:
502
+ - host: api.example.com
503
+ ports: [443]
504
+ infrastructure: []
493
505
  proxy:
494
- enabled: true
495
- global: [forgeapi.puppet.com]
496
- rules:
497
- - hosts: ["*-web-*"]
498
- allow: [github.com, "*.github.com"]
506
+ nodes:
507
+ enabled: true
508
+ global: [forgeapi.puppet.com]
509
+ rules:
510
+ - hosts: ["*-web-*"]
511
+ allow: [github.com, "*.github.com"]
512
+ infrastructure:
513
+ enabled: false
514
+ allow: []
499
515
  hiera:
500
516
  mounts:
501
517
  - source: ../hieradata
@@ -507,7 +523,12 @@ vm:
507
523
  root_password: null
508
524
  ```
509
525
 
510
- When `proxy.enabled` is true, every node may use its matching destination policy.
526
+ When `proxy.nodes.enabled` is true, every node may use its matching destination policy.
527
+ Infrastructure access is separately controlled by `proxy.infrastructure.enabled`
528
+ and `proxy.infrastructure.allow`; both scopes default to disabled. Disabled
529
+ infrastructure has no configured proxy endpoint even when nodes use the proxy.
530
+ Keep its direct-egress list empty for internal-only infrastructure. See the
531
+ [migration instructions](proxy.md#migrating-shared-settings) for former shared keys.
511
532
  After bootstrap cleanup, Empeira reconciles normal proxy settings on running,
512
533
  provisioned nodes during `up` and `node start`. Repeated Puppet runs use the
513
534
  existing guest configuration. No
@@ -719,21 +740,24 @@ desired state; Puppet owns package state after the first catalog.
719
740
 
720
741
  ## Direct egress
721
742
 
722
- `network.egress` contains destination TCP grants, independent of HTTP/HTTPS proxy
723
- policy. Entries have exactly one `host` or IPv4 `ip` and a nonempty unique `ports`
743
+ `network.egress.nodes` and `network.egress.infrastructure` contain separate
744
+ destination TCP grants, independent of HTTP/HTTPS proxy policy. A node grant never
745
+ applies to infrastructure or conversely. Entries have exactly one `host` or IPv4 `ip` and a nonempty unique `ports`
724
746
  array (1–65535). `up` resolves all A records and replaces the complete policy;
725
747
  resolution/apply failure blocks egress. IPv6 and external UDP grants are unsupported.
726
748
 
727
749
  ```yaml
728
750
  network:
729
751
  egress:
730
- - host: repository.example.net
731
- ports: [443]
732
- - ip: 203.0.113.10
733
- ports: [443, 8443]
752
+ nodes:
753
+ - host: repository.example.net
754
+ ports: [443]
755
+ - ip: 203.0.113.10
756
+ ports: [443, 8443]
757
+ infrastructure: []
734
758
  ```
735
759
 
736
- Named direct destinations enter generated `NO_PROXY`; applications choose DIRECT
760
+ Direct destinations enter only their own scope's generated `NO_PROXY`; applications choose DIRECT
737
761
  or proxy themselves. See [networking](networking.md#one-gateway-one-direct-egress-policy)
738
762
  for routing, revocation and privilege boundaries.
739
763
 
@@ -765,7 +789,7 @@ vm:
765
789
  network: 192.0.2.20/32
766
790
  ```
767
791
 
768
- Patterns use the same `*`/`?` glob matcher as `proxy.rules`, against the full stored
792
+ Patterns use the same `*`/`?` glob matcher as `proxy.nodes.rules`, against the full stored
769
793
  node hostname, case-insensitively. Patterns within a rule are alternatives. All
770
794
  matching rules contribute devices; identical definitions merge once. A conflicting
771
795
  definition names the hostname, device and both configuration paths. No match creates
@@ -947,7 +971,10 @@ The default control plane has six containers, or seven with the proxy, plus the
947
971
  runtime's peer-network adapter. Requested limits
948
972
  are 1536 MiB for the server (768 MiB JVM heap), 768 MiB for PuppetDB (256 MiB heap),
949
973
  256 MiB for PostgreSQL, 64 MiB for DNS, 96 MiB for the database relay, 64 MiB
950
- for the gateway and 128 MiB for Squid. The control-plane memory ceiling is about
974
+ for the gateway and 512 MiB for the regular Squid proxy (`proxy.memory`). A temporary
975
+ bootstrap Squid container has its own additional limit of the same size while
976
+ running. These are per-container limits, not reservations or measured peaks.
977
+ The control-plane memory ceiling is about
951
978
  2.7 GiB without the proxy, excluding the peer adapter and nodes. The server gets two CPUs and each
952
979
  other service one. Actual idle use is lower and varies by provider and platform.
953
980
  `up` automatically pulls missing remote images and builds missing recipe images.
@@ -207,8 +207,8 @@ The obsolete `images.modules` key is rejected.
207
207
  The disposable installer belongs to the update plane. It uses the selected
208
208
  runtime's normal bridge network (`bridge` on Docker, `podman` on Podman), with
209
209
  normal runtime DNS and external connectivity. It never joins an Empeira workspace
210
- network and needs neither CoreDNS nor Squid. Workspace `network.egress`, `proxy.enabled`,
211
- `proxy.global` and `proxy.rules` do not affect
210
+ network and needs neither CoreDNS nor Squid. Workspace `network.egress`, `proxy.nodes.enabled`,
211
+ `proxy.nodes.global` and `proxy.nodes.rules` do not affect
212
212
  Forge or Git acquisition. Supplied `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` and their
213
213
  lowercase equivalents are inherited unchanged; Empeira generates no proxy URL.
214
214
  The host/runtime's normal network and corporate-proxy configuration must permit
data/docs/networking.md CHANGED
@@ -102,17 +102,26 @@ Every peer uses the owned gateway as its IPv4 default route. Only the gateway ha
102
102
  an external uplink; DNS, proxies, server, database and nodes stay isolated.
103
103
  The fixed browser UI relay has a separate loopback access path with forwarding off.
104
104
 
105
- `network.egress` grants destination IPv4/TCP-port pairs:
105
+ `network.egress.nodes` and `network.egress.infrastructure` grant destination
106
+ IPv4/TCP-port pairs independently:
106
107
 
107
108
  ```yaml
108
109
  network:
109
110
  egress:
110
- - host: repository.example.net
111
- ports: [443]
112
- - ip: 203.0.113.10
113
- ports: [443, 8443]
111
+ nodes:
112
+ - host: repository.example.net
113
+ ports: [443]
114
+ - ip: 203.0.113.10
115
+ ports: [443, 8443]
116
+ infrastructure: []
114
117
  ```
115
118
 
119
+ The gateway restricts node grants to the reserved VM/container node address ranges
120
+ and infrastructure grants to its separate runtime allocation pool. Both directions
121
+ of direct connections are source-bound. The shared workspace subnet is never used
122
+ as a blanket source for direct grants. Custom gateway images must advertise
123
+ `scoped-egress-v1`; an older image fails closed before receiving external access.
124
+
116
125
  Entries have exactly one `host` or `ip`. Hostnames match exactly, without wildcards;
117
126
  `up` resolves every current A record and replaces the complete policy. IP selectors
118
127
  bypass DNS and may intentionally permit private LAN/VPN destinations. A different
@@ -128,9 +137,9 @@ CoreDNS alone receives TCP/UDP 53 to its selected resolvers. Proxies alone recei
128
137
  TCP 80/443 and enforce their destination allowlists. Normal proxy allow rules apply
129
138
  regardless of target IP range; the temporary authenticated bootstrap proxy retains
130
139
  its separate private/reserved-address restrictions.
131
- `proxy.global` does not grant direct egress. Applications choose DIRECT/proxy using
140
+ `proxy.nodes.global` does not grant direct egress. Applications choose DIRECT/proxy using
132
141
  their own settings and `NO_PROXY`; direct/proxy selection has no automatic fallback.
133
- Both explicit hostnames and IPv4 selectors from `network.egress` are included in
142
+ Both explicit hostnames and IPv4 selectors are included only in their own scope's
134
143
  normal proxy bypass settings. The gateway still permits only their configured TCP ports.
135
144
  See [proxy policy](proxy.md) for hostname rules and bootstrap destinations.
136
145
 
data/docs/nodes.md CHANGED
@@ -296,12 +296,18 @@ Empeira uses the private privileged VirtIO channel internally to install the VM
296
296
  enroll certificates and run Puppet. User-facing SSH opens an ordinary interactive
297
297
  login without the internal provisioning command or automatic sudo.
298
298
 
299
- VM exec, shell, SSH and serial sessions release the workspace mutation lock after ownership and
300
- running-state checks. Parallel sessions and read-only status remain available. A
299
+ Container SSH and VM exec, shell, SSH and serial sessions release the workspace
300
+ mutation lock after ownership and running-state checks. Parallel sessions and read-only status remain available. A
301
301
  shared instance guard prevents `node stop`, `node destroy` or restart from replacing
302
- the VM while a session is attached; detach first and retry the conflicting operation.
302
+ that node instance while a session is attached; detach first and retry the conflicting operation.
303
303
  Other nodes and workspace commands can proceed. Instance lock files stay stable;
304
304
  their open descriptors are released on detach, disconnect, errors and signals.
305
+ Container SSH preparation remains inside the workspace transaction and pins the
306
+ observed container ID before releasing that lock. Concurrent SSH sessions to the
307
+ same or different nodes are allowed. Container `exec` and `shell` retain their
308
+ existing workspace transaction. Mutations still use the exclusive workspace lock;
309
+ `down` continues to require node removal, and destroy retains attached instances
310
+ for a retry after sessions detach.
305
311
 
306
312
  ## Root diagnostic commands and interactive shell
307
313
 
data/docs/proxy.md CHANGED
@@ -1,19 +1,24 @@
1
1
  # Proxy policy
2
2
 
3
- Normal external HTTP/HTTPS access stays behind Squid. Enabling the workspace proxy
4
- makes it available to every node; global and hostname rules remain the destination
5
- allowlist:
3
+ Normal external HTTP/HTTPS access stays behind Squid. Nodes and infrastructure
4
+ have independent switches and destination lists, both disabled by default. Enabling
5
+ the node scope makes its global and hostname rules available to nodes:
6
6
 
7
7
  ```yaml
8
8
  proxy:
9
- enabled: true
10
- global:
11
- - forgeapi.puppet.com
12
- rules:
13
- - hosts: ["lab-*"]
14
- allow: ["*.github.com"]
15
- - hosts: ["*-web-*"]
16
- allow: [github.com, nginx.org]
9
+ memory: 512 # MiB per regular or temporary bootstrap Squid container
10
+ nodes:
11
+ enabled: true
12
+ global:
13
+ - forgeapi.puppet.com
14
+ rules:
15
+ - hosts: ["lab-*"]
16
+ allow: ["*.github.com"]
17
+ - hosts: ["*-web-*"]
18
+ allow: [github.com, nginx.org]
19
+ infrastructure:
20
+ enabled: false
21
+ allow: []
17
22
  ```
18
23
 
19
24
  ```console
@@ -44,9 +49,60 @@ The diagnostic resolves desired policy even for a hostname that does not exist.
44
49
  It reports when the normal proxy is disabled and does not claim that live
45
50
  infrastructure has already been reconciled.
46
51
 
52
+ ## Memory budget and startup diagnostics
53
+
54
+ `proxy.memory` is a positive integer MiB limit per managed Squid container,
55
+ defaulting to 512. Node access and infrastructure access share one regular proxy;
56
+ the same limit applies when either or both scopes are enabled. The temporary,
57
+ authenticated bootstrap proxy inherits this value as its own container limit.
58
+ If both containers run simultaneously, each has a separate 512 MiB limit by
59
+ default (1024 MiB combined), not one shared 512 MiB budget. These limits are
60
+ neither memory reservations nor measured peak consumption.
61
+
62
+ Existing configurations inherit the default. Setting `proxy.memory` alone does
63
+ not enable a regular proxy or start a bootstrap proxy. Node and infrastructure
64
+ access keep independent activation switches and destination grants. Bootstrap
65
+ keeps its separate temporary activation, authentication and destination grants.
66
+ Docker and Podman use the same native
67
+ `--memory` argument, including rootless Podman. The engine host (or its Linux VM on
68
+ macOS) still needs enough available memory and working cgroup memory controls.
69
+
70
+ The former fixed 128 MiB limit was below the reported killed process's approximately
71
+ 139 MiB resident footprint (anonymous plus file RSS), before accounting for other
72
+ cgroup charges. The 512 MiB default provides headroom for startup, concurrent
73
+ requests, DNS and allocator overhead. It is a conservative budget, not a measured
74
+ maximum or a guarantee for arbitrary workloads. Validate actual peak usage with
75
+ native container stats and host cgroup/OOM diagnostics in the affected environment.
76
+
77
+ Generated policy keeps `cache deny all`, sets `cache_mem 16 MB`, and explicitly uses
78
+ `workers 1` together with `-N` for one foreground main process. An explicit
79
+ `workers 0` causes Squid 7.7 to exit during startup with
80
+ `FATAL: check failed: theProcessCount > 0`; `-N` already selects foreground mode.
81
+ No disk cache is configured. Squid's [cache_mem documentation](https://www.squid-cache.org/Doc/config/cache_mem/)
82
+ describes an object-memory target, not a cap on process RSS; in-flight requests can
83
+ exceed it. The [workers setting](https://www.squid-cache.org/Doc/config/workers/)
84
+ avoids multiplying main-process memory. These settings preserve DNS, source-bound
85
+ client rules and the HTTP/HTTPS destination policy.
86
+
87
+ If the regular proxy exits during startup or its readiness check, diagnostics
88
+ include the native exit code, `OOMKilled` observation and actual inspected memory
89
+ limit in bytes/MiB when available. Only `OOMKilled=true` confirms an OOM kill. Exit 137 alone does not:
90
+ external SIGKILL can produce the same exit. Missing observations are explicitly
91
+ reported as unavailable, and a runtime-reported zero limit is shown as unlimited.
92
+ For confirmed proxy OOM, increase `proxy.memory`, check engine-host capacity, and
93
+ run `empeira up`. Native logs remain available via the displayed runtime command.
94
+ The temporary bootstrap proxy uses its own endpoint verification and readiness
95
+ probe; the new regular-proxy OOM diagnostics do not apply to that separate path.
96
+
97
+ A memory change replaces only the regular proxy on the next `up`; CA/database
98
+ storage, nodes and unrelated services retain their identities. Unchanged runs
99
+ reuse the proxy. Generated policy changes use the existing readiness/reload checkpoint.
100
+ Replacing or reloading the proxy may interrupt active proxy connections.
101
+ The next temporary bootstrap proxy receives the configured limit when created.
102
+
47
103
  ## Matching and destinations
48
104
 
49
- Defaults are `proxy.global: []` and `proxy.rules: []`. Every node receives
105
+ Defaults are `proxy.nodes.global: []` and `proxy.nodes.rules: []`. Every node receives
50
106
  all global destinations plus `allow` from **every** matching rule. Duplicate entries
51
107
  are removed while preserving configuration order. A rule matches when any of its
52
108
  `hosts` patterns matches the complete lowercase hostname. Patterns must be lowercase.
@@ -66,14 +122,14 @@ Destination rules are separate from hostname globs:
66
122
  Only HTTP port 80 and HTTPS CONNECT port 443 are allowed. Allowed DNS destinations
67
123
  are reachable regardless of their target IP range, including private, loopback,
68
124
  link-local and reserved addresses. Domains without a matching allow rule remain
69
- denied. There is no TLS interception. Server-side outbound traffic gets
70
- `proxy.global` only; node hostname rules do not expand server access.
125
+ denied. There is no TLS interception. Node destinations never grant infrastructure
126
+ access, and infrastructure destinations never grant node access.
71
127
 
72
128
  ## Node policy and reconciliation
73
129
 
74
- When `proxy.enabled` is true, all nodes are authorized as proxy clients. Nodes get
130
+ When `proxy.nodes.enabled` is true, all nodes are authorized as proxy clients. Nodes get
75
131
  upper/lowercase proxy variables and internal `NO_PROXY` entries, including explicitly
76
- configured `network.egress` hosts. Those named hosts use the common workspace
132
+ configured `network.egress.nodes` hosts. Those named hosts use the common workspace
77
133
  gateway for their configured ports. VM access uses the same destination policy bound
78
134
  directly to the VM instance's reserved peer address. The proxy is reached at
79
135
  `proxy.empeira.internal:3128` on the isolated network. A request without a matching
@@ -90,17 +146,56 @@ Container and VM nodes consume the new policy without a restart; open proxy
90
146
  connections can end during reconciliation. Enabling/disabling proxy mode is a
91
147
  separate topology change and may require explicit node cleanup as reported by `up`.
92
148
 
93
- Changing direct-egress destinations also changes the server's `NO_PROXY`
94
- environment. `up` replaces a server whose environment definition changes while
149
+ Changing `network.egress.infrastructure` destinations also changes infrastructure
150
+ `NO_PROXY`. Changing node destinations does not change the server environment. `up` replaces a server whose environment definition changes while
95
151
  retaining CA storage and node identities. Changing only destination ports or
96
152
  transparent redirect rules preserves the server when its definition is unchanged.
97
153
 
154
+ ## Infrastructure policy
155
+
156
+ `proxy.infrastructure.enabled` controls the Puppet/OpenVox server, PostgreSQL,
157
+ PuppetDB backend/relay and `containers.additional` services. Its independent
158
+ `allow` list uses the same exact-domain and domain-family syntax as node grants.
159
+ The browser, DNS, Squid and gateway transports do not receive these application
160
+ proxy settings.
161
+
162
+ ```yaml
163
+ proxy:
164
+ infrastructure:
165
+ enabled: true
166
+ allow: [api.example.net]
167
+ ```
168
+
169
+ When disabled, infrastructure definitions contain empty proxy values, overriding
170
+ any image defaults. They contain no configured proxy endpoint and receive no Squid
171
+ client grants, even if the node proxy is enabled. No startup wrapper removes an
172
+ otherwise configured proxy. Internal names and mocks remain directly reachable.
173
+ When enabled, only infrastructure's own direct destinations enter its `NO_PROXY`.
174
+ Explicit environment settings in additional-service definitions remain application
175
+ configuration; they do not add Squid permissions.
176
+
177
+ `up` creates the shared normal proxy when either scope is enabled and removes it
178
+ when both are disabled. Changing infrastructure's switch updates affected service
179
+ definitions while retaining storage and node identities. Allowlist-only edits reload
180
+ Squid without replacing unrelated services. Direct grants remain independent of
181
+ both proxy switches; keep `network.egress.infrastructure: []` for internal-only
182
+ infrastructure.
183
+
184
+ ## Migrating shared settings
185
+
186
+ The former `proxy.enabled`, `proxy.global` and `proxy.rules` keys are rejected with
187
+ a migration diagnostic. Move them under `proxy.nodes`; configure infrastructure
188
+ explicitly with its own switch and `allow` list. Old shared `network.egress` arrays
189
+ and legacy `mode` values are also rejected. Move grants to `network.egress.nodes`
190
+ or `network.egress.infrastructure` according to the intended clients. No existing
191
+ grant is silently copied to infrastructure.
192
+
98
193
  ## Managed bootstrap
99
194
 
100
195
  Managed bootstrap destinations are separate from normal user policy. Reviewed
101
196
  agent and distribution endpoints in `agent.install` and `bootstrap.guests` allow
102
197
  Puppet/OpenVox and required guest packages to be installed without duplicating them
103
- in `proxy.global`. The temporary authenticated bootstrap proxy uses workspace DNS
198
+ in `proxy.nodes.global`. The temporary authenticated bootstrap proxy uses workspace DNS
104
199
  and retains its private, loopback, link-local and reserved destination-address
105
200
  restrictions independently of normal proxy policy. Its credential is not stored in
106
201
  the guest seed, JSON inventory or logs. A /32 source ACL admits only the provisioning
@@ -132,7 +132,7 @@ or restart of that same VM conflicts with the shared instance guard: detach with
132
132
  ## Proxy destination denied
133
133
 
134
134
  Run `empeira proxy show HOSTNAME` and check every matching rule. Verify the node
135
- exists in the workspace, `proxy.enabled` is true, and `up` has reconciled the
135
+ exists in the workspace, `proxy.nodes.enabled` is true, and `up` has reconciled the
136
136
  configuration and current node addresses. Allowed DNS destinations may resolve to
137
137
  private or reserved IP addresses; only HTTP port 80 and HTTPS CONNECT port 443 are
138
138
  permitted. After updating Empeira, run `empeira up` to replace a proxy still using
@@ -367,6 +367,6 @@ external runtime network as a workaround. Docker workspace bridge attachment
367
367
  errors require an accessible Linux-engine host namespace and an identifiable
368
368
  iptables backend. The helper changes only owned bridge rules. Do not disable the
369
369
  host firewall globally. Check Docker Desktop host-network restrictions separately.
370
- Proxy errors require checking Squid source bindings and `proxy.global`/`proxy.rules`,
370
+ Proxy errors require checking Squid source bindings and `proxy.nodes.global`/`proxy.nodes.rules`,
371
371
  not adding unrelated direct grants. Corporate DNS still follows host discovery;
372
372
  IPv6-only upstreams must be replaced with reachable IPv4 resolvers for this backend.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.0-alpha.4",
3
- "revision": "b073c6a15db14c4dace4a732703240f4289cbb48",
4
- "build_time": "2026-10-21T11:41:02Z"
2
+ "version": "0.1.0-alpha.7",
3
+ "revision": "16b913976904b1abadca0ba55d571039e762b997",
4
+ "build_time": "2026-11-13T14:35:59Z"
5
5
  }
@@ -5,7 +5,7 @@ module Empeira
5
5
  class Proxy < Base
6
6
  desc 'show HOSTNAME', 'Explain the normal proxy destination policy for a hostname'
7
7
  def show(hostname)
8
- config = application.context.configuration.fetch('proxy')
8
+ config = application.context.configuration.dig('proxy', 'nodes')
9
9
  resolver = Network::HostPolicy.new(config)
10
10
  say "Effective proxy policy for #{hostname.downcase}"
11
11
  say access_message(config)
@@ -19,7 +19,7 @@ module Empeira
19
19
  return "All workspace nodes may use this policy after 'empeira up' reconciles the proxy." if
20
20
  config.fetch('enabled')
21
21
 
22
- 'The normal workspace proxy is disabled.'
22
+ 'The normal node proxy is disabled.'
23
23
  end
24
24
 
25
25
  def display_matches(rules)
@@ -20,8 +20,11 @@ module Empeira
20
20
  'environment' => :environment,
21
21
  'memory' => :positive_integer, 'cpus' => :positive_integer, 'timeout' => :positive_integer },
22
22
  'puppetdb' => { 'enabled' => :boolean, 'memory' => :positive_integer, 'runtime' => :puppetdb_runtime },
23
- 'network' => { 'egress' => :network_egress, 'redirects' => :network_redirects },
24
- 'proxy' => { 'enabled' => :boolean, 'global' => :allowlist, 'rules' => :proxy_rules },
23
+ 'network' => { 'egress' => { 'nodes' => :network_egress, 'infrastructure' => :network_egress },
24
+ 'redirects' => :network_redirects },
25
+ 'proxy' => { 'memory' => :positive_integer,
26
+ 'nodes' => { 'enabled' => :boolean, 'global' => :allowlist, 'rules' => :proxy_rules },
27
+ 'infrastructure' => { 'enabled' => :boolean, 'allow' => :allowlist } },
25
28
  'vm' => { 'disk' => :vm_disk, 'interfaces' => :vm_interfaces,
26
29
  'console' => { 'root_password' => :optional_string } },
27
30
  'dns' => { 'upstream' => { 'mode' => :dns_mode, 'servers' => :dns_servers },
@@ -116,12 +119,35 @@ module Empeira
116
119
  end
117
120
 
118
121
  def validate_fragment!(data)
122
+ validate_scope_migration!(data)
119
123
  validate_mapping!(data, RULES, [])
120
124
  data
121
125
  end
122
126
 
123
127
  private
124
128
 
129
+ def validate_scope_migration!(data)
130
+ return unless data.is_a?(Hash)
131
+
132
+ validate_proxy_scope_migration!(data['proxy'])
133
+ network = data['network']
134
+ validate_egress_scope_migration!(network['egress']) if network.is_a?(Hash)
135
+ end
136
+
137
+ def validate_proxy_scope_migration!(proxy)
138
+ return unless proxy.is_a?(Hash) && proxy.keys.intersect?(%w[enabled global rules])
139
+
140
+ raise ConfigurationError, 'Shared proxy settings have moved to proxy.nodes; configure ' \
141
+ 'proxy.infrastructure separately'
142
+ end
143
+
144
+ def validate_egress_scope_migration!(egress)
145
+ return unless egress.is_a?(Array) || (egress.is_a?(Hash) && egress.key?('mode'))
146
+
147
+ raise ConfigurationError, 'Shared network.egress grants must be moved explicitly to ' \
148
+ 'network.egress.nodes or network.egress.infrastructure'
149
+ end
150
+
125
151
  def validate_operational!(data)
126
152
  validate_images!(data.fetch('images'))
127
153
  NodeImages.effective!(data.fetch('images'))
@@ -254,8 +254,10 @@ module Empeira
254
254
  end
255
255
 
256
256
  def resolve_direct_egress
257
- config = @plan.config.dig('network', 'egress')
258
- @direct_resolver.resolve(config)
257
+ entries = @plan.config.dig('network', 'egress').flat_map do |scope, grants|
258
+ @direct_resolver.resolve(grants).entries.map { |entry| entry.merge('scope' => scope) }
259
+ end
260
+ Network::DirectEgress::Resolved.new(entries)
259
261
  end
260
262
 
261
263
  def bootstrap_dns?(upstreams:, routes:)
@@ -526,8 +528,7 @@ module Empeira
526
528
  def require_started!(key, resource)
527
529
  return if resource.fetch('state') == 'running'
528
530
 
529
- raise Error, "Managed service #{key} is #{resource.fetch('state')} after start; " \
530
- "inspect #{@context.container_engine} logs #{resource.fetch('id')}"
531
+ raise StartupFailure.new(key: key, resource: resource, engine: @context.container_engine)
531
532
  end
532
533
 
533
534
  def update_hosts(reload_proxy: false)
@@ -636,6 +637,7 @@ module Empeira
636
637
  end
637
638
  reconcile('gateway')
638
639
  lockdown_gateway(@observed.fetch('gateway'))
640
+ require_scoped_egress_capability
639
641
  require_redirect_capability
640
642
  attach_gateway('gateway')
641
643
  apply_gateway_policy
@@ -668,6 +670,16 @@ module Empeira
668
670
  'images.direct_egress image'
669
671
  end
670
672
 
673
+ def require_scoped_egress_capability
674
+ return if @gateway_plan.fetch('entries').empty?
675
+
676
+ result = @runtime.service_exec(@observed.fetch('gateway'),
677
+ [Network::Gateway::EXECUTABLE, 'scoped-egress-capability'])
678
+ return if result.success? && result.stdout.strip == 'scoped-egress-v1'
679
+
680
+ raise Error, 'Gateway does not support scoped direct egress; configure a compatible images.direct_egress image'
681
+ end
682
+
671
683
  def apply_gateway_policy
672
684
  # An uncertain apply must force fresh lockdown during failure recovery.
673
685
  @gateway_locked_id = nil