empeira 0.1.0.pre.alpha.3 → 0.1.0.pre.alpha.6
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.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/config/defaults.yaml +10 -4
- data/docs/addons.md +10 -6
- data/docs/architecture.md +6 -3
- data/docs/configuration.md +61 -33
- data/docs/development.md +5 -3
- data/docs/hiera-and-eyaml.md +6 -4
- data/docs/networking.md +16 -7
- data/docs/nodes.md +10 -6
- data/docs/proxy.md +62 -19
- data/docs/troubleshooting.md +18 -7
- data/lib/empeira/agent/native_commands.rb +3 -3
- data/lib/empeira/build.json +3 -3
- data/lib/empeira/cli/proxy.rb +2 -2
- data/lib/empeira/configuration/schema.rb +27 -2
- data/lib/empeira/control_plane/controller.rb +19 -5
- data/lib/empeira/control_plane/environment.rb +25 -5
- data/lib/empeira/control_plane/files.rb +1 -8
- data/lib/empeira/control_plane/plan.rb +16 -14
- data/lib/empeira/network/direct_egress.rb +0 -7
- data/lib/empeira/network/gateway.rb +9 -1
- data/lib/empeira/network/host_policy.rb +1 -1
- data/lib/empeira/network/peer/layout.rb +8 -0
- data/lib/empeira/network/proxy_bindings.rb +27 -10
- data/lib/empeira/node/runtime_proxy.rb +2 -2
- data/lib/empeira/runtime/service_arguments.rb +7 -1
- data/lib/empeira/server/environment_configuration.rb +8 -2
- data/resources/gateway/configure.rb +21 -11
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6a6bd2bd179b9a3fa789f2342e0646d9e5d681941cd2df234770cf8a94a01f31
|
|
4
|
+
data.tar.gz: 8925fd4ded29c7206cfe6c1d2d072f7a41a649582504dbf1a29232543525aacf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 988ab09ce7e604a6bddfe1f5acad7f2847939b96e33ad6b73d06a03d49a6aafc7048222f5bb2a26eddb412dd75442b6d57addddd9558cb974a675603c11a1fb6
|
|
7
|
+
data.tar.gz: 6d5c737b4f18f0d400f279c59b0b10f7e68bb3b564c649b42207e0f447ba7b68a2acd93b34258ca7d025252d91de4c2befcfc741b85195f23a8b8feecaa99e46
|
data/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](https://github.com/c8m6/empeira/actions/workflows/openvox.yml)
|
|
7
7
|
[](https://github.com/c8m6/empeira/releases)
|
|
8
8
|
[](docs/installation.md)
|
|
9
|
-
[](LICENSE)
|
|
10
10
|
|
|
11
11
|
**ἐμπειρία (empeiría)** — experience gained through practice. The name reflects
|
|
12
12
|
learning about infrastructure by experimenting, testing and seeing what happens.
|
|
@@ -149,6 +149,6 @@ and [pull requests](https://github.com/c8m6/empeira/pulls) are welcome. Follow t
|
|
|
149
149
|
[security policy](SECURITY.md).
|
|
150
150
|
|
|
151
151
|
Project-owned code and documentation are licensed under
|
|
152
|
-
[AGPL-3.0-
|
|
152
|
+
[AGPL-3.0-or-later](LICENSE). Third-party packages and images retain their own licenses
|
|
153
153
|
and notices and are obtained from upstream; empeira does not bundle them or
|
|
154
154
|
redistribute proprietary Puppet Enterprise software.
|
data/config/defaults.yaml
CHANGED
|
@@ -50,12 +50,18 @@ 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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
nodes:
|
|
59
|
+
enabled: false
|
|
60
|
+
global: []
|
|
61
|
+
rules: []
|
|
62
|
+
infrastructure:
|
|
63
|
+
enabled: false
|
|
64
|
+
allow: []
|
|
59
65
|
vm:
|
|
60
66
|
disk: 30
|
|
61
67
|
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
|
-
|
|
212
|
-
|
|
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 `
|
|
221
|
-
`
|
|
222
|
-
`
|
|
223
|
-
`
|
|
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
|
@@ -184,9 +184,12 @@ there are no implicit database-major migrations or automatic node replacements.
|
|
|
184
184
|
|
|
185
185
|
Managed servers disable environment caching through native Puppet configuration
|
|
186
186
|
(`environment_timeout = 0` in `[server]`), applied by the startup wrapper before
|
|
187
|
-
the configured image entrypoint. `
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
the configured image entrypoint. An independent `environment.conf` copy in the
|
|
188
|
+
managed environment enforces zero even when the control repository specifies
|
|
189
|
+
`unlimited`; the original remains unchanged and read-only. `up` refreshes the copy,
|
|
190
|
+
checks both native and selected-environment settings, and restarts only a server
|
|
191
|
+
whose native setting changed. It preserves every other configuration key and
|
|
192
|
+
retained storage. The configured Puppet confdir must agree with Puppetserver
|
|
190
193
|
HOCON `master-conf-dir`. Catalog
|
|
191
194
|
requests reload live code without repository hashes or environment-cache checkpoints.
|
|
192
195
|
See [nodes](nodes.md#environment-cache-and-live-code).
|
data/docs/configuration.md
CHANGED
|
@@ -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
|
-
|
|
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,14 @@ 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` |
|
|
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`;
|
|
156
|
-
| `proxy.global` | DNS destination array, `[]` |
|
|
157
|
-
| `proxy.rules` | Additive hostname-glob rules with `hosts` and `allow` arrays, `[]` |
|
|
162
|
+
| `proxy.nodes.enabled` | Boolean, `false`; enables normal node HTTP/HTTPS proxy access |
|
|
163
|
+
| `proxy.nodes.global` | DNS destination array, `[]` |
|
|
164
|
+
| `proxy.nodes.rules` | Additive node hostname-glob rules with `hosts` and `allow` arrays, `[]` |
|
|
165
|
+
| `proxy.infrastructure.enabled` | Boolean, `false`; enables infrastructure HTTP/HTTPS proxy access |
|
|
166
|
+
| `proxy.infrastructure.allow` | Independent infrastructure DNS destination array, `[]` |
|
|
158
167
|
| `vm.disk` | New VM virtual disk capacity in GiB, integer `1`–`2048`, default `30`; must accommodate the base image |
|
|
159
168
|
| `vm.interfaces` | VM-only hostname rules defining dummy/VLAN devices with explicit IPv4 CIDR host addresses, `[]` |
|
|
160
169
|
| `vm.console.root_password` | Initial console password, `empeira`; a string overrides it and `null` disables it |
|
|
@@ -404,20 +413,25 @@ runtime values without inferring them from the image name. Missing or unsafe
|
|
|
404
413
|
runtime fields fail configuration validation. Managed servers use
|
|
405
414
|
`environment_timeout = 0` in the native `[server]` section. The startup wrapper
|
|
406
415
|
uses `puppet config set` before the configured entrypoint, independently of image
|
|
407
|
-
environment-variable mappings.
|
|
408
|
-
|
|
409
|
-
|
|
416
|
+
environment-variable mappings. Empeira also prepares an independent copy of the
|
|
417
|
+
project's `environment.conf` in its managed environment directory, replacing or
|
|
418
|
+
adding only `environment_timeout = 0`. Other settings, including `modulepath` and
|
|
419
|
+
`config_version`, are preserved. The original file and the control repository
|
|
420
|
+
remain read-only in the server. The copy is refreshed during `up`, including when
|
|
421
|
+
the project has no `environment.conf` or Hiera mounts.
|
|
422
|
+
|
|
423
|
+
`up` verifies both the native server setting and the effective timeout for
|
|
424
|
+
`server.environment`, against the confdir containing
|
|
425
|
+
`server.runtime.paths.puppetdb_config`; custom Puppetserver
|
|
410
426
|
`jruby-puppet.master-conf-dir` must refer to that same directory. Puppetserver
|
|
411
|
-
HOCON master directories take precedence over Puppet CLI defaults.
|
|
412
|
-
|
|
427
|
+
HOCON master directories take precedence over Puppet CLI defaults. Native setting
|
|
428
|
+
drift updates only that key and restarts the same owned server; an
|
|
413
429
|
entrypoint that overwrites it fails verification. CA, SSL and database storage
|
|
414
430
|
are preserved. The next catalog request reloads live code without
|
|
415
|
-
an Empeira content scan or admin API invalidation.
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
selected environment's override, so it can report `unlimited` while the native
|
|
420
|
-
server setting is zero. Inspect both configuration layers when diagnosing caching.
|
|
431
|
+
an Empeira content scan or admin API invalidation. A project timeout of `unlimited`
|
|
432
|
+
therefore stays unchanged in the checkout while the managed server uses zero.
|
|
433
|
+
To verify it, run `puppet config print environment_timeout --section server
|
|
434
|
+
--environment ENVIRONMENT` inside the server with the configured environment name.
|
|
421
435
|
|
|
422
436
|
`puppetdb.runtime` holds the independent OpenVoxDB process contract. Custom
|
|
423
437
|
images may override its executable, numeric user, Java and main arguments,
|
|
@@ -483,14 +497,20 @@ The numeric gem packaging fallback is never used to bypass this check.
|
|
|
483
497
|
```yaml
|
|
484
498
|
network:
|
|
485
499
|
egress:
|
|
486
|
-
|
|
487
|
-
|
|
500
|
+
nodes:
|
|
501
|
+
- host: api.example.com
|
|
502
|
+
ports: [443]
|
|
503
|
+
infrastructure: []
|
|
488
504
|
proxy:
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
505
|
+
nodes:
|
|
506
|
+
enabled: true
|
|
507
|
+
global: [forgeapi.puppet.com]
|
|
508
|
+
rules:
|
|
509
|
+
- hosts: ["*-web-*"]
|
|
510
|
+
allow: [github.com, "*.github.com"]
|
|
511
|
+
infrastructure:
|
|
512
|
+
enabled: false
|
|
513
|
+
allow: []
|
|
494
514
|
hiera:
|
|
495
515
|
mounts:
|
|
496
516
|
- source: ../hieradata
|
|
@@ -502,7 +522,12 @@ vm:
|
|
|
502
522
|
root_password: null
|
|
503
523
|
```
|
|
504
524
|
|
|
505
|
-
When `proxy.enabled` is true, every node may use its matching destination policy.
|
|
525
|
+
When `proxy.nodes.enabled` is true, every node may use its matching destination policy.
|
|
526
|
+
Infrastructure access is separately controlled by `proxy.infrastructure.enabled`
|
|
527
|
+
and `proxy.infrastructure.allow`; both scopes default to disabled. Disabled
|
|
528
|
+
infrastructure has no configured proxy endpoint even when nodes use the proxy.
|
|
529
|
+
Keep its direct-egress list empty for internal-only infrastructure. See the
|
|
530
|
+
[migration instructions](proxy.md#migrating-shared-settings) for former shared keys.
|
|
506
531
|
After bootstrap cleanup, Empeira reconciles normal proxy settings on running,
|
|
507
532
|
provisioned nodes during `up` and `node start`. Repeated Puppet runs use the
|
|
508
533
|
existing guest configuration. No
|
|
@@ -714,21 +739,24 @@ desired state; Puppet owns package state after the first catalog.
|
|
|
714
739
|
|
|
715
740
|
## Direct egress
|
|
716
741
|
|
|
717
|
-
`network.egress`
|
|
718
|
-
|
|
742
|
+
`network.egress.nodes` and `network.egress.infrastructure` contain separate
|
|
743
|
+
destination TCP grants, independent of HTTP/HTTPS proxy policy. A node grant never
|
|
744
|
+
applies to infrastructure or conversely. Entries have exactly one `host` or IPv4 `ip` and a nonempty unique `ports`
|
|
719
745
|
array (1–65535). `up` resolves all A records and replaces the complete policy;
|
|
720
746
|
resolution/apply failure blocks egress. IPv6 and external UDP grants are unsupported.
|
|
721
747
|
|
|
722
748
|
```yaml
|
|
723
749
|
network:
|
|
724
750
|
egress:
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
751
|
+
nodes:
|
|
752
|
+
- host: repository.example.net
|
|
753
|
+
ports: [443]
|
|
754
|
+
- ip: 203.0.113.10
|
|
755
|
+
ports: [443, 8443]
|
|
756
|
+
infrastructure: []
|
|
729
757
|
```
|
|
730
758
|
|
|
731
|
-
|
|
759
|
+
Direct destinations enter only their own scope's generated `NO_PROXY`; applications choose DIRECT
|
|
732
760
|
or proxy themselves. See [networking](networking.md#one-gateway-one-direct-egress-policy)
|
|
733
761
|
for routing, revocation and privilege boundaries.
|
|
734
762
|
|
|
@@ -760,7 +788,7 @@ vm:
|
|
|
760
788
|
network: 192.0.2.20/32
|
|
761
789
|
```
|
|
762
790
|
|
|
763
|
-
Patterns use the same `*`/`?` glob matcher as `proxy.rules`, against the full stored
|
|
791
|
+
Patterns use the same `*`/`?` glob matcher as `proxy.nodes.rules`, against the full stored
|
|
764
792
|
node hostname, case-insensitively. Patterns within a rule are alternatives. All
|
|
765
793
|
matching rules contribute devices; identical definitions merge once. A conflicting
|
|
766
794
|
definition names the hostname, device and both configuration paths. No match creates
|
data/docs/development.md
CHANGED
|
@@ -253,6 +253,7 @@ The APT authentication regression uses a local HTTPS repository with synthetic B
|
|
|
253
253
|
credentials and an ephemeral GPG signing key. It checks the helper's effective
|
|
254
254
|
`auth.conf.d` configuration, private file mode and exact credential bytes, then
|
|
255
255
|
verifies metadata returns 401 before one interactive login and 200 after the retry.
|
|
256
|
+
The fixture omits the 401 reason phrase to exercise APT's bare status/IP diagnostic.
|
|
256
257
|
The selected package download must reuse that login and pass its SHA-256 check.
|
|
257
258
|
It requires host `gpg`/`gpgconf` and a locally reachable container engine; helper and
|
|
258
259
|
host TLS verification remain enabled. Run it explicitly for either engine:
|
|
@@ -511,9 +512,10 @@ Run `EMPEIRA_INTEGRATION=server-environment bundle exec rspec spec/integration/s
|
|
|
511
512
|
to check native timeout 0, live edits, CA retention, PuppetDB and idempotent up
|
|
512
513
|
under Docker/rootless Podman with the default server and a custom entrypoint
|
|
513
514
|
that ignores timeout environment mapping.
|
|
514
|
-
The same gate verifies
|
|
515
|
-
|
|
516
|
-
|
|
515
|
+
The same gate verifies that an explicit `environment.conf` timeout of `unlimited`
|
|
516
|
+
is zero in the managed copy while the original stays unchanged. It edits a Ruby
|
|
517
|
+
Puppet function between catalogs and checks the changed result without `up` or a
|
|
518
|
+
restart, followed by an idempotent `up`.
|
|
517
519
|
|
|
518
520
|
Run the isolated diagnostics/recovery gate with each native runtime:
|
|
519
521
|
|
data/docs/hiera-and-eyaml.md
CHANGED
|
@@ -105,7 +105,8 @@ An optional mount is not permission to ignore unsafe configuration.
|
|
|
105
105
|
Puppet's [default modulepath](https://help.puppet.com/core/current/Content/PuppetCore/config_file_environment.htm)
|
|
106
106
|
includes the environment's `modules` directory. Empeira checks explicit
|
|
107
107
|
`environment.conf` modulepaths and reports a known exclusion before startup. It
|
|
108
|
-
never rewrites
|
|
108
|
+
never rewrites the original file. Its managed copy changes only
|
|
109
|
+
`environment_timeout` to zero. Expressions containing unresolved Puppet variables cannot
|
|
109
110
|
be verified completely from the CLI host; ensure the effective server modulepath
|
|
110
111
|
includes the mount destination. Environment mounts can be referenced by ordinary
|
|
111
112
|
relative paths in `hiera.yaml`, such as `data/external` or `external` when the datadir
|
|
@@ -117,7 +118,8 @@ an image rebuild, `up`, or a service restart.
|
|
|
117
118
|
Empeira neither copies their data nor writes probe files into them. It uses a managed
|
|
118
119
|
environment directory containing links to the read-only control repository so nested
|
|
119
120
|
mount targets can exist without modifying control code. Existing control files and
|
|
120
|
-
linked directories remain live
|
|
121
|
+
linked directories remain live; `environment.conf` is an independent copy refreshed
|
|
122
|
+
by `up`. `up` also refreshes links for new top-level control entries and entries under
|
|
121
123
|
projected ancestor directories. EYAML keys remain configured separately in `eyaml`,
|
|
122
124
|
and decryption runs on the server.
|
|
123
125
|
|
|
@@ -205,8 +207,8 @@ The obsolete `images.modules` key is rejected.
|
|
|
205
207
|
The disposable installer belongs to the update plane. It uses the selected
|
|
206
208
|
runtime's normal bridge network (`bridge` on Docker, `podman` on Podman), with
|
|
207
209
|
normal runtime DNS and external connectivity. It never joins an Empeira workspace
|
|
208
|
-
network and needs neither CoreDNS nor Squid. Workspace `network.egress`, `proxy.enabled`,
|
|
209
|
-
`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
|
|
210
212
|
Forge or Git acquisition. Supplied `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` and their
|
|
211
213
|
lowercase equivalents are inherited unchanged; Empeira generates no proxy URL.
|
|
212
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`
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
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
|
@@ -87,18 +87,22 @@ Managed servers enforce native `[server] environment_timeout = 0` before startup
|
|
|
87
87
|
and verify the effective setting during `up`, independently of image environment
|
|
88
88
|
variable support. Existing workspaces adopt it on
|
|
89
89
|
their next `up`; run that once after updating from a version that enabled caching.
|
|
90
|
+
Adopting the managed environment copy changes the server mount and recreates its
|
|
91
|
+
container once, retaining CA, SSL and database storage.
|
|
90
92
|
Each catalog request reloads live control code, modules and mounted Hiera data.
|
|
91
93
|
Empeira does not hash these trees,
|
|
92
94
|
persist content checkpoints or call the environment-cache admin API. Code edits
|
|
93
95
|
need neither `up` nor a server restart, including for agents invoked manually.
|
|
94
96
|
Empeira does not enable `use_cached_catalog`.
|
|
95
97
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
Empeira copies `environment.conf` into its managed environment and sets its timeout
|
|
99
|
+
to zero there, preserving all other settings and the original file. Run `up` to
|
|
100
|
+
refresh this copy after editing other environment settings. Avoid concurrent edits
|
|
101
|
+
during a catalog run. Existing files and directories stay live through read-only
|
|
102
|
+
links. New top-level entries and new siblings under ancestors projected for nested
|
|
103
|
+
Hiera mounts require `up` to create their links. `node start` resumes the node
|
|
104
|
+
without invoking Puppet; Puppet services enabled by a catalog may run independently
|
|
105
|
+
on startup.
|
|
102
106
|
|
|
103
107
|
## Distribution package sources
|
|
104
108
|
|
data/docs/proxy.md
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
# Proxy policy
|
|
2
2
|
|
|
3
|
-
Normal external HTTP/HTTPS access stays behind Squid.
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
nodes:
|
|
10
|
+
enabled: true
|
|
11
|
+
global:
|
|
12
|
+
- forgeapi.puppet.com
|
|
13
|
+
rules:
|
|
14
|
+
- hosts: ["lab-*"]
|
|
15
|
+
allow: ["*.github.com"]
|
|
16
|
+
- hosts: ["*-web-*"]
|
|
17
|
+
allow: [github.com, nginx.org]
|
|
18
|
+
infrastructure:
|
|
19
|
+
enabled: false
|
|
20
|
+
allow: []
|
|
17
21
|
```
|
|
18
22
|
|
|
19
23
|
```console
|
|
@@ -46,7 +50,7 @@ infrastructure has already been reconciled.
|
|
|
46
50
|
|
|
47
51
|
## Matching and destinations
|
|
48
52
|
|
|
49
|
-
Defaults are `proxy.global: []` and `proxy.rules: []`. Every node receives
|
|
53
|
+
Defaults are `proxy.nodes.global: []` and `proxy.nodes.rules: []`. Every node receives
|
|
50
54
|
all global destinations plus `allow` from **every** matching rule. Duplicate entries
|
|
51
55
|
are removed while preserving configuration order. A rule matches when any of its
|
|
52
56
|
`hosts` patterns matches the complete lowercase hostname. Patterns must be lowercase.
|
|
@@ -66,14 +70,14 @@ Destination rules are separate from hostname globs:
|
|
|
66
70
|
Only HTTP port 80 and HTTPS CONNECT port 443 are allowed. Allowed DNS destinations
|
|
67
71
|
are reachable regardless of their target IP range, including private, loopback,
|
|
68
72
|
link-local and reserved addresses. Domains without a matching allow rule remain
|
|
69
|
-
denied. There is no TLS interception.
|
|
70
|
-
|
|
73
|
+
denied. There is no TLS interception. Node destinations never grant infrastructure
|
|
74
|
+
access, and infrastructure destinations never grant node access.
|
|
71
75
|
|
|
72
76
|
## Node policy and reconciliation
|
|
73
77
|
|
|
74
|
-
When `proxy.enabled` is true, all nodes are authorized as proxy clients. Nodes get
|
|
78
|
+
When `proxy.nodes.enabled` is true, all nodes are authorized as proxy clients. Nodes get
|
|
75
79
|
upper/lowercase proxy variables and internal `NO_PROXY` entries, including explicitly
|
|
76
|
-
configured `network.egress` hosts. Those named hosts use the common workspace
|
|
80
|
+
configured `network.egress.nodes` hosts. Those named hosts use the common workspace
|
|
77
81
|
gateway for their configured ports. VM access uses the same destination policy bound
|
|
78
82
|
directly to the VM instance's reserved peer address. The proxy is reached at
|
|
79
83
|
`proxy.empeira.internal:3128` on the isolated network. A request without a matching
|
|
@@ -90,17 +94,56 @@ Container and VM nodes consume the new policy without a restart; open proxy
|
|
|
90
94
|
connections can end during reconciliation. Enabling/disabling proxy mode is a
|
|
91
95
|
separate topology change and may require explicit node cleanup as reported by `up`.
|
|
92
96
|
|
|
93
|
-
Changing
|
|
94
|
-
environment. `up` replaces a server whose environment definition changes while
|
|
97
|
+
Changing `network.egress.infrastructure` destinations also changes infrastructure
|
|
98
|
+
`NO_PROXY`. Changing node destinations does not change the server environment. `up` replaces a server whose environment definition changes while
|
|
95
99
|
retaining CA storage and node identities. Changing only destination ports or
|
|
96
100
|
transparent redirect rules preserves the server when its definition is unchanged.
|
|
97
101
|
|
|
102
|
+
## Infrastructure policy
|
|
103
|
+
|
|
104
|
+
`proxy.infrastructure.enabled` controls the Puppet/OpenVox server, PostgreSQL,
|
|
105
|
+
PuppetDB backend/relay and `containers.additional` services. Its independent
|
|
106
|
+
`allow` list uses the same exact-domain and domain-family syntax as node grants.
|
|
107
|
+
The browser, DNS, Squid and gateway transports do not receive these application
|
|
108
|
+
proxy settings.
|
|
109
|
+
|
|
110
|
+
```yaml
|
|
111
|
+
proxy:
|
|
112
|
+
infrastructure:
|
|
113
|
+
enabled: true
|
|
114
|
+
allow: [api.example.net]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
When disabled, infrastructure definitions contain empty proxy values, overriding
|
|
118
|
+
any image defaults. They contain no configured proxy endpoint and receive no Squid
|
|
119
|
+
client grants, even if the node proxy is enabled. No startup wrapper removes an
|
|
120
|
+
otherwise configured proxy. Internal names and mocks remain directly reachable.
|
|
121
|
+
When enabled, only infrastructure's own direct destinations enter its `NO_PROXY`.
|
|
122
|
+
Explicit environment settings in additional-service definitions remain application
|
|
123
|
+
configuration; they do not add Squid permissions.
|
|
124
|
+
|
|
125
|
+
`up` creates the shared normal proxy when either scope is enabled and removes it
|
|
126
|
+
when both are disabled. Changing infrastructure's switch updates affected service
|
|
127
|
+
definitions while retaining storage and node identities. Allowlist-only edits reload
|
|
128
|
+
Squid without replacing unrelated services. Direct grants remain independent of
|
|
129
|
+
both proxy switches; keep `network.egress.infrastructure: []` for internal-only
|
|
130
|
+
infrastructure.
|
|
131
|
+
|
|
132
|
+
## Migrating shared settings
|
|
133
|
+
|
|
134
|
+
The former `proxy.enabled`, `proxy.global` and `proxy.rules` keys are rejected with
|
|
135
|
+
a migration diagnostic. Move them under `proxy.nodes`; configure infrastructure
|
|
136
|
+
explicitly with its own switch and `allow` list. Old shared `network.egress` arrays
|
|
137
|
+
and legacy `mode` values are also rejected. Move grants to `network.egress.nodes`
|
|
138
|
+
or `network.egress.infrastructure` according to the intended clients. No existing
|
|
139
|
+
grant is silently copied to infrastructure.
|
|
140
|
+
|
|
98
141
|
## Managed bootstrap
|
|
99
142
|
|
|
100
143
|
Managed bootstrap destinations are separate from normal user policy. Reviewed
|
|
101
144
|
agent and distribution endpoints in `agent.install` and `bootstrap.guests` allow
|
|
102
145
|
Puppet/OpenVox and required guest packages to be installed without duplicating them
|
|
103
|
-
in `proxy.global`. The temporary authenticated bootstrap proxy uses workspace DNS
|
|
146
|
+
in `proxy.nodes.global`. The temporary authenticated bootstrap proxy uses workspace DNS
|
|
104
147
|
and retains its private, loopback, link-local and reserved destination-address
|
|
105
148
|
restrictions independently of normal proxy policy. Its credential is not stored in
|
|
106
149
|
the guest seed, JSON inventory or logs. A /32 source ACL admits only the provisioning
|
data/docs/troubleshooting.md
CHANGED
|
@@ -33,6 +33,14 @@ HTTP 403 does not trigger login, and HTTP 407 identifies proxy authentication. R
|
|
|
33
33
|
authentication continues to use native `docker login` / `podman login` and the reported
|
|
34
34
|
registry host; Empeira does not log into registries automatically.
|
|
35
35
|
|
|
36
|
+
APT can report an HTTP status without a reason phrase, for example
|
|
37
|
+
`401 [IP: 192.0.2.1 443]`. This still triggers the single repository login attempt
|
|
38
|
+
when no ENV credentials are set and both standard input and standard error are
|
|
39
|
+
connected to a TTY. The corresponding `403` and `407` forms retain their access-denied
|
|
40
|
+
and proxy-authentication diagnoses without prompting. A node retained as `incomplete`
|
|
41
|
+
after an acquisition failure must be destroyed with `empeira node destroy HOSTNAME`
|
|
42
|
+
and recreated with the original `empeira node run` arguments after resolving the cause.
|
|
43
|
+
|
|
36
44
|
APT helper credentials use literal `login` and `password` tokens in a temporary
|
|
37
45
|
0600 file under `/etc/apt/auth.conf.d`. APT treats surrounding quotes and added
|
|
38
46
|
backslashes as credential bytes, so Empeira does not add them. A successful login
|
|
@@ -124,7 +132,7 @@ or restart of that same VM conflicts with the shared instance guard: detach with
|
|
|
124
132
|
## Proxy destination denied
|
|
125
133
|
|
|
126
134
|
Run `empeira proxy show HOSTNAME` and check every matching rule. Verify the node
|
|
127
|
-
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
|
|
128
136
|
configuration and current node addresses. Allowed DNS destinations may resolve to
|
|
129
137
|
private or reserved IP addresses; only HTTP port 80 and HTTPS CONNECT port 443 are
|
|
130
138
|
permitted. After updating Empeira, run `empeira up` to replace a proxy still using
|
|
@@ -140,11 +148,14 @@ problem. A successful probe with different directory listings indicates a
|
|
|
140
148
|
different engine view of the source. Empeira does not weaken mount checks or
|
|
141
149
|
change host permissions automatically.
|
|
142
150
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
151
|
+
Empeira enforces `environment_timeout = 0` in both the native server setting and
|
|
152
|
+
its independent copy of `environment.conf`. The project's original file remains
|
|
153
|
+
unchanged, even when it specifies `unlimited`. Run `up` after updating Empeira or
|
|
154
|
+
editing other settings in `environment.conf` to refresh the managed copy. Inside
|
|
155
|
+
the server, `puppet config print environment_timeout --section server --environment
|
|
156
|
+
ENVIRONMENT` must report `0` for the configured environment. A different value
|
|
157
|
+
fails startup verification; check the custom server's confdir and environmentpath.
|
|
158
|
+
No explicit cache invalidation is performed.
|
|
148
159
|
|
|
149
160
|
`config show` includes `effective_hiera_mounts`. A skipped optional source is
|
|
150
161
|
reported during `up`; mount it locally or correct the source path, then run `up`
|
|
@@ -356,6 +367,6 @@ external runtime network as a workaround. Docker workspace bridge attachment
|
|
|
356
367
|
errors require an accessible Linux-engine host namespace and an identifiable
|
|
357
368
|
iptables backend. The helper changes only owned bridge rules. Do not disable the
|
|
358
369
|
host firewall globally. Check Docker Desktop host-network restrictions separately.
|
|
359
|
-
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`,
|
|
360
371
|
not adding unrelated direct grants. Corporate DNS still follows host discovery;
|
|
361
372
|
IPv6-only upstreams must be replaced with reachable IPv4 resolvers for this backend.
|
|
@@ -39,11 +39,11 @@ module Empeira
|
|
|
39
39
|
diagnostic = Execution::Diagnostics.native(result.with(stderr: text, stdout: ''),
|
|
40
40
|
operation: "Agent #{operation}", tool: arguments.first)
|
|
41
41
|
case text
|
|
42
|
-
when /407\s+Proxy Authentication Required|(?:HTTP|Status code|HTTP code)\s*:?\s*407\b
|
|
42
|
+
when /407\s+Proxy Authentication Required|(?:HTTP|Status code|HTTP code)\s*:?\s*407\b|\b407[ \t]+\[IP:/i
|
|
43
43
|
raise Error, "Host helper proxy authentication failed\n#{diagnostic}", cause: nil
|
|
44
|
-
when /401\s+Unauthorized|(?:HTTP|Status code|HTTP code)\s*:?\s*401\b
|
|
44
|
+
when /401\s+Unauthorized|(?:HTTP|Status code|HTTP code)\s*:?\s*401\b|\b401[ \t]+\[IP:/i
|
|
45
45
|
raise Agent::AuthenticationRequired, diagnostic, cause: nil
|
|
46
|
-
when /403\s+Forbidden|(?:HTTP|Status code|HTTP code)\s*:?\s*403\b
|
|
46
|
+
when /403\s+Forbidden|(?:HTTP|Status code|HTTP code)\s*:?\s*403\b|\b403[ \t]+\[IP:/i
|
|
47
47
|
raise Error, "Agent repository access denied\n#{diagnostic}", cause: nil
|
|
48
48
|
end
|
|
49
49
|
raise Error, "Agent #{operation} failed\n#{diagnostic}", cause: nil
|
data/lib/empeira/build.json
CHANGED
data/lib/empeira/cli/proxy.rb
CHANGED
|
@@ -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.
|
|
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
|
|
22
|
+
'The normal node proxy is disabled.'
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def display_matches(rules)
|
|
@@ -20,8 +20,10 @@ 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, '
|
|
24
|
-
|
|
23
|
+
'network' => { 'egress' => { 'nodes' => :network_egress, 'infrastructure' => :network_egress },
|
|
24
|
+
'redirects' => :network_redirects },
|
|
25
|
+
'proxy' => { 'nodes' => { 'enabled' => :boolean, 'global' => :allowlist, 'rules' => :proxy_rules },
|
|
26
|
+
'infrastructure' => { 'enabled' => :boolean, 'allow' => :allowlist } },
|
|
25
27
|
'vm' => { 'disk' => :vm_disk, 'interfaces' => :vm_interfaces,
|
|
26
28
|
'console' => { 'root_password' => :optional_string } },
|
|
27
29
|
'dns' => { 'upstream' => { 'mode' => :dns_mode, 'servers' => :dns_servers },
|
|
@@ -116,12 +118,35 @@ module Empeira
|
|
|
116
118
|
end
|
|
117
119
|
|
|
118
120
|
def validate_fragment!(data)
|
|
121
|
+
validate_scope_migration!(data)
|
|
119
122
|
validate_mapping!(data, RULES, [])
|
|
120
123
|
data
|
|
121
124
|
end
|
|
122
125
|
|
|
123
126
|
private
|
|
124
127
|
|
|
128
|
+
def validate_scope_migration!(data)
|
|
129
|
+
return unless data.is_a?(Hash)
|
|
130
|
+
|
|
131
|
+
validate_proxy_scope_migration!(data['proxy'])
|
|
132
|
+
network = data['network']
|
|
133
|
+
validate_egress_scope_migration!(network['egress']) if network.is_a?(Hash)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def validate_proxy_scope_migration!(proxy)
|
|
137
|
+
return unless proxy.is_a?(Hash) && proxy.keys.intersect?(%w[enabled global rules])
|
|
138
|
+
|
|
139
|
+
raise ConfigurationError, 'Shared proxy settings have moved to proxy.nodes; configure ' \
|
|
140
|
+
'proxy.infrastructure separately'
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def validate_egress_scope_migration!(egress)
|
|
144
|
+
return unless egress.is_a?(Array) || (egress.is_a?(Hash) && egress.key?('mode'))
|
|
145
|
+
|
|
146
|
+
raise ConfigurationError, 'Shared network.egress grants must be moved explicitly to ' \
|
|
147
|
+
'network.egress.nodes or network.egress.infrastructure'
|
|
148
|
+
end
|
|
149
|
+
|
|
125
150
|
def validate_operational!(data)
|
|
126
151
|
validate_images!(data.fetch('images'))
|
|
127
152
|
NodeImages.effective!(data.fetch('images'))
|
|
@@ -254,8 +254,10 @@ module Empeira
|
|
|
254
254
|
end
|
|
255
255
|
|
|
256
256
|
def resolve_direct_egress
|
|
257
|
-
|
|
258
|
-
|
|
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:)
|
|
@@ -350,10 +352,11 @@ module Empeira
|
|
|
350
352
|
|
|
351
353
|
def reconcile_server_environment
|
|
352
354
|
environment = Server::EnvironmentConfiguration.new(runtime: @runtime, plan: @plan)
|
|
353
|
-
|
|
355
|
+
if environment.reconcile(@observed.fetch('server'))
|
|
356
|
+
@changed = true
|
|
357
|
+
@health.wait('server', @observed)
|
|
358
|
+
end
|
|
354
359
|
|
|
355
|
-
@changed = true
|
|
356
|
-
@health.wait('server', @observed)
|
|
357
360
|
environment.verify!(@observed.fetch('server'))
|
|
358
361
|
end
|
|
359
362
|
|
|
@@ -635,6 +638,7 @@ module Empeira
|
|
|
635
638
|
end
|
|
636
639
|
reconcile('gateway')
|
|
637
640
|
lockdown_gateway(@observed.fetch('gateway'))
|
|
641
|
+
require_scoped_egress_capability
|
|
638
642
|
require_redirect_capability
|
|
639
643
|
attach_gateway('gateway')
|
|
640
644
|
apply_gateway_policy
|
|
@@ -667,6 +671,16 @@ module Empeira
|
|
|
667
671
|
'images.direct_egress image'
|
|
668
672
|
end
|
|
669
673
|
|
|
674
|
+
def require_scoped_egress_capability
|
|
675
|
+
return if @gateway_plan.fetch('entries').empty?
|
|
676
|
+
|
|
677
|
+
result = @runtime.service_exec(@observed.fetch('gateway'),
|
|
678
|
+
[Network::Gateway::EXECUTABLE, 'scoped-egress-capability'])
|
|
679
|
+
return if result.success? && result.stdout.strip == 'scoped-egress-v1'
|
|
680
|
+
|
|
681
|
+
raise Error, 'Gateway does not support scoped direct egress; configure a compatible images.direct_egress image'
|
|
682
|
+
end
|
|
683
|
+
|
|
670
684
|
def apply_gateway_policy
|
|
671
685
|
# An uncertain apply must force fresh lockdown during failure recovery.
|
|
672
686
|
@gateway_locked_id = nil
|
|
@@ -9,7 +9,7 @@ module Empeira
|
|
|
9
9
|
def initialize(context:, hiera:)
|
|
10
10
|
@project = context.project.path
|
|
11
11
|
@root = root(context)
|
|
12
|
-
@targets = mount_targets(hiera)
|
|
12
|
+
@targets = ['environment.conf', *mount_targets(hiera)]
|
|
13
13
|
@modules = Modules::Storage.new(context: context).root if @project.join('Puppetfile').file?
|
|
14
14
|
@targets |= ['modules'] if @modules
|
|
15
15
|
@base = context.locations.workspace(context.workspace).join('environments')
|
|
@@ -17,14 +17,11 @@ module Empeira
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def mounts
|
|
20
|
-
return [bind(@project, @root)] if @targets.empty?
|
|
21
|
-
|
|
22
20
|
[bind(@project, CONTROL), bind(@directory, @root),
|
|
23
21
|
*(@modules ? [bind(@modules, "#{@root}/modules")] : [])]
|
|
24
22
|
end
|
|
25
23
|
|
|
26
24
|
def prepare
|
|
27
|
-
return if @targets.empty?
|
|
28
25
|
raise Error, 'Managed environment directory must not be a symlink' if @directory.symlink?
|
|
29
26
|
|
|
30
27
|
FileUtils.mkdir_p(@directory, mode: 0o755)
|
|
@@ -81,13 +78,36 @@ module Empeira
|
|
|
81
78
|
end
|
|
82
79
|
|
|
83
80
|
def create_entry(relative, destination)
|
|
84
|
-
if
|
|
81
|
+
if relative == 'environment.conf'
|
|
82
|
+
prepare_configuration(destination)
|
|
83
|
+
elsif @targets.any? { |target| target == relative || target.start_with?("#{relative}/") }
|
|
85
84
|
create_target(relative, destination)
|
|
86
85
|
else
|
|
87
86
|
create_link(relative, destination)
|
|
88
87
|
end
|
|
89
88
|
end
|
|
90
89
|
|
|
90
|
+
def prepare_configuration(destination)
|
|
91
|
+
content = environment_configuration
|
|
92
|
+
return if !destination.symlink? && destination.file? && destination.binread == content
|
|
93
|
+
|
|
94
|
+
Tempfile.create('environment-', @directory) do |file|
|
|
95
|
+
file.chmod(0o644)
|
|
96
|
+
file.write(content)
|
|
97
|
+
file.flush
|
|
98
|
+
File.rename(file.path, destination)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def environment_configuration
|
|
103
|
+
source = @project.join('environment.conf')
|
|
104
|
+
content = source.exist? ? source.binread : ''
|
|
105
|
+
content = content.gsub(/^[ \t]*environment_timeout[ \t]*=[^\r\n]*/, 'environment_timeout = 0')
|
|
106
|
+
content += "\n" unless content.empty? || content.end_with?("\n")
|
|
107
|
+
content += "environment_timeout = 0\n" unless content.match?(/^environment_timeout = 0\r?$/)
|
|
108
|
+
content
|
|
109
|
+
end
|
|
110
|
+
|
|
91
111
|
def create_target(relative, destination)
|
|
92
112
|
raise Error, 'Managed environment destination is unsafe' if destination.symlink?
|
|
93
113
|
|
|
@@ -116,10 +116,7 @@ module Empeira
|
|
|
116
116
|
bindings = Network::ProxyBindings.new(context: @context, resources: resources, network: network, nodes: nodes)
|
|
117
117
|
# The workspace parent is private; Squid's mapped service user must read the bind mount.
|
|
118
118
|
rules_changed = write('proxy-rules.conf', bindings.configuration)
|
|
119
|
-
|
|
120
|
-
addresses = resources.slice(*allowed).values.filter_map do |resource|
|
|
121
|
-
resource&.dig('networks', network, 'IPAddress')
|
|
122
|
-
end
|
|
119
|
+
addresses = bindings.addresses
|
|
123
120
|
clients_changed = write('proxy-clients', "#{(['127.0.0.1/32'] + addresses.reject(&:empty?).sort).join("\n")}\n")
|
|
124
121
|
rules_changed || clients_changed
|
|
125
122
|
end
|
|
@@ -131,10 +128,6 @@ module Empeira
|
|
|
131
128
|
Infrastructure::Definition.fingerprint(content)
|
|
132
129
|
end
|
|
133
130
|
|
|
134
|
-
def proxy_allowed(nodes)
|
|
135
|
-
['server', *nodes.keys]
|
|
136
|
-
end
|
|
137
|
-
|
|
138
131
|
private
|
|
139
132
|
|
|
140
133
|
def corefile_content(upstreams:, routes:, bootstrap: false)
|
|
@@ -27,8 +27,7 @@ module Empeira
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def proxy?
|
|
30
|
-
|
|
31
|
-
config.dig('proxy', 'enabled') || (egress.is_a?(Hash) && egress['mode'] == 'proxy')
|
|
30
|
+
%w[nodes infrastructure].any? { |scope| config.dig('proxy', scope, 'enabled') }
|
|
32
31
|
end
|
|
33
32
|
|
|
34
33
|
def database?
|
|
@@ -63,7 +62,7 @@ module Empeira
|
|
|
63
62
|
def additional_services(common = {})
|
|
64
63
|
config.dig('containers', 'additional').each_with_index.to_h do |entry, index|
|
|
65
64
|
options = { image: image_reference(entry.fetch('image')), memory: 256,
|
|
66
|
-
environment:
|
|
65
|
+
environment: infrastructure_proxy_environment.merge(entry.fetch('environment', {})),
|
|
67
66
|
command: entry.fetch('command', []) }
|
|
68
67
|
location = "containers.additional[#{index}].configuration"
|
|
69
68
|
generated = additional_configurations.options(entry, mounts: common.fetch(:mounts, []), location: location)
|
|
@@ -175,9 +174,9 @@ module Empeira
|
|
|
175
174
|
end
|
|
176
175
|
|
|
177
176
|
def node_proxy_environment
|
|
178
|
-
return {} unless proxy
|
|
177
|
+
return {} unless config.dig('proxy', 'nodes', 'enabled')
|
|
179
178
|
|
|
180
|
-
proxy_environment_for("http://#{naming.hostname('proxy')}:3128")
|
|
179
|
+
proxy_environment_for("http://#{naming.hostname('proxy')}:3128", scope: 'nodes')
|
|
181
180
|
end
|
|
182
181
|
|
|
183
182
|
private
|
|
@@ -209,9 +208,10 @@ module Empeira
|
|
|
209
208
|
def database_services(common)
|
|
210
209
|
return {} unless database?
|
|
211
210
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
'puppetdb' => service('puppetdb', **
|
|
211
|
+
options = common.merge(environment: infrastructure_proxy_environment)
|
|
212
|
+
{ 'postgres' => service('postgres', **options, **postgres),
|
|
213
|
+
'puppetdb-backend' => service('puppetdb-backend', **options, **puppetdb),
|
|
214
|
+
'puppetdb' => service('puppetdb', **options, **puppetdb_relay(dns: common[:dns])) }
|
|
215
215
|
end
|
|
216
216
|
|
|
217
217
|
def server
|
|
@@ -271,15 +271,16 @@ module Empeira
|
|
|
271
271
|
reports: database? ? 'puppetdb' : 'log',
|
|
272
272
|
# Upstream entrypoints otherwise rewrite puppetdb.conf from their default URL.
|
|
273
273
|
puppetdb_server_urls: ''
|
|
274
|
-
).merge(
|
|
274
|
+
).merge(infrastructure_proxy_environment)
|
|
275
275
|
end
|
|
276
276
|
|
|
277
|
-
def
|
|
278
|
-
|
|
277
|
+
def infrastructure_proxy_environment
|
|
278
|
+
enabled = config.dig('proxy', 'infrastructure', 'enabled')
|
|
279
|
+
proxy_environment_for(enabled ? "http://#{naming.hostname('proxy')}:3128" : '', scope: 'infrastructure')
|
|
279
280
|
end
|
|
280
281
|
|
|
281
|
-
def proxy_environment_for(url)
|
|
282
|
-
bypass = Network::DirectEgress.destinations(config.dig('network', 'egress'))
|
|
282
|
+
def proxy_environment_for(url, scope:)
|
|
283
|
+
bypass = Network::DirectEgress.destinations(config.dig('network', 'egress', scope))
|
|
283
284
|
no_proxy = naming.no_proxy(bypass)
|
|
284
285
|
{ 'http_proxy' => url, 'https_proxy' => url, 'HTTP_PROXY' => url, 'HTTPS_PROXY' => url,
|
|
285
286
|
'all_proxy' => '', 'ALL_PROXY' => '', 'no_proxy' => no_proxy, 'NO_PROXY' => no_proxy }
|
|
@@ -289,7 +290,8 @@ module Empeira
|
|
|
289
290
|
{ image: image_reference(config.dig('images', 'postgres')), memory: 256,
|
|
290
291
|
env_file: files.secret_path,
|
|
291
292
|
configuration: files.configuration.fetch('database-setup.sql'),
|
|
292
|
-
environment:
|
|
293
|
+
environment: infrastructure_proxy_environment.merge('POSTGRES_USER' => 'postgres',
|
|
294
|
+
'POSTGRES_DB' => 'puppetdb'),
|
|
293
295
|
mounts: [storage('postgres-data', '/var/lib/postgresql/data'),
|
|
294
296
|
bind(files.path('database-setup.sql'), '/empeira/setup.sql')],
|
|
295
297
|
command: ['postgres', '-c', 'shared_buffers=64MB', '-c', 'max_connections=50'] }
|
|
@@ -24,7 +24,6 @@ module Empeira
|
|
|
24
24
|
module_function
|
|
25
25
|
|
|
26
26
|
def validate!(value, path = 'network.egress')
|
|
27
|
-
return legacy!(value, path) if value.is_a?(Hash)
|
|
28
27
|
unless value.is_a?(Array) && value.size <= MAX_HOSTS
|
|
29
28
|
raise ConfigurationError, "#{path} must be an array with at most #{MAX_HOSTS} destination entries"
|
|
30
29
|
end
|
|
@@ -33,12 +32,6 @@ module Empeira
|
|
|
33
32
|
raise ConfigurationError, "#{path} contains duplicate destinations" unless hosts.uniq.size == hosts.size
|
|
34
33
|
end
|
|
35
34
|
|
|
36
|
-
def legacy!(value, path)
|
|
37
|
-
return if value.keys == ['mode'] && %w[none proxy].include?(value['mode'])
|
|
38
|
-
|
|
39
|
-
raise ConfigurationError, "#{path} must be a host array (legacy mode accepts only none or proxy)"
|
|
40
|
-
end
|
|
41
|
-
|
|
42
35
|
def destinations(value)
|
|
43
36
|
return [] unless value.is_a?(Array)
|
|
44
37
|
|
|
@@ -22,7 +22,15 @@ module Empeira
|
|
|
22
22
|
{ 'version' => 1, 'subnet' => subnet, 'gateway' => address(subnet),
|
|
23
23
|
'dns' => address(subnet, 'dns'), 'resolvers' => resolvers,
|
|
24
24
|
'proxies' => %w[proxy bootstrap-proxy].map { |key| address(subnet, key) },
|
|
25
|
-
'blocked' => blocked_addresses(state), 'entries' => resolved
|
|
25
|
+
'blocked' => blocked_addresses(state), 'entries' => scoped_entries(resolved, subnet),
|
|
26
|
+
'redirects' => redirects }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.scoped_entries(resolved, subnet)
|
|
30
|
+
layout = Peer::Layout.new(subnet)
|
|
31
|
+
resolved.entries.map do |entry|
|
|
32
|
+
entry.except('scope').merge('sources' => layout.egress_sources(entry.fetch('scope')))
|
|
33
|
+
end
|
|
26
34
|
end
|
|
27
35
|
|
|
28
36
|
def self.blocked_addresses(state)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Empeira
|
|
4
4
|
module Network
|
|
5
5
|
class HostPolicy
|
|
6
|
-
def self.validate!(rules, path = 'proxy.rules')
|
|
6
|
+
def self.validate!(rules, path = 'proxy.nodes.rules')
|
|
7
7
|
raise ConfigurationError, "#{path} must be an array" unless rules.is_a?(Array)
|
|
8
8
|
|
|
9
9
|
rules.each_with_index { |rule, index| validate_rule!(rule, "#{path}.#{index}") }
|
|
@@ -28,6 +28,14 @@ module Empeira
|
|
|
28
28
|
"#{address(128)}/25"
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
def egress_sources(scope)
|
|
32
|
+
case scope
|
|
33
|
+
when 'nodes' then ["#{address(32)}/27", "#{address(64)}/26"]
|
|
34
|
+
when 'infrastructure' then [container_pool]
|
|
35
|
+
else raise Error, 'Unknown direct-egress scope'
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
31
39
|
def lease(nodes)
|
|
32
40
|
used = nodes.values.filter_map { |record| record.dig('peer', 'ip') }
|
|
33
41
|
ip = (32..95).map { |offset| address(offset) }.find { |candidate| !used.include?(candidate) }
|
|
@@ -12,22 +12,39 @@ module Empeira
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def configuration
|
|
15
|
-
entries =
|
|
16
|
-
rules(
|
|
15
|
+
entries = bindings.filter_map do |label, hostname, domains|
|
|
16
|
+
rules(label, address(hostname), domains)
|
|
17
17
|
end
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
"#{entries.join("\n")}\n"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def addresses
|
|
22
|
+
bindings.filter_map { |_, hostname, _| address(hostname) }.uniq
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
private
|
|
25
26
|
|
|
26
|
-
def
|
|
27
|
-
|
|
27
|
+
def bindings
|
|
28
|
+
infrastructure_bindings + node_bindings
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def infrastructure_bindings
|
|
32
|
+
scope = @config.fetch('infrastructure')
|
|
33
|
+
return [] unless scope.fetch('enabled')
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
names = %w[server postgres puppetdb-backend puppetdb] +
|
|
36
|
+
@context.configuration.dig('containers', 'additional').map { |service| service.fetch('name') }
|
|
37
|
+
names.map { |name| [name.tr('-', '_'), name, scope.fetch('allow')] }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def node_bindings
|
|
41
|
+
scope = @config.fetch('nodes')
|
|
42
|
+
return [] unless scope.fetch('enabled')
|
|
43
|
+
|
|
44
|
+
policy = HostPolicy.new(scope)
|
|
45
|
+
@nodes.sort.each_with_index.filter_map do |(hostname, record), index|
|
|
46
|
+
["node_#{index}", hostname, policy.resolve(hostname)] unless record['network_phase'] == 'bootstrap'
|
|
47
|
+
end
|
|
31
48
|
end
|
|
32
49
|
|
|
33
50
|
def address(name)
|
|
@@ -63,9 +63,9 @@ module Empeira
|
|
|
63
63
|
|
|
64
64
|
naming = Network::Naming.new
|
|
65
65
|
internal = naming.reserved(@context.configuration).flat_map { |name| naming.aliases(name) }
|
|
66
|
+
bypass = Network::DirectEgress.destinations(@context.configuration.dig('network', 'egress', 'nodes'))
|
|
66
67
|
{ 'url' => environment.fetch('http_proxy'), 'no_proxy' => environment.fetch('no_proxy'),
|
|
67
|
-
'direct' => ['localhost', '127.0.0.1', *internal,
|
|
68
|
-
*Network::DirectEgress.destinations(@context.configuration.dig('network', 'egress'))].uniq }
|
|
68
|
+
'direct' => ['localhost', '127.0.0.1', *internal, *bypass].uniq }
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
def execute(desired, previous)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module Empeira
|
|
4
4
|
module Runtime
|
|
5
5
|
class ServiceArguments
|
|
6
|
+
PROXY_VARIABLES = %w[http_proxy https_proxy all_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY no_proxy NO_PROXY].freeze
|
|
6
7
|
SCALARS = { 'dns' => '--dns', 'user' => '--user', 'entrypoint' => '--entrypoint',
|
|
7
8
|
'env_file' => '--env-file', 'shm_size' => '--shm-size', 'mac_address' => '--mac-address',
|
|
8
9
|
'ip' => '--ip', 'runtime_ip' => '--ip' }.freeze
|
|
@@ -20,7 +21,7 @@ module Empeira
|
|
|
20
21
|
'--network', @options.fetch('network'), '--memory', "#{@options.fetch('memory')}m",
|
|
21
22
|
'--cpus', @options.fetch('cpus', 1).to_s, '--no-healthcheck',
|
|
22
23
|
*init_options, *security_options, *pairs('--label', @definition.labels), *scalar_options,
|
|
23
|
-
*pairs('--env',
|
|
24
|
+
*pairs('--env', environment),
|
|
24
25
|
*pairs('--sysctl', @options.fetch('sysctls', {})),
|
|
25
26
|
*@options.fetch('mounts', []).flat_map { |mount| ['--mount', mount] },
|
|
26
27
|
*@options.fetch('ports', []).flat_map { |port| ['--publish', port] },
|
|
@@ -29,6 +30,11 @@ module Empeira
|
|
|
29
30
|
|
|
30
31
|
private
|
|
31
32
|
|
|
33
|
+
def environment
|
|
34
|
+
PROXY_VARIABLES.to_h { |key| [key, ''] }.merge(@options.fetch('environment', {}))
|
|
35
|
+
.merge(@options.fetch('runtime_environment', {}))
|
|
36
|
+
end
|
|
37
|
+
|
|
32
38
|
def security_options
|
|
33
39
|
values = { 'cap_drop' => '--cap-drop', 'cap_add' => '--cap-add', 'devices' => '--device',
|
|
34
40
|
'security_options' => '--security-opt' }
|
|
@@ -24,10 +24,10 @@ module Empeira
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def verify!(resource)
|
|
27
|
-
return if current(resource) == '0'
|
|
27
|
+
return if current(resource) == '0' && effective(resource) == '0'
|
|
28
28
|
|
|
29
29
|
raise Error, 'Puppetserver environment_timeout is not 0 after startup; the custom entrypoint or ' \
|
|
30
|
-
'server confdir overrides the managed setting; inspect the server runtime contract'
|
|
30
|
+
'server confdir/environment overrides the managed setting; inspect the server runtime contract'
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
private
|
|
@@ -36,6 +36,12 @@ module Empeira
|
|
|
36
36
|
execute(resource, [ControlPlane::Health::RUBY, READER, '--confdir', confdir]).stdout.strip
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
def effective(resource)
|
|
40
|
+
arguments = [PUPPET, 'config', 'print', 'environment_timeout', '--section', 'server',
|
|
41
|
+
'--confdir', confdir, '--environment', @plan.config.dig('server', 'environment')]
|
|
42
|
+
execute(resource, arguments).stdout.strip
|
|
43
|
+
end
|
|
44
|
+
|
|
39
45
|
def execute(resource, arguments)
|
|
40
46
|
result = @runtime.service_exec(resource, arguments)
|
|
41
47
|
return result if result.success?
|
|
@@ -111,17 +111,13 @@ module EmpeiraGateway
|
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
def direct_rules(plan, internal, external)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
"-d #{ip}/32 -p tcp --dport #{port} -j ACCEPT"
|
|
120
|
-
filter << reply(internal, external, destination: ip, port: port)
|
|
121
|
-
end
|
|
114
|
+
plan.fetch('entries').flat_map do |entry|
|
|
115
|
+
entry.fetch('sources').product(entry.fetch('addresses'), entry.fetch('ports')).flat_map do |source, ip, port|
|
|
116
|
+
["-A FORWARD -i #{internal} -o #{external} -s #{source} " \
|
|
117
|
+
"-d #{ip}/32 -p tcp --dport #{port} -j ACCEPT",
|
|
118
|
+
reply(internal, external, source: source, destination: ip, port: port)]
|
|
122
119
|
end
|
|
123
120
|
end
|
|
124
|
-
filter
|
|
125
121
|
end
|
|
126
122
|
|
|
127
123
|
def reply(internal, external, port:, source: nil, destination: nil, protocol: 'tcp')
|
|
@@ -145,7 +141,7 @@ module EmpeiraGateway
|
|
|
145
141
|
raise 'Invalid workspace subnet' unless subnet.ipv4? && subnet.private? && subnet.prefix == 24
|
|
146
142
|
|
|
147
143
|
validate_addresses(plan, subnet)
|
|
148
|
-
validate_entries(plan.fetch('entries'))
|
|
144
|
+
validate_entries(plan.fetch('entries'), subnet)
|
|
149
145
|
validate_redirects(plan.fetch('redirects', []), subnet)
|
|
150
146
|
end
|
|
151
147
|
|
|
@@ -197,14 +193,27 @@ module EmpeiraGateway
|
|
|
197
193
|
%w[resolvers proxies blocked].each { |key| plan.fetch(key, []).each { |ip| ipv4(ip) } }
|
|
198
194
|
end
|
|
199
195
|
|
|
200
|
-
def validate_entries(entries)
|
|
196
|
+
def validate_entries(entries, subnet)
|
|
201
197
|
entries.each do |entry|
|
|
198
|
+
validate_sources(entry.fetch('sources'), subnet)
|
|
202
199
|
entry.fetch('addresses').each { |ip| ipv4(ip) }
|
|
203
200
|
valid = entry.fetch('ports').all? { |port| port.is_a?(Integer) && port.between?(1, 65_535) }
|
|
204
201
|
raise 'Invalid gateway ports' unless valid
|
|
205
202
|
end
|
|
206
203
|
end
|
|
207
204
|
|
|
205
|
+
def validate_sources(sources, subnet)
|
|
206
|
+
valid = sources.is_a?(Array) && !sources.empty? && sources.all? { |source| valid_source?(source, subnet) }
|
|
207
|
+
raise 'Invalid direct-egress source ranges' unless valid
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def valid_source?(source, subnet)
|
|
211
|
+
range = IPAddr.new(source)
|
|
212
|
+
range.ipv4? && range.prefix > subnet.prefix && subnet.include?(range) && "#{range}/#{range.prefix}" == source
|
|
213
|
+
rescue IPAddr::InvalidAddressError, TypeError
|
|
214
|
+
false
|
|
215
|
+
end
|
|
216
|
+
|
|
208
217
|
def ipv4_interfaces
|
|
209
218
|
Socket.getifaddrs.select { |entry| entry.addr&.ipv4? && !entry.addr.ipv4_loopback? }
|
|
210
219
|
end
|
|
@@ -301,6 +310,7 @@ module EmpeiraGateway
|
|
|
301
310
|
when 'redirects-capability'
|
|
302
311
|
command('conntrack', '--version')
|
|
303
312
|
puts 'tcp-udp-redirects-v2'
|
|
313
|
+
when 'scoped-egress-capability' then puts 'scoped-egress-v1'
|
|
304
314
|
when 'lockdown'
|
|
305
315
|
restore(lockdown)
|
|
306
316
|
when 'apply' then apply(arguments.fetch(1))
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: empeira
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.0.pre.alpha.
|
|
4
|
+
version: 0.1.0.pre.alpha.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Christian Meißner (c8m6)
|
|
@@ -304,7 +304,7 @@ files:
|
|
|
304
304
|
- resources/relay/server_start.sh
|
|
305
305
|
homepage: https://github.com/c8m6/empeira
|
|
306
306
|
licenses:
|
|
307
|
-
- AGPL-3.0-
|
|
307
|
+
- AGPL-3.0-or-later
|
|
308
308
|
metadata:
|
|
309
309
|
rubygems_mfa_required: 'true'
|
|
310
310
|
rdoc_options: []
|