feat(deploy): collab-relay production configs, CN docker firewall, HSM locator
Add production deployment configs for the collab relay and locator: region-split compose (CN/global), direct nginx gateways and location maps, the CN docker-user firewall install/verify/validate tooling and systemd unit, and an SoftHSM-backed locator variant. Extend the collab security/deployment boundary checks to cover the new artifacts. The CN application host address is a placeholder (10.0.0.10); substitute the real private address at deploy time.
This commit is contained in:
parent
fbcc1d0dc6
commit
a39406c12b
10
.github/workflows/collab-security.yml
vendored
10
.github/workflows/collab-security.yml
vendored
|
|
@ -16,6 +16,7 @@ on:
|
|||
- 'crates/op-collab-relay-server/**'
|
||||
- 'crates/op-collab-relay-control-plane/**'
|
||||
- 'crates/op-collab-policy-file/**'
|
||||
- 'crates/op-collab-relay-locator-hsm/**'
|
||||
- 'crates/op-collab-relay-locator-server/**'
|
||||
- 'crates/op-auth-bridge/**'
|
||||
- 'crates/op-util/**'
|
||||
|
|
@ -29,6 +30,7 @@ on:
|
|||
- 'deploy/collab-relay/**'
|
||||
- 'deploy/collab-relay-edge/**'
|
||||
- 'deploy/collab-relay-locator/**'
|
||||
- 'deploy/collab-relay-locator-hsm/**'
|
||||
- 'deploy/collab-relay-locator-edge/**'
|
||||
- 'docs/security/p2p-collaboration-threat-model.md'
|
||||
- 'docs/testing/p2p-collaboration-platform-acceptance.md'
|
||||
|
|
@ -57,6 +59,7 @@ on:
|
|||
- 'crates/op-collab-relay-server/**'
|
||||
- 'crates/op-collab-relay-control-plane/**'
|
||||
- 'crates/op-collab-policy-file/**'
|
||||
- 'crates/op-collab-relay-locator-hsm/**'
|
||||
- 'crates/op-collab-relay-locator-server/**'
|
||||
- 'crates/op-auth-bridge/**'
|
||||
- 'crates/op-util/**'
|
||||
|
|
@ -70,6 +73,7 @@ on:
|
|||
- 'deploy/collab-relay/**'
|
||||
- 'deploy/collab-relay-edge/**'
|
||||
- 'deploy/collab-relay-locator/**'
|
||||
- 'deploy/collab-relay-locator-hsm/**'
|
||||
- 'deploy/collab-relay-locator-edge/**'
|
||||
- 'docs/security/p2p-collaboration-threat-model.md'
|
||||
- 'docs/testing/p2p-collaboration-platform-acceptance.md'
|
||||
|
|
@ -146,8 +150,14 @@ jobs:
|
|||
cargo test --locked -p op-collab-relay-server
|
||||
cargo test --locked -p op-collab-relay-control-plane
|
||||
cargo test --locked -p op-collab-policy-file
|
||||
cargo test --locked -p op-collab-relay-locator-hsm
|
||||
cargo test --locked -p op-collab-relay-locator-server
|
||||
|
||||
- name: Test the locator signer against SoftHSM
|
||||
run: |
|
||||
docker build --target test \
|
||||
-f deploy/collab-relay-locator-hsm/Dockerfile .
|
||||
|
||||
- name: Test production/test trust-root isolation
|
||||
run: |
|
||||
cargo test --locked -p op-auth-bridge --features test-issuer \
|
||||
|
|
|
|||
41
deploy/collab-relay-locator-hsm/Dockerfile
Normal file
41
deploy/collab-relay-locator-hsm/Dockerfile
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# syntax=docker/dockerfile:1.7
|
||||
ARG DOCKER_REGISTRY=docker.io
|
||||
|
||||
FROM ${DOCKER_REGISTRY}/library/rust:1.94-bookworm@sha256:6ae102bdbf528294bc79ad6e1fae682f6f7c2a6e6621506ba959f9685b308a55 AS build
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN cargo build --locked --release -p op-collab-relay-locator-hsm
|
||||
|
||||
FROM build AS test
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends softhsm2 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
ENV OPENPENCIL_SOFTHSM2_MODULE=/usr/lib/softhsm/libsofthsm2.so
|
||||
RUN cargo test --locked -p op-collab-relay-locator-hsm --test softhsm -- --nocapture
|
||||
|
||||
FROM ${DOCKER_REGISTRY}/library/debian:bookworm-20260713-slim@sha256:7b140f374b289a7c2befc338f42ebe6441b7ea838a042bbd5acbfca6ec875818
|
||||
|
||||
ARG LOCATOR_UID=65532
|
||||
ARG SIGNER_UID=65533
|
||||
ARG SHARED_GID=65532
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends softhsm2 && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
groupadd --gid "${SHARED_GID}" openpencil-locator && \
|
||||
useradd --uid "${SIGNER_UID}" --gid "${SHARED_GID}" --no-create-home \
|
||||
--home-dir /nonexistent --shell /usr/sbin/nologin openpencil-locator-hsm && \
|
||||
test "${LOCATOR_UID}" != "${SIGNER_UID}" && \
|
||||
install -d -o "${SIGNER_UID}" -g "${SHARED_GID}" -m 0700 /var/lib/openpencil-softhsm/tokens
|
||||
|
||||
COPY --from=build /src/target/release/op-collab-relay-locator-hsm /usr/local/bin/op-collab-relay-locator-hsm
|
||||
COPY --chmod=0444 deploy/collab-relay-locator-hsm/softhsm2.conf /etc/openpencil-softhsm2.conf
|
||||
|
||||
ENV SOFTHSM2_CONF=/etc/openpencil-softhsm2.conf
|
||||
USER ${SIGNER_UID}:${SHARED_GID}
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
||||
CMD ["/usr/local/bin/op-collab-relay-locator-hsm", "check", "--config", "/run/openpencil-config/locator-hsm.json"]
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/op-collab-relay-locator-hsm"]
|
||||
CMD ["serve", "--config", "/run/openpencil-config/locator-hsm.json"]
|
||||
185
deploy/collab-relay-locator-hsm/README.md
Normal file
185
deploy/collab-relay-locator-hsm/README.md
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
# OpenPencil locator SoftHSM signer
|
||||
|
||||
This package is the production OPLS Unix-socket adapter for the relay locator.
|
||||
It is a separate non-root container with no network namespace. Only this
|
||||
container receives the SoftHSM token directory and user PIN. The locator sees
|
||||
only `signer.sock` read-only; it cannot read the token store, PIN, or private
|
||||
key.
|
||||
|
||||
The adapter accepts exactly one 339-byte OPLS v1 request per connection,
|
||||
authenticates the locator UID/GID with Unix peer credentials, requires the
|
||||
configured active key id, and rejects bytes that are not a canonical locator
|
||||
for the configured region. This confines the dedicated Ed25519 key to the
|
||||
locator protocol without changing the existing 268-byte signature input. Each
|
||||
accepted request uses single-part PKCS#11 EdDSA signing and the returned
|
||||
signature is verified against the token public key before release.
|
||||
|
||||
## Key profile
|
||||
|
||||
Configuration contains exactly two public entries: active and next. Object ids
|
||||
and labels must be unique. Provisioning refuses to overwrite either. Startup
|
||||
requires one public and one private object for each entry and verifies, among
|
||||
other attributes:
|
||||
|
||||
- `CKK_EC_EDWARDS`, Ed25519 parameters, and locally generated key material;
|
||||
- private `CKA_SENSITIVE=true`, `CKA_EXTRACTABLE=false`,
|
||||
`CKA_ALWAYS_SENSITIVE=true`, and `CKA_NEVER_EXTRACTABLE=true`;
|
||||
- private sign-only and public verify-only use with the Ed25519 key type;
|
||||
- token persistence, exact labels/object ids, and no duplicate pair objects.
|
||||
|
||||
This deployment profile intentionally limits public key ids and token labels
|
||||
to ASCII letters, digits, `-`, `_`, and `.`. Migrate any older printable wire
|
||||
ids containing other punctuation before using this adapter.
|
||||
|
||||
Neither `initialize` nor `provision` exports private bytes. `public` emits the
|
||||
two public Ed25519 records accepted by the relay key-file parser. The signed
|
||||
desktop bootstrap uses the same base64url public value under its `x` field.
|
||||
|
||||
## Host preparation
|
||||
|
||||
Use separate directories, token labels, key ids, and object ids in CN and
|
||||
Global. Concrete public hosts and generated key ids belong in the private
|
||||
deployment inventory, not in this repository. The numeric identities used by
|
||||
the checked-in images are locator UID/GID `65532:65532` and signer
|
||||
`65533:65532`.
|
||||
|
||||
Prepare external host paths on each regional Linux host:
|
||||
|
||||
```sh
|
||||
sudo install -d -o 65533 -g 65532 -m 0700 /secure/openpencil/locator-hsm/tokens
|
||||
sudo install -o 65533 -g 65532 -m 0400 /dev/null /secure/openpencil/locator-hsm/user-pin
|
||||
sudo install -o 65533 -g 65532 -m 0400 /dev/null /secure/openpencil/locator-hsm/so-pin
|
||||
sudo install -o root -g 65532 -m 0440 \
|
||||
deploy/collab-relay-locator-hsm/config.example.json \
|
||||
/secure/openpencil/locator-hsm/config.json
|
||||
sudo install -o root -g root -m 0644 \
|
||||
deploy/collab-relay-locator-hsm/openpencil-locator-hsm.conf \
|
||||
/etc/tmpfiles.d/openpencil-locator-hsm.conf
|
||||
sudo systemd-tmpfiles --create /etc/tmpfiles.d/openpencil-locator-hsm.conf
|
||||
```
|
||||
|
||||
Replace the example region, token label, active/next public key ids, and unique
|
||||
object ids. Populate both PIN files through the operator secret channel; do not
|
||||
put a PIN on a command line, in Compose environment, or in shell history.
|
||||
Docker file-backed mounts preserve host ownership rather than secret `uid` or
|
||||
`mode` declarations, so verify these numeric modes on the target host.
|
||||
The checked-in `tmpfiles.d` rule recreates the volatile `/run` socket directory
|
||||
as `root:65532` mode `0770` after every boot. Install it before Compose; never
|
||||
let Docker auto-create a missing bind source as `root:root` mode `0755`.
|
||||
|
||||
Set the Compose inputs:
|
||||
|
||||
```sh
|
||||
export OPENPENCIL_COLLAB_HSM_CONFIG_HOST_FILE=/secure/openpencil/locator-hsm/config.json
|
||||
export OPENPENCIL_COLLAB_HSM_PIN_HOST_FILE=/secure/openpencil/locator-hsm/user-pin
|
||||
export OPENPENCIL_COLLAB_HSM_TOKEN_HOST_DIR=/secure/openpencil/locator-hsm/tokens
|
||||
export OPENPENCIL_COLLAB_HSM_SOCKET_HOST_DIR=/run/openpencil/locator-hsm
|
||||
```
|
||||
|
||||
## Initialize and provision
|
||||
|
||||
Build the image, then initialize the empty regional token. The SO PIN is an
|
||||
extra one-shot mount and is not part of the running service:
|
||||
|
||||
```sh
|
||||
docker compose \
|
||||
-f deploy/collab-relay-locator-hsm/compose.yaml \
|
||||
build locator-hsm
|
||||
|
||||
docker compose \
|
||||
-f deploy/collab-relay-locator-hsm/compose.yaml \
|
||||
run --rm \
|
||||
-v /secure/openpencil/locator-hsm/so-pin:/run/secrets/locator-hsm-so-pin:ro \
|
||||
locator-hsm initialize --config /run/openpencil-config/locator-hsm.json \
|
||||
--so-pin-file /run/secrets/locator-hsm-so-pin
|
||||
```
|
||||
|
||||
Provision the two configured key ids one at a time. This makes a partial
|
||||
failure explicit and never silently reuses or replaces an existing object:
|
||||
|
||||
```sh
|
||||
docker compose -f deploy/collab-relay-locator-hsm/compose.yaml \
|
||||
run --rm locator-hsm provision \
|
||||
--config /run/openpencil-config/locator-hsm.json --kid replace-with-active-public-kid
|
||||
|
||||
docker compose -f deploy/collab-relay-locator-hsm/compose.yaml \
|
||||
run --rm locator-hsm provision \
|
||||
--config /run/openpencil-config/locator-hsm.json --kid replace-with-next-public-kid
|
||||
```
|
||||
|
||||
Export public records to a candidate file, validate it, then distribute that
|
||||
same active+next set to the relay and signed desktop bootstrap before starting
|
||||
issuance:
|
||||
|
||||
```sh
|
||||
docker compose -f deploy/collab-relay-locator-hsm/compose.yaml \
|
||||
run --rm locator-hsm public \
|
||||
--config /run/openpencil-config/locator-hsm.json > /secure/public/locator-keys.candidate.json
|
||||
```
|
||||
|
||||
Remove the SO PIN mount/file from routine operations after its recovery copy
|
||||
has been placed in the operator-controlled secret system.
|
||||
|
||||
## Start, readiness, and rotation
|
||||
|
||||
Start the signer and locator together only after the public policy and key
|
||||
files are ready:
|
||||
|
||||
```sh
|
||||
export OPENPENCIL_COLLAB_POLICY_HOST_FILE=/secure/public/collab-policy.json
|
||||
export OPENPENCIL_COLLAB_LOCATOR_HSM_KEY_ID=replace-with-active-public-kid
|
||||
export OPENPENCIL_COLLAB_LOCATOR_HSM_PEER_UID=65533
|
||||
export OPENPENCIL_COLLAB_LOCATOR_HSM_PEER_GID=65532
|
||||
docker compose \
|
||||
-f deploy/collab-relay-locator-hsm/compose.yaml \
|
||||
-f deploy/collab-relay-locator/compose.yaml \
|
||||
-f deploy/collab-relay-locator/compose.production.global.yaml \
|
||||
-f deploy/collab-relay-locator/compose.hsm.yaml \
|
||||
up --build -d
|
||||
```
|
||||
|
||||
That command is for Global. On CN, replace only the regional overlay with
|
||||
`compose.production.cn.yaml`. These overlays hard-code the matching home
|
||||
region and host bind; there is no host-bind variable to override.
|
||||
|
||||
Startup and `check` validate both key pairs and perform a locator-shaped HSM
|
||||
sign-and-software-verify canary with each private key. The container health
|
||||
check additionally requires the Unix socket and a fresh heartbeat written by
|
||||
the main serving loop. The locator waits for a healthy signer.
|
||||
|
||||
For rotation from A(active)+B(next), first publish A+B everywhere. Prepare a
|
||||
candidate config with B(active)+C(next), mount it into a one-shot signer,
|
||||
provision C, and export the B+C candidate. Before promoting, merge the previous
|
||||
and candidate public files into an A+B+C overlap verifier bundle; the relay
|
||||
accepts up to 64 pinned keys. For example:
|
||||
|
||||
```sh
|
||||
jq -s '{version: 1, keys: ([.[].keys[]] | unique_by(.kid))}' \
|
||||
/secure/public/locator-keys.current.json \
|
||||
/secure/public/locator-keys.candidate.json \
|
||||
> /secure/public/locator-keys.overlap.json
|
||||
```
|
||||
|
||||
Distribute A+B+C to the relay and signed desktop bootstrap, atomically install
|
||||
the B+C signer config, and restart signer plus locator. Retain A in verifier
|
||||
bundles for the maximum locator lifetime and rollback window; only then publish
|
||||
B+C alone. The signer's `public` command emits its configured pair and does not
|
||||
perform this verifier-history merge. This adapter intentionally has no
|
||||
key-destruction command; retirement is a separate audited token-admin
|
||||
operation.
|
||||
|
||||
For every start or promotion,
|
||||
`OPENPENCIL_COLLAB_LOCATOR_HSM_KEY_ID` on the locator must exactly equal the
|
||||
signer config's `active_kid`. Exercise one authenticated locator issuance and
|
||||
verify the returned signature against the newly distributed public key before
|
||||
opening traffic.
|
||||
|
||||
## Verification
|
||||
|
||||
The Docker test target runs the integration test against a real SoftHSM module:
|
||||
|
||||
```sh
|
||||
docker build --target test \
|
||||
-f deploy/collab-relay-locator-hsm/Dockerfile .
|
||||
docker build -f deploy/collab-relay-locator-hsm/Dockerfile .
|
||||
```
|
||||
33
deploy/collab-relay-locator-hsm/compose.yaml
Normal file
33
deploy/collab-relay-locator-hsm/compose.yaml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
services:
|
||||
locator-hsm:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: deploy/collab-relay-locator-hsm/Dockerfile
|
||||
restart: unless-stopped
|
||||
network_mode: none
|
||||
environment:
|
||||
OPENPENCIL_LOCATOR_HSM_LOG: ${OPENPENCIL_LOCATOR_HSM_LOG:-info}
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ${OPENPENCIL_COLLAB_HSM_CONFIG_HOST_FILE:?set absolute public signer config file}
|
||||
target: /run/openpencil-config/locator-hsm.json
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ${OPENPENCIL_COLLAB_HSM_PIN_HOST_FILE:?set absolute signer PIN file}
|
||||
target: /run/secrets/locator-hsm-pin
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ${OPENPENCIL_COLLAB_HSM_TOKEN_HOST_DIR:?set absolute SoftHSM token directory}
|
||||
target: /var/lib/openpencil-softhsm/tokens
|
||||
- type: bind
|
||||
source: ${OPENPENCIL_COLLAB_HSM_SOCKET_HOST_DIR:?set absolute HSM socket directory}
|
||||
target: /run/openpencil-hsm
|
||||
read_only: true
|
||||
cap_drop:
|
||||
- ALL
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
pids_limit: 64
|
||||
mem_limit: 128m
|
||||
tmpfs:
|
||||
- /tmp:size=8m,mode=1700,noexec,nosuid,nodev
|
||||
22
deploy/collab-relay-locator-hsm/config.example.json
Normal file
22
deploy/collab-relay-locator-hsm/config.example.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"version": 1,
|
||||
"region": "global",
|
||||
"socket_path": "/run/openpencil-hsm/signer.sock",
|
||||
"expected_client_uid": 65532,
|
||||
"expected_client_gid": 65532,
|
||||
"pkcs11_module_path": "/usr/lib/softhsm/libsofthsm2.so",
|
||||
"token_label": "openpencil-locator-global",
|
||||
"pin_file": "/run/secrets/locator-hsm-pin",
|
||||
"active_kid": "replace-with-active-public-kid",
|
||||
"keys": [
|
||||
{
|
||||
"kid": "replace-with-active-public-kid",
|
||||
"object_id_hex": "7101"
|
||||
},
|
||||
{
|
||||
"kid": "replace-with-next-public-kid",
|
||||
"object_id_hex": "7102"
|
||||
}
|
||||
],
|
||||
"request_timeout_ms": 2000
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# systemd-tmpfiles rule for the volatile shared Unix-socket directory.
|
||||
d /run/openpencil/locator-hsm 0770 root 65532 -
|
||||
4
deploy/collab-relay-locator-hsm/softhsm2.conf
Normal file
4
deploy/collab-relay-locator-hsm/softhsm2.conf
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
directories.tokendir = /var/lib/openpencil-softhsm/tokens/
|
||||
objectstore.backend = file
|
||||
log.level = ERROR
|
||||
slots.removable = false
|
||||
|
|
@ -13,8 +13,13 @@ network. Terminate public TLS at a trusted ingress, include
|
|||
`nginx-http-limits.conf` once in its `http` block, and include
|
||||
`nginx-location.conf` in the exact-host TLS server. The checked-in zones
|
||||
enforce per-source request and connection limits before the process-wide
|
||||
aggregate limiter. If a load balancer sits in front, restore client addresses
|
||||
only from its fixed trusted addresses; never trust a public
|
||||
aggregate limiter. Docker NAT collapses clients crossing the host-published
|
||||
port into one locator-visible peer address. The immutable regional overlays
|
||||
therefore set `OPENPENCIL_COLLAB_LOCATOR_CLIENT_RATE_PER_SECOND=100`, equal to
|
||||
the global locator ceiling, while the common Compose file sets no production
|
||||
override. Host Nginx is the trusted real-source boundary and must retain the
|
||||
checked-in per-source zones. If a load balancer sits in front, restore client
|
||||
addresses only from its fixed trusted addresses; never trust a public
|
||||
`X-Forwarded-For`. The ingress checks the raw `$request_uri`, so queries and
|
||||
percent-encoded path aliases cannot be normalized into the route, and it
|
||||
requires a real client `Host` header instead of Nginx's fallback host. Only
|
||||
|
|
@ -24,7 +29,6 @@ exposes no configuration or dependency status.
|
|||
|
||||
The server enforces:
|
||||
|
||||
- at most 32 headers and a 16 KiB HTTP header buffer;
|
||||
- at most 32 headers and a 64 KiB HTTP header buffer, enough for the bounded
|
||||
48 KiB collaboration-ticket envelope;
|
||||
- a five-second header timeout and body timeout;
|
||||
|
|
@ -68,9 +72,10 @@ response (70 bytes, then EOF)
|
|||
|
||||
Status `0` is success and `1` is rejection. Unknown status, zero signatures,
|
||||
truncation, trailing bytes, timeout, peer mismatch, and socket replacement all
|
||||
fail closed. The HSM-side adapter is deployment-owned; it must authorize the
|
||||
configured key id and keep the Ed25519 private key inside the HSM/KMS boundary.
|
||||
There is intentionally no software production signer in this repository.
|
||||
fail closed. The isolated SoftHSM adapter in
|
||||
[`deploy/collab-relay-locator-hsm`](../collab-relay-locator-hsm/README.md)
|
||||
implements this boundary. It authorizes only the configured active key id and
|
||||
keeps the Ed25519 private key and token PIN outside this container.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
|
@ -93,9 +98,19 @@ The compose wrapper instead requires these host mount inputs:
|
|||
- `OPENPENCIL_COLLAB_HSM_SOCKET_HOST_DIR`, an absolute host directory
|
||||
containing `signer.sock`.
|
||||
|
||||
On Unix, the public signed-policy file may be owned either by the locator's
|
||||
effective UID or by root. It must be a regular non-symlink file and must not be
|
||||
group- or world-writable. Root-owned mode `0440` (when the container identity
|
||||
has group read) or `0444` is accepted because the policy contains public
|
||||
verification material; writable modes fail closed.
|
||||
|
||||
The compose file mounts policy and socket inputs read-only, drops all Linux
|
||||
capabilities, uses the distroless non-root identity, enables a read-only root
|
||||
filesystem, and does not publish port 8092 to the host.
|
||||
filesystem, and publishes no host port. The immutable Global production
|
||||
overlay sets `home_region=global` and binds `127.0.0.1:8092`; the immutable CN
|
||||
overlay sets `home_region=cn` and binds `10.0.0.10:8092`. There is no
|
||||
host-bind variable or wildcard default. Restrict the CN service-host firewall
|
||||
to the front gateway.
|
||||
|
||||
Both Dockerfile base images are pinned by multi-architecture manifest digest,
|
||||
verified from their upstream registries on 2026-07-29. Base-image updates must
|
||||
|
|
@ -109,7 +124,11 @@ Optional runtime settings are
|
|||
`OPENPENCIL_COLLAB_LOCATOR_POLICY_MAX_AGE_SECONDS` (`1..=3600`),
|
||||
`OPENPENCIL_COLLAB_LOCATOR_HSM_TIMEOUT_MS` (`50..=5000`),
|
||||
`OPENPENCIL_COLLAB_LOCATOR_MAX_AUTH_IN_FLIGHT` (`1..=256`), and
|
||||
`OPENPENCIL_COLLAB_LOCATOR_RATE_PER_SECOND` (`1..=10000`). The optional
|
||||
`OPENPENCIL_COLLAB_LOCATOR_RATE_PER_SECOND` (`1..=10000`).
|
||||
`OPENPENCIL_COLLAB_LOCATOR_CLIENT_RATE_PER_SECOND` (`1..=10000`) is also
|
||||
optional outside the immutable regional production overlays; those overlays
|
||||
fix it at `100` because Docker NAT removes usable client identity at the
|
||||
locator socket. The optional
|
||||
`OPENPENCIL_COLLAB_LOCATOR_LOG_LEVEL` accepts only `error`, `warn`, `info`, or
|
||||
`debug` and is scoped to this crate; arbitrary dependency trace filters are
|
||||
not accepted, so HTTP headers cannot be enabled through logging configuration.
|
||||
|
|
@ -120,14 +139,106 @@ Validate the fail-closed deployment invariants and resolved compose model:
|
|||
deploy/collab-relay-locator/validate.sh
|
||||
```
|
||||
|
||||
Start after provisioning the public policy file and external signer socket:
|
||||
Start after provisioning the public policy file and signer token:
|
||||
|
||||
```sh
|
||||
docker compose -f deploy/collab-relay-locator/compose.yaml up --build
|
||||
export OPENPENCIL_COLLAB_POLICY_HOST_FILE=/secure/public/collab-policy.json
|
||||
export OPENPENCIL_COLLAB_LOCATOR_HSM_KEY_ID=replace-with-active-public-kid
|
||||
export OPENPENCIL_COLLAB_LOCATOR_HSM_PEER_UID=65533
|
||||
export OPENPENCIL_COLLAB_LOCATOR_HSM_PEER_GID=65532
|
||||
docker compose \
|
||||
-f deploy/collab-relay-locator-hsm/compose.yaml \
|
||||
-f deploy/collab-relay-locator/compose.yaml \
|
||||
-f deploy/collab-relay-locator/compose.production.global.yaml \
|
||||
-f deploy/collab-relay-locator/compose.hsm.yaml \
|
||||
up --build
|
||||
```
|
||||
|
||||
That command is for Global. On CN, replace only the regional overlay with
|
||||
`compose.production.cn.yaml`. The base file alone supplies neither a home
|
||||
region nor a host port, and the two regional overlays must never be combined.
|
||||
|
||||
For a CN home session, deploy this service and the blind relay in the CN
|
||||
region. Overseas owners/guests call the CN locator/relay endpoints over
|
||||
TLS/WSS. A global edge may proxy the opaque TLS/WSS flows to the CN home
|
||||
services, but it must not mint a different region or replace signed endpoint
|
||||
policy.
|
||||
region. Any user who selects CN calls the CN application's `/v1/locator` and
|
||||
`/v1/tunnel` paths directly over TLS/WSS. Global home sessions use the same
|
||||
two paths on the Global application host. Concrete hosts stay in private
|
||||
deployment inventory and signed bootstrap metadata; this direct topology does
|
||||
not require a Global L4 edge. The two Compose projects have independent Docker
|
||||
networks: Global host Nginx reaches their loopback-published ports, while the
|
||||
CN front gateway reaches both ports on `10.0.0.10`. Host Nginx must not use
|
||||
the Compose-only `locator` or `relay` DNS names.
|
||||
|
||||
## Persistent CN service-host firewall
|
||||
|
||||
The CN host-published relay and locator ports require the checked-in
|
||||
`DOCKER-USER` boundary. Its private inputs are deployment inventory and must
|
||||
not be committed. Copy `cn-docker-user-firewall.env.example` to a root-owned,
|
||||
non-symlink inventory path under root-owned, non-writable directories, replace
|
||||
the RFC 5737 examples, and keep the file exactly `root:root` mode `0600`:
|
||||
|
||||
```sh
|
||||
sudo install -d -o root -g root -m 0700 /etc/openpencil/inventory
|
||||
sudo install -o root -g root -m 0600 \
|
||||
deploy/collab-relay-locator/cn-docker-user-firewall.env.example \
|
||||
/etc/openpencil/inventory/collab-cn-firewall.env
|
||||
sudoedit /etc/openpencil/inventory/collab-cn-firewall.env
|
||||
sudo deploy/collab-relay-locator/install-cn-docker-user-firewall.sh \
|
||||
/etc/openpencil/inventory/collab-cn-firewall.env
|
||||
```
|
||||
|
||||
The inventory accepts exactly three unquoted data values: the existing Linux
|
||||
ingress interface, the one gateway source IPv4, and the service host's
|
||||
original-destination IPv4. Hostnames, CIDRs, shell syntax, leading-zero IPv4
|
||||
octets, unknown keys, duplicates, symlinks, non-root ownership, and permissive
|
||||
modes fail closed. The inventory is parsed as data and is never sourced.
|
||||
Installation also requires that the service IPv4 exactly equals the single
|
||||
numeric `host_ip` in both immutable CN production overlays: relay TCP `8091`
|
||||
and locator TCP `8092`. The derived value is retained separately as a
|
||||
root-owned mode `0400` deployment binding. Every apply and verify then requires
|
||||
that exact IPv4 to be assigned exactly once on the configured live interface.
|
||||
|
||||
The installer atomically replaces `/etc/openpencil/collab-cn-firewall.env`,
|
||||
installs immutable root-owned helpers, enables a pre-Docker systemd gate, and
|
||||
adds a Docker drop-in that reapplies and verifies the rules after every daemon
|
||||
start. When Docker is already active, installation calls the immutable apply
|
||||
and verify helpers directly and proves its active state and main PID did not
|
||||
change; it does not start, stop, or restart either unit. During the next
|
||||
maintenance restart, require both units to remain healthy:
|
||||
|
||||
```sh
|
||||
sudo systemctl restart docker.service
|
||||
sudo systemctl status --no-pager \
|
||||
openpencil-collab-cn-firewall.service docker.service
|
||||
sudo /usr/local/libexec/openpencil-collab-cn-firewall/verify-cn-docker-user-firewall.sh
|
||||
```
|
||||
|
||||
The atomic `iptables-restore --noflush` transaction places one dedicated chain
|
||||
at the start of `DOCKER-USER`. For each original-destination TCP port `8091`
|
||||
and `8092`, it returns only traffic arriving on the configured interface from
|
||||
the configured source IPv4, then drops every other source to that exact
|
||||
original destination. The final rule returns all unrelated traffic, so ports
|
||||
`18080`, `18770`, SSH, and every unrelated host rule remain authoritative and
|
||||
untouched. The read-only verifier checks the exact rules, their order and
|
||||
count from one filter-table snapshot, the first-position jumps, the strict
|
||||
inventory, the immutable Compose binding, and the live interface assignment
|
||||
without changing kernel state. Apply refuses to flush a pre-existing chain of
|
||||
the managed name unless the canonical anchor or exact prior five-rule shape
|
||||
proves ownership; any foreign jump or goto reference from `FORWARD`, `INPUT`,
|
||||
or another custom chain also fails closed before mutation.
|
||||
|
||||
This boundary requires Docker's iptables-compatible `FORWARD` to
|
||||
`DOCKER-USER` hook. A Docker native-nftables configuration that omits that
|
||||
hook, a missing interface, or another firewall manager rewriting the managed
|
||||
chain makes the post-start verifier fail and therefore makes Docker activation
|
||||
fail. Resolve that host integration before starting the CN Compose projects;
|
||||
do not bypass the unit or weaken the verifier. This package assumes exclusive
|
||||
ownership of its dedicated chain. A privileged firewall reload after Docker is
|
||||
already active is outside the lifecycle hooks and must either invoke the apply
|
||||
and verify helpers or be prohibited by host policy.
|
||||
|
||||
After starting both CN Compose projects, run the read-only verifier again,
|
||||
inspect the managed-chain counters, and probe both paths once from the approved
|
||||
gateway and once from a disallowed source. This live acceptance check proves
|
||||
that the host is actually publishing the reviewed immutable overlays; the
|
||||
installer deliberately does not start containers or infer permission to
|
||||
deploy them.
|
||||
|
|
|
|||
182
deploy/collab-relay-locator/apply-cn-docker-user-firewall.sh
Executable file
182
deploy/collab-relay-locator/apply-cn-docker-user-firewall.sh
Executable file
|
|
@ -0,0 +1,182 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
LC_ALL=C
|
||||
PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
||||
export LC_ALL PATH
|
||||
|
||||
runtime_dir=/usr/local/libexec/openpencil-collab-cn-firewall
|
||||
config_file=/etc/openpencil/collab-cn-firewall.env
|
||||
binding_file=/etc/openpencil/collab-cn-firewall-compose.env
|
||||
chain_name=OPENPENCIL-CN-INGRESS
|
||||
anchor_comment=openpencil-cn-ingress-v1
|
||||
allow_relay_comment=openpencil-cn-allow-8091-v1
|
||||
drop_relay_comment=openpencil-cn-drop-8091-v1
|
||||
allow_locator_comment=openpencil-cn-allow-8092-v1
|
||||
drop_locator_comment=openpencil-cn-drop-8092-v1
|
||||
fallthrough_comment=openpencil-cn-fallthrough-v1
|
||||
|
||||
if [ "$#" -ne 0 ]; then
|
||||
echo "usage: $0" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "CN firewall: applying host rules requires root" >&2
|
||||
exit 1
|
||||
fi
|
||||
for command_name in stat ip iptables iptables-save iptables-restore \
|
||||
mktemp chmod flock awk
|
||||
do
|
||||
command -v "$command_name" >/dev/null 2>&1 || {
|
||||
echo "CN firewall: required command is unavailable: $command_name" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
common_file="$script_dir/cn-docker-user-firewall-common.sh"
|
||||
reference_checker="$script_dir/check-cn-docker-user-firewall-references.awk"
|
||||
address_checker="$script_dir/check-cn-docker-user-firewall-interface-address.awk"
|
||||
managed_chain_checker="$script_dir/check-cn-docker-user-firewall-managed-chain.awk"
|
||||
if [ "$script_dir" != "$runtime_dir" ] || [ -L "$script_dir" ] ||
|
||||
[ "$(stat -c '%u:%g:%a' -- "$script_dir")" != '0:0:755' ] ||
|
||||
[ -L "$0" ] || [ "$(stat -c '%u:%g:%a' -- "$0")" != '0:0:755' ] ||
|
||||
[ ! -f "$common_file" ] || [ -L "$common_file" ] ||
|
||||
[ "$(stat -c '%u:%g:%a' -- "$common_file")" != '0:0:644' ] ||
|
||||
[ ! -f "$reference_checker" ] || [ -L "$reference_checker" ] ||
|
||||
[ "$(stat -c '%u:%g:%a' -- "$reference_checker")" != '0:0:644' ] ||
|
||||
[ ! -f "$address_checker" ] || [ -L "$address_checker" ] ||
|
||||
[ "$(stat -c '%u:%g:%a' -- "$address_checker")" != '0:0:644' ] ||
|
||||
[ ! -f "$managed_chain_checker" ] || [ -L "$managed_chain_checker" ] ||
|
||||
[ "$(stat -c '%u:%g:%a' -- "$managed_chain_checker")" != '0:0:644' ]
|
||||
then
|
||||
echo "CN firewall: installed runtime must be immutable root-owned files" >&2
|
||||
exit 1
|
||||
fi
|
||||
. "$common_file"
|
||||
|
||||
cn_firewall_load_config "$config_file" strict
|
||||
cn_firewall_load_deployment_binding "$binding_file"
|
||||
if ! ip link show dev "$OPENPENCIL_CN_INGRESS_INTERFACE" >/dev/null 2>&1; then
|
||||
cn_firewall_fail "configured ingress interface does not exist"
|
||||
exit 1
|
||||
fi
|
||||
cn_fw_interface_addresses=$(ip -4 -o addr show dev \
|
||||
"$OPENPENCIL_CN_INGRESS_INTERFACE") || {
|
||||
cn_firewall_fail "could not inspect configured ingress-interface addresses"
|
||||
exit 1
|
||||
}
|
||||
cn_fw_service_assignments=$(printf '%s\n' "$cn_fw_interface_addresses" | awk \
|
||||
-v expected_ipv4="$OPENPENCIL_CN_SERVICE_IPV4" -f "$address_checker")
|
||||
if [ "$cn_fw_service_assignments" -ne 1 ]; then
|
||||
cn_firewall_fail \
|
||||
"configured service IPv4 must be assigned exactly once to the ingress interface"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case $(iptables --version) in
|
||||
*nf_tables*) cn_fw_iptables_backend=nft ;;
|
||||
*legacy*) cn_fw_iptables_backend=legacy ;;
|
||||
*) cn_firewall_fail "unrecognized iptables backend"; exit 1 ;;
|
||||
esac
|
||||
case $(iptables-restore --version) in
|
||||
*nf_tables*) cn_fw_restore_backend=nft ;;
|
||||
*legacy*) cn_fw_restore_backend=legacy ;;
|
||||
*) cn_firewall_fail "unrecognized iptables-restore backend"; exit 1 ;;
|
||||
esac
|
||||
if [ "$cn_fw_iptables_backend" != "$cn_fw_restore_backend" ]; then
|
||||
cn_firewall_fail "iptables and iptables-restore backends differ"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec 9>/run/openpencil-collab-cn-firewall.lock
|
||||
flock -x 9
|
||||
|
||||
filter_snapshot=$(iptables-save -t filter)
|
||||
if printf '%s\n' "$filter_snapshot" |
|
||||
grep -F ":$chain_name " >/dev/null
|
||||
then
|
||||
cn_fw_chain_exists=1
|
||||
else
|
||||
cn_fw_chain_exists=0
|
||||
fi
|
||||
if printf '%s\n' "$filter_snapshot" |
|
||||
grep -F ':DOCKER-USER ' >/dev/null
|
||||
then
|
||||
cn_fw_docker_user_exists=1
|
||||
else
|
||||
cn_fw_docker_user_exists=0
|
||||
fi
|
||||
|
||||
set -- $(printf '%s\n' "$filter_snapshot" | awk \
|
||||
-v managed_chain="$chain_name" -v anchor_comment="$anchor_comment" \
|
||||
-f "$reference_checker")
|
||||
cn_fw_target_references=$1
|
||||
cn_fw_canonical_jumps=$2
|
||||
cn_fw_foreign_references=$4
|
||||
if [ "$cn_fw_target_references" -ne "$cn_fw_canonical_jumps" ] ||
|
||||
[ "$cn_fw_foreign_references" -ne 0 ]
|
||||
then
|
||||
cn_firewall_fail \
|
||||
"foreign jump or goto reference into the managed chain is forbidden"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cn_fw_managed_summary=$(printf '%s\n' "$filter_snapshot" | awk \
|
||||
-v managed_chain="$chain_name" \
|
||||
-v ingress_interface="$OPENPENCIL_CN_INGRESS_INTERFACE" \
|
||||
-v gateway_ipv4="$OPENPENCIL_CN_GATEWAY_SOURCE_IPV4" \
|
||||
-v service_ipv4="$OPENPENCIL_CN_SERVICE_IPV4" \
|
||||
-v allow_relay_comment="$allow_relay_comment" \
|
||||
-v drop_relay_comment="$drop_relay_comment" \
|
||||
-v allow_locator_comment="$allow_locator_comment" \
|
||||
-v drop_locator_comment="$drop_locator_comment" \
|
||||
-v fallthrough_comment="$fallthrough_comment" \
|
||||
-f "$managed_chain_checker")
|
||||
if [ "$cn_fw_chain_exists" -eq 1 ] && [ "$cn_fw_canonical_jumps" -eq 0 ] &&
|
||||
[ "$cn_fw_managed_summary" != '5 5 5 0' ]
|
||||
then
|
||||
cn_firewall_fail \
|
||||
"existing unanchored managed-chain name is not an exact prior OpenPencil chain"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
umask 077
|
||||
staged=$(mktemp /run/openpencil-collab-cn-firewall.XXXXXX)
|
||||
cleanup() {
|
||||
rm -f "$staged"
|
||||
}
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
chmod 0600 "$staged"
|
||||
|
||||
{
|
||||
printf '%s\n' '*filter'
|
||||
if [ "$cn_fw_docker_user_exists" -eq 0 ]; then
|
||||
printf '%s\n' ':DOCKER-USER - [0:0]'
|
||||
fi
|
||||
if [ "$cn_fw_chain_exists" -eq 0 ]; then
|
||||
printf ':%s - [0:0]\n' "$chain_name"
|
||||
fi
|
||||
printf '%s\n' "-F $chain_name"
|
||||
cn_fw_jump_index=0
|
||||
while [ "$cn_fw_jump_index" -lt "$cn_fw_canonical_jumps" ]; do
|
||||
printf '%s\n' \
|
||||
"-D DOCKER-USER -m comment --comment $anchor_comment -j $chain_name"
|
||||
cn_fw_jump_index=$((cn_fw_jump_index + 1))
|
||||
done
|
||||
printf '%s\n' \
|
||||
"-I DOCKER-USER 1 -m comment --comment $anchor_comment -j $chain_name" \
|
||||
"-A $chain_name -i $OPENPENCIL_CN_INGRESS_INTERFACE -s $OPENPENCIL_CN_GATEWAY_SOURCE_IPV4/32 -p tcp -m conntrack --ctdir ORIGINAL --ctorigdst $OPENPENCIL_CN_SERVICE_IPV4/32 --ctorigdstport 8091 -m comment --comment $allow_relay_comment -j RETURN" \
|
||||
"-A $chain_name -p tcp -m conntrack --ctdir ORIGINAL --ctorigdst $OPENPENCIL_CN_SERVICE_IPV4/32 --ctorigdstport 8091 -m comment --comment $drop_relay_comment -j DROP" \
|
||||
"-A $chain_name -i $OPENPENCIL_CN_INGRESS_INTERFACE -s $OPENPENCIL_CN_GATEWAY_SOURCE_IPV4/32 -p tcp -m conntrack --ctdir ORIGINAL --ctorigdst $OPENPENCIL_CN_SERVICE_IPV4/32 --ctorigdstport 8092 -m comment --comment $allow_locator_comment -j RETURN" \
|
||||
"-A $chain_name -p tcp -m conntrack --ctdir ORIGINAL --ctorigdst $OPENPENCIL_CN_SERVICE_IPV4/32 --ctorigdstport 8092 -m comment --comment $drop_locator_comment -j DROP" \
|
||||
"-A $chain_name -m comment --comment $fallthrough_comment -j RETURN" \
|
||||
'COMMIT'
|
||||
} >"$staged"
|
||||
|
||||
# --noflush preserves every unrelated filter rule. Each restore is one kernel
|
||||
# transaction, so the managed chain and its first-position anchor change as a
|
||||
# unit or remain at their previous state on failure.
|
||||
iptables-restore --wait 10 --noflush --test <"$staged"
|
||||
iptables-restore --wait 10 --noflush <"$staged"
|
||||
|
||||
echo "CN Docker ingress firewall reconciled"
|
||||
17
deploy/collab-relay-locator/check-cn-docker-user-firewall-config.sh
Executable file
17
deploy/collab-relay-locator/check-cn-docker-user-firewall-config.sh
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
LC_ALL=C
|
||||
export LC_ALL
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "usage: $0 INVENTORY_FILE" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
# This helper performs syntax checks only for release validation. The installed
|
||||
# mutating and kernel-verification paths independently require root ownership.
|
||||
. "$script_dir/cn-docker-user-firewall-common.sh"
|
||||
cn_firewall_load_config "$1" syntax-only
|
||||
|
||||
echo "CN firewall inventory syntax is valid"
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
LC_ALL=C
|
||||
export LC_ALL
|
||||
|
||||
usage() {
|
||||
echo "usage: $0 [--syntax-only] INVENTORY LOCATOR_CN_OVERLAY RELAY_CN_OVERLAY" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
ownership_mode=strict
|
||||
if [ "${1-}" = --syntax-only ]; then
|
||||
ownership_mode=syntax-only
|
||||
shift
|
||||
fi
|
||||
[ "$#" -eq 3 ] || usage
|
||||
inventory_file=$1
|
||||
locator_overlay=$2
|
||||
relay_overlay=$3
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
. "$script_dir/cn-docker-user-firewall-common.sh"
|
||||
cn_firewall_load_config "$inventory_file" "$ownership_mode"
|
||||
|
||||
extract_overlay_bind() {
|
||||
cn_fw_overlay=$1
|
||||
cn_fw_expected_port=$2
|
||||
if [ ! -f "$cn_fw_overlay" ] || [ -L "$cn_fw_overlay" ] ||
|
||||
grep -F '${' "$cn_fw_overlay" >/dev/null
|
||||
then
|
||||
cn_firewall_fail "CN Compose overlay is missing, symlinked, or mutable"
|
||||
return 1
|
||||
fi
|
||||
cn_fw_host_count=$(grep -Ec '^[[:space:]]+host_ip:[[:space:]]+[0-9.]+$' \
|
||||
"$cn_fw_overlay" || true)
|
||||
cn_fw_target_count=$(grep -Ec \
|
||||
"^[[:space:]]+-[[:space:]]+target:[[:space:]]+$cn_fw_expected_port$" \
|
||||
"$cn_fw_overlay" || true)
|
||||
cn_fw_published_count=$(grep -Ec \
|
||||
"^[[:space:]]+published:[[:space:]]+\"$cn_fw_expected_port\"$" \
|
||||
"$cn_fw_overlay" || true)
|
||||
cn_fw_protocol_count=$(grep -Ec \
|
||||
'^[[:space:]]+protocol:[[:space:]]+tcp$' "$cn_fw_overlay" || true)
|
||||
cn_fw_ports_count=$(grep -Ec \
|
||||
'^[[:space:]]+ports:[[:space:]]*$' "$cn_fw_overlay" || true)
|
||||
cn_fw_list_item_count=$(grep -Ec \
|
||||
'^[[:space:]]+-[[:space:]]+' "$cn_fw_overlay" || true)
|
||||
cn_fw_all_hosts=$(grep -Ec \
|
||||
'^[[:space:]]+host_ip:' "$cn_fw_overlay" || true)
|
||||
cn_fw_all_targets=$(grep -Ec \
|
||||
'^[[:space:]]+-[[:space:]]+target:' "$cn_fw_overlay" || true)
|
||||
cn_fw_all_published=$(grep -Ec \
|
||||
'^[[:space:]]+published:' "$cn_fw_overlay" || true)
|
||||
cn_fw_all_protocols=$(grep -Ec \
|
||||
'^[[:space:]]+protocol:' "$cn_fw_overlay" || true)
|
||||
if [ "$cn_fw_host_count" -ne 1 ] || [ "$cn_fw_target_count" -ne 1 ] ||
|
||||
[ "$cn_fw_published_count" -ne 1 ] ||
|
||||
[ "$cn_fw_protocol_count" -ne 1 ] || [ "$cn_fw_ports_count" -ne 1 ] ||
|
||||
[ "$cn_fw_list_item_count" -ne 1 ] || [ "$cn_fw_all_hosts" -ne 1 ] ||
|
||||
[ "$cn_fw_all_targets" -ne 1 ] || [ "$cn_fw_all_published" -ne 1 ] ||
|
||||
[ "$cn_fw_all_protocols" -ne 1 ]
|
||||
then
|
||||
cn_firewall_fail \
|
||||
"CN Compose overlay must publish exactly its protected IPv4 TCP port"
|
||||
return 1
|
||||
fi
|
||||
cn_fw_extracted_bind=$(awk '$1 == "host_ip:" { print $2 }' \
|
||||
"$cn_fw_overlay")
|
||||
cn_firewall_validate_ipv4 "$cn_fw_extracted_bind" || return 1
|
||||
printf '%s\n' "$cn_fw_extracted_bind"
|
||||
}
|
||||
|
||||
locator_bind=$(extract_overlay_bind "$locator_overlay" 8092)
|
||||
relay_bind=$(extract_overlay_bind "$relay_overlay" 8091)
|
||||
if [ "$locator_bind" != "$relay_bind" ] ||
|
||||
[ "$locator_bind" != "$OPENPENCIL_CN_SERVICE_IPV4" ]
|
||||
then
|
||||
cn_firewall_fail \
|
||||
"inventory service IPv4 must equal both immutable CN Compose host_ip values"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "$locator_bind"
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# Count exact configured IPv4 assignments in `ip -4 -o addr show dev` output.
|
||||
$3 == "inet" {
|
||||
split($4, address_and_prefix, "/")
|
||||
if (address_and_prefix[1] == expected_ipv4 &&
|
||||
address_and_prefix[2] ~ /^[0-9]+$/) {
|
||||
matches++
|
||||
}
|
||||
}
|
||||
END {
|
||||
print matches + 0
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
# Validate the exact five-rule managed-chain shape from one iptables-save
|
||||
# snapshot. Output: total valid ordered invalid.
|
||||
function exact_singleton_ipv4(value, expected_ipv4) {
|
||||
# xtables-nft serializes conntrack's exact `/32` original destination as a
|
||||
# bare IPv4, while other compatible backends may retain `/32`.
|
||||
return value == expected_ipv4 || value == expected_ipv4 "/32"
|
||||
}
|
||||
|
||||
$1 == "-A" && $2 == managed_chain {
|
||||
total++
|
||||
interface_name = ""
|
||||
source_ipv4 = ""
|
||||
protocol = ""
|
||||
conntrack_modules = 0
|
||||
comment_modules = 0
|
||||
ct_direction = ""
|
||||
original_destination = ""
|
||||
original_port = ""
|
||||
rule_comment = ""
|
||||
target = ""
|
||||
unknown = 0
|
||||
interface_options = 0
|
||||
source_options = 0
|
||||
protocol_options = 0
|
||||
direction_options = 0
|
||||
destination_options = 0
|
||||
port_options = 0
|
||||
comment_options = 0
|
||||
target_options = 0
|
||||
|
||||
for (i = 3; i <= NF; i++) {
|
||||
if (($i == "-i" || $i == "-s" || $i == "-p" ||
|
||||
$i == "--ctdir" || $i == "--ctorigdst" ||
|
||||
$i == "--ctorigdstport" || $i == "--comment" ||
|
||||
$i == "-j") && i < NF) {
|
||||
option = $i
|
||||
value = $(i + 1)
|
||||
i++
|
||||
if (option == "-i") {
|
||||
interface_options++
|
||||
interface_name = value
|
||||
} else if (option == "-s") {
|
||||
source_options++
|
||||
source_ipv4 = value
|
||||
} else if (option == "-p") {
|
||||
protocol_options++
|
||||
protocol = value
|
||||
} else if (option == "--ctdir") {
|
||||
direction_options++
|
||||
ct_direction = value
|
||||
} else if (option == "--ctorigdst") {
|
||||
destination_options++
|
||||
original_destination = value
|
||||
} else if (option == "--ctorigdstport") {
|
||||
port_options++
|
||||
original_port = value
|
||||
}
|
||||
else if (option == "--comment") {
|
||||
comment_options++
|
||||
rule_comment = value
|
||||
gsub(/^"|"$/, "", rule_comment)
|
||||
} else if (option == "-j") {
|
||||
target_options++
|
||||
target = value
|
||||
}
|
||||
} else if ($i == "-m" && i < NF) {
|
||||
module_name = $(i + 1)
|
||||
i++
|
||||
if (module_name == "conntrack") conntrack_modules++
|
||||
else if (module_name == "comment") comment_modules++
|
||||
else unknown++
|
||||
} else {
|
||||
unknown++
|
||||
}
|
||||
}
|
||||
|
||||
expected_comment = ""
|
||||
expected_interface = ""
|
||||
expected_source = ""
|
||||
expected_protocol = "tcp"
|
||||
expected_conntrack_modules = 1
|
||||
expected_comment_modules = 1
|
||||
expected_direction = "ORIGINAL"
|
||||
expected_destination = service_ipv4
|
||||
expected_port = ""
|
||||
expected_target = ""
|
||||
expected_interface_options = 0
|
||||
expected_source_options = 0
|
||||
expected_protocol_options = 1
|
||||
expected_direction_options = 1
|
||||
expected_destination_options = 1
|
||||
expected_port_options = 1
|
||||
expected_comment_options = 1
|
||||
expected_target_options = 1
|
||||
if (total == 1) {
|
||||
expected_comment = allow_relay_comment
|
||||
expected_interface = ingress_interface
|
||||
expected_source = gateway_ipv4 "/32"
|
||||
expected_port = "8091"
|
||||
expected_target = "RETURN"
|
||||
expected_interface_options = 1
|
||||
expected_source_options = 1
|
||||
} else if (total == 2) {
|
||||
expected_comment = drop_relay_comment
|
||||
expected_port = "8091"
|
||||
expected_target = "DROP"
|
||||
} else if (total == 3) {
|
||||
expected_comment = allow_locator_comment
|
||||
expected_interface = ingress_interface
|
||||
expected_source = gateway_ipv4 "/32"
|
||||
expected_port = "8092"
|
||||
expected_target = "RETURN"
|
||||
expected_interface_options = 1
|
||||
expected_source_options = 1
|
||||
} else if (total == 4) {
|
||||
expected_comment = drop_locator_comment
|
||||
expected_port = "8092"
|
||||
expected_target = "DROP"
|
||||
} else if (total == 5) {
|
||||
expected_comment = fallthrough_comment
|
||||
expected_protocol = ""
|
||||
expected_conntrack_modules = 0
|
||||
expected_direction = ""
|
||||
expected_destination = ""
|
||||
expected_target = "RETURN"
|
||||
expected_protocol_options = 0
|
||||
expected_direction_options = 0
|
||||
expected_destination_options = 0
|
||||
expected_port_options = 0
|
||||
}
|
||||
|
||||
if (unknown == 0 && rule_comment == expected_comment &&
|
||||
interface_name == expected_interface && source_ipv4 == expected_source &&
|
||||
protocol == expected_protocol &&
|
||||
conntrack_modules == expected_conntrack_modules &&
|
||||
comment_modules == expected_comment_modules &&
|
||||
ct_direction == expected_direction &&
|
||||
((expected_destination == "" && original_destination == "") ||
|
||||
(expected_destination != "" &&
|
||||
exact_singleton_ipv4(original_destination, expected_destination))) &&
|
||||
original_port == expected_port && target == expected_target &&
|
||||
interface_options == expected_interface_options &&
|
||||
source_options == expected_source_options &&
|
||||
protocol_options == expected_protocol_options &&
|
||||
direction_options == expected_direction_options &&
|
||||
destination_options == expected_destination_options &&
|
||||
port_options == expected_port_options &&
|
||||
comment_options == expected_comment_options &&
|
||||
target_options == expected_target_options) {
|
||||
valid++
|
||||
if (total <= 5) ordered++
|
||||
} else {
|
||||
invalid++
|
||||
}
|
||||
}
|
||||
END {
|
||||
print total + 0, valid + 0, ordered + 0, invalid + 0
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# Inspect an iptables-save filter-table snapshot for every reference to the
|
||||
# managed chain. Output: total canonical first-position foreign.
|
||||
$1 == "-A" {
|
||||
source_chain = $2
|
||||
if (source_chain == "DOCKER-USER") docker_user_rules++
|
||||
reference_kind = ""
|
||||
reference_target = ""
|
||||
found_comment = ""
|
||||
for (i = 3; i <= NF; i++) {
|
||||
if (($i == "-j" || $i == "--jump" || $i == "-g" ||
|
||||
$i == "--goto") && i < NF) {
|
||||
reference_kind = $i
|
||||
reference_target = $(i + 1)
|
||||
}
|
||||
if ($i == "--comment" && i < NF) {
|
||||
found_comment = $(i + 1)
|
||||
gsub(/^"|"$/, "", found_comment)
|
||||
}
|
||||
}
|
||||
if (reference_target == managed_chain) {
|
||||
total++
|
||||
if (source_chain == "DOCKER-USER" && reference_kind == "-j" &&
|
||||
NF == 8 && $3 == "-m" && $4 == "comment" &&
|
||||
$5 == "--comment" && found_comment == anchor_comment &&
|
||||
$7 == "-j" && $8 == managed_chain) {
|
||||
canonical++
|
||||
if (docker_user_rules == 1) first++
|
||||
} else {
|
||||
foreign++
|
||||
}
|
||||
}
|
||||
}
|
||||
END {
|
||||
print total + 0, canonical + 0, first + 0, foreign + 0
|
||||
}
|
||||
277
deploy/collab-relay-locator/cn-docker-user-firewall-common.sh
Normal file
277
deploy/collab-relay-locator/cn-docker-user-firewall-common.sh
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
# Shared, non-executable helpers for the CN service-host firewall scripts.
|
||||
# Configuration is parsed as data. It is never sourced by a shell.
|
||||
|
||||
cn_firewall_fail() {
|
||||
echo "CN firewall: $*" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
cn_firewall_validate_ipv4() {
|
||||
cn_fw_address=$1
|
||||
case $cn_fw_address in
|
||||
''|*[!0-9.]*)
|
||||
cn_firewall_fail "IPv4 values must use canonical dotted-decimal notation"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
cn_fw_old_ifs=$IFS
|
||||
IFS=.
|
||||
set -- $cn_fw_address
|
||||
IFS=$cn_fw_old_ifs
|
||||
if [ "$#" -ne 4 ]; then
|
||||
cn_firewall_fail "IPv4 values must contain exactly four octets"
|
||||
return 1
|
||||
fi
|
||||
for cn_fw_octet in "$@"; do
|
||||
case $cn_fw_octet in
|
||||
''|*[!0-9]*)
|
||||
cn_firewall_fail "IPv4 octets must be decimal integers"
|
||||
return 1
|
||||
;;
|
||||
0) ;;
|
||||
0*)
|
||||
cn_firewall_fail "IPv4 octets must not contain leading zeroes"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
if [ "${#cn_fw_octet}" -gt 3 ]; then
|
||||
cn_firewall_fail "IPv4 octets must contain at most three digits"
|
||||
return 1
|
||||
fi
|
||||
if [ "$cn_fw_octet" -gt 255 ]; then
|
||||
cn_firewall_fail "IPv4 octets must be in the range 0 through 255"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
cn_fw_canonical_address=$1.$2.$3.$4
|
||||
if [ "$cn_fw_address" != "$cn_fw_canonical_address" ]; then
|
||||
cn_firewall_fail "IPv4 value is not canonical dotted-decimal"
|
||||
return 1
|
||||
fi
|
||||
|
||||
case $cn_fw_address in
|
||||
0.0.0.0|255.255.255.255)
|
||||
cn_firewall_fail "unspecified and limited-broadcast IPv4 values are forbidden"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
cn_firewall_validate_interface() {
|
||||
cn_fw_interface=$1
|
||||
case $cn_fw_interface in
|
||||
[A-Za-z0-9]*) ;;
|
||||
*)
|
||||
cn_firewall_fail "ingress interface must start with an ASCII alphanumeric"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
case $cn_fw_interface in
|
||||
*[!A-Za-z0-9_.:-]*)
|
||||
cn_firewall_fail "ingress interface contains a forbidden character"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
if [ "${#cn_fw_interface}" -gt 15 ]; then
|
||||
cn_firewall_fail "ingress interface exceeds Linux IFNAMSIZ"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
cn_firewall_mode_is_directory_safe() {
|
||||
cn_fw_mode=$1
|
||||
case $cn_fw_mode in
|
||||
[0-7][0-7][0-7]) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
cn_fw_group_digit=${cn_fw_mode#?}
|
||||
cn_fw_group_digit=${cn_fw_group_digit%?}
|
||||
cn_fw_other_digit=${cn_fw_mode#??}
|
||||
case $cn_fw_group_digit$cn_fw_other_digit in
|
||||
*[2367]*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
cn_firewall_require_secure_parent_directories() {
|
||||
cn_fw_secure_path=$1
|
||||
cn_fw_parent=$(dirname -- "$cn_fw_secure_path")
|
||||
while [ "$cn_fw_parent" != / ]; do
|
||||
if [ ! -d "$cn_fw_parent" ] || [ -L "$cn_fw_parent" ] ||
|
||||
[ "$(stat -c '%u' -- "$cn_fw_parent")" -ne 0 ] ||
|
||||
[ "$(stat -c '%g' -- "$cn_fw_parent")" -ne 0 ] ||
|
||||
! cn_firewall_mode_is_directory_safe \
|
||||
"$(stat -c '%a' -- "$cn_fw_parent")"
|
||||
then
|
||||
cn_firewall_fail \
|
||||
"configuration parents must be root-owned, non-symlink, and non-writable by group/other"
|
||||
return 1
|
||||
fi
|
||||
cn_fw_parent=$(dirname -- "$cn_fw_parent")
|
||||
done
|
||||
}
|
||||
|
||||
cn_firewall_require_secure_root_file() {
|
||||
cn_fw_secure_file=$1
|
||||
cn_fw_required_mode=$2
|
||||
case $cn_fw_secure_file in
|
||||
/*) ;;
|
||||
*)
|
||||
cn_firewall_fail "secure data path must be absolute"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
if [ ! -f "$cn_fw_secure_file" ] || [ -L "$cn_fw_secure_file" ]; then
|
||||
cn_firewall_fail "secure data must be a regular non-symlink file"
|
||||
return 1
|
||||
fi
|
||||
if [ "$(stat -c '%u' -- "$cn_fw_secure_file")" -ne 0 ] ||
|
||||
[ "$(stat -c '%g' -- "$cn_fw_secure_file")" -ne 0 ] ||
|
||||
[ "$(stat -c '%a' -- "$cn_fw_secure_file")" != \
|
||||
"$cn_fw_required_mode" ]
|
||||
then
|
||||
cn_firewall_fail \
|
||||
"secure data must have the required root:root ownership and mode"
|
||||
return 1
|
||||
fi
|
||||
cn_firewall_require_secure_parent_directories "$cn_fw_secure_file"
|
||||
}
|
||||
|
||||
cn_firewall_require_secure_config() {
|
||||
cn_fw_config=$1
|
||||
if ! cn_firewall_require_secure_root_file "$cn_fw_config" 600; then
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
cn_firewall_load_config() {
|
||||
cn_fw_config=$1
|
||||
cn_fw_ownership_mode=$2
|
||||
if [ "$cn_fw_ownership_mode" = strict ]; then
|
||||
cn_firewall_require_secure_config "$cn_fw_config" || return 1
|
||||
elif [ "$cn_fw_ownership_mode" != syntax-only ]; then
|
||||
cn_firewall_fail "internal configuration-validation mode is invalid"
|
||||
return 1
|
||||
fi
|
||||
if [ ! -f "$cn_fw_config" ] || [ -L "$cn_fw_config" ]; then
|
||||
cn_firewall_fail "configuration must be a regular non-symlink file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
OPENPENCIL_CN_INGRESS_INTERFACE=
|
||||
OPENPENCIL_CN_GATEWAY_SOURCE_IPV4=
|
||||
OPENPENCIL_CN_SERVICE_IPV4=
|
||||
cn_fw_seen_interface=0
|
||||
cn_fw_seen_source=0
|
||||
cn_fw_seen_service=0
|
||||
cn_fw_line_number=0
|
||||
|
||||
while IFS= read -r cn_fw_line || [ -n "$cn_fw_line" ]; do
|
||||
cn_fw_line_number=$((cn_fw_line_number + 1))
|
||||
case $cn_fw_line in
|
||||
''|'#'*) continue ;;
|
||||
*=*) ;;
|
||||
*)
|
||||
cn_firewall_fail \
|
||||
"invalid configuration line $cn_fw_line_number"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
cn_fw_key=${cn_fw_line%%=*}
|
||||
cn_fw_value=${cn_fw_line#*=}
|
||||
if [ -z "$cn_fw_value" ]; then
|
||||
cn_firewall_fail \
|
||||
"empty configuration value on line $cn_fw_line_number"
|
||||
return 1
|
||||
fi
|
||||
case $cn_fw_key in
|
||||
OPENPENCIL_CN_INGRESS_INTERFACE)
|
||||
if [ "$cn_fw_seen_interface" -ne 0 ]; then
|
||||
cn_firewall_fail "duplicate ingress-interface key"
|
||||
return 1
|
||||
fi
|
||||
cn_fw_seen_interface=1
|
||||
OPENPENCIL_CN_INGRESS_INTERFACE=$cn_fw_value
|
||||
;;
|
||||
OPENPENCIL_CN_GATEWAY_SOURCE_IPV4)
|
||||
if [ "$cn_fw_seen_source" -ne 0 ]; then
|
||||
cn_firewall_fail "duplicate gateway-source key"
|
||||
return 1
|
||||
fi
|
||||
cn_fw_seen_source=1
|
||||
OPENPENCIL_CN_GATEWAY_SOURCE_IPV4=$cn_fw_value
|
||||
;;
|
||||
OPENPENCIL_CN_SERVICE_IPV4)
|
||||
if [ "$cn_fw_seen_service" -ne 0 ]; then
|
||||
cn_firewall_fail "duplicate service-address key"
|
||||
return 1
|
||||
fi
|
||||
cn_fw_seen_service=1
|
||||
OPENPENCIL_CN_SERVICE_IPV4=$cn_fw_value
|
||||
;;
|
||||
*)
|
||||
cn_firewall_fail \
|
||||
"unknown configuration key on line $cn_fw_line_number"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done <"$cn_fw_config"
|
||||
|
||||
if [ "$cn_fw_seen_interface" -ne 1 ] ||
|
||||
[ "$cn_fw_seen_source" -ne 1 ] ||
|
||||
[ "$cn_fw_seen_service" -ne 1 ]
|
||||
then
|
||||
cn_firewall_fail "configuration must contain each required key exactly once"
|
||||
return 1
|
||||
fi
|
||||
cn_firewall_validate_interface "$OPENPENCIL_CN_INGRESS_INTERFACE" ||
|
||||
return 1
|
||||
cn_firewall_validate_ipv4 "$OPENPENCIL_CN_GATEWAY_SOURCE_IPV4" ||
|
||||
return 1
|
||||
cn_firewall_validate_ipv4 "$OPENPENCIL_CN_SERVICE_IPV4" || return 1
|
||||
if [ "$OPENPENCIL_CN_GATEWAY_SOURCE_IPV4" = \
|
||||
"$OPENPENCIL_CN_SERVICE_IPV4" ]
|
||||
then
|
||||
cn_firewall_fail "gateway source and service destination must differ"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
cn_firewall_load_deployment_binding() {
|
||||
cn_fw_binding_file=$1
|
||||
cn_firewall_require_secure_root_file "$cn_fw_binding_file" 400 || return 1
|
||||
OPENPENCIL_CN_EXPECTED_SERVICE_IPV4=
|
||||
cn_fw_binding_seen=0
|
||||
cn_fw_binding_line_number=0
|
||||
while IFS= read -r cn_fw_binding_line || [ -n "$cn_fw_binding_line" ]; do
|
||||
cn_fw_binding_line_number=$((cn_fw_binding_line_number + 1))
|
||||
case $cn_fw_binding_line in
|
||||
OPENPENCIL_CN_EXPECTED_SERVICE_IPV4=*)
|
||||
if [ "$cn_fw_binding_seen" -ne 0 ]; then
|
||||
cn_firewall_fail "duplicate deployment-binding key"
|
||||
return 1
|
||||
fi
|
||||
cn_fw_binding_seen=1
|
||||
OPENPENCIL_CN_EXPECTED_SERVICE_IPV4=${cn_fw_binding_line#*=}
|
||||
;;
|
||||
*)
|
||||
cn_firewall_fail \
|
||||
"invalid deployment-binding line $cn_fw_binding_line_number"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done <"$cn_fw_binding_file"
|
||||
if [ "$cn_fw_binding_seen" -ne 1 ]; then
|
||||
cn_firewall_fail "deployment binding must contain exactly one key"
|
||||
return 1
|
||||
fi
|
||||
cn_firewall_validate_ipv4 "$OPENPENCIL_CN_EXPECTED_SERVICE_IPV4" ||
|
||||
return 1
|
||||
if [ "$OPENPENCIL_CN_EXPECTED_SERVICE_IPV4" != \
|
||||
"$OPENPENCIL_CN_SERVICE_IPV4" ]
|
||||
then
|
||||
cn_firewall_fail \
|
||||
"inventory service IPv4 differs from the installed Compose binding"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Copy to a private inventory location as root:root mode 0600, then replace
|
||||
# these RFC 5737 documentation addresses with the reviewed CN inventory.
|
||||
OPENPENCIL_CN_INGRESS_INTERFACE=eth0
|
||||
OPENPENCIL_CN_GATEWAY_SOURCE_IPV4=198.51.100.10
|
||||
OPENPENCIL_CN_SERVICE_IPV4=203.0.113.10
|
||||
5
deploy/collab-relay-locator/compose.hsm.yaml
Normal file
5
deploy/collab-relay-locator/compose.hsm.yaml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
services:
|
||||
locator:
|
||||
depends_on:
|
||||
locator-hsm:
|
||||
condition: service_healthy
|
||||
10
deploy/collab-relay-locator/compose.production.cn.yaml
Normal file
10
deploy/collab-relay-locator/compose.production.cn.yaml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
services:
|
||||
locator:
|
||||
environment:
|
||||
OPENPENCIL_COLLAB_LOCATOR_HOME_REGION: cn
|
||||
OPENPENCIL_COLLAB_LOCATOR_CLIENT_RATE_PER_SECOND: "100"
|
||||
ports:
|
||||
- target: 8092
|
||||
published: "8092"
|
||||
host_ip: 10.0.0.10
|
||||
protocol: tcp
|
||||
10
deploy/collab-relay-locator/compose.production.global.yaml
Normal file
10
deploy/collab-relay-locator/compose.production.global.yaml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
services:
|
||||
locator:
|
||||
environment:
|
||||
OPENPENCIL_COLLAB_LOCATOR_HOME_REGION: global
|
||||
OPENPENCIL_COLLAB_LOCATOR_CLIENT_RATE_PER_SECOND: "100"
|
||||
ports:
|
||||
- target: 8092
|
||||
published: "8092"
|
||||
host_ip: 127.0.0.1
|
||||
protocol: tcp
|
||||
|
|
@ -6,7 +6,6 @@ services:
|
|||
restart: unless-stopped
|
||||
environment:
|
||||
OPENPENCIL_COLLAB_LOCATOR_LISTEN: 0.0.0.0:8092
|
||||
OPENPENCIL_COLLAB_LOCATOR_HOME_REGION: ${OPENPENCIL_COLLAB_LOCATOR_HOME_REGION:?set cn or global}
|
||||
OPENPENCIL_COLLAB_LOCATOR_TICKET_POLICY_FILE: /run/secrets/collab-policy.json
|
||||
OPENPENCIL_COLLAB_LOCATOR_HSM_SOCKET: /run/openpencil-hsm/signer.sock
|
||||
OPENPENCIL_COLLAB_LOCATOR_HSM_KEY_ID: ${OPENPENCIL_COLLAB_LOCATOR_HSM_KEY_ID:?set public HSM key id}
|
||||
|
|
|
|||
199
deploy/collab-relay-locator/install-cn-docker-user-firewall.sh
Executable file
199
deploy/collab-relay-locator/install-cn-docker-user-firewall.sh
Executable file
|
|
@ -0,0 +1,199 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
LC_ALL=C
|
||||
PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
||||
export LC_ALL PATH
|
||||
|
||||
runtime_dir=/usr/local/libexec/openpencil-collab-cn-firewall
|
||||
config_dir=/etc/openpencil
|
||||
config_file=$config_dir/collab-cn-firewall.env
|
||||
binding_file=$config_dir/collab-cn-firewall-compose.env
|
||||
unit_file=/etc/systemd/system/openpencil-collab-cn-firewall.service
|
||||
docker_dropin_dir=/etc/systemd/system/docker.service.d
|
||||
docker_dropin=$docker_dropin_dir/50-openpencil-collab-cn-firewall.conf
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "usage: $0 ROOT_OWNED_INVENTORY_FILE" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "CN firewall: installation requires root" >&2
|
||||
exit 1
|
||||
fi
|
||||
for command_name in stat install mktemp chmod mv systemctl flock dirname; do
|
||||
command -v "$command_name" >/dev/null 2>&1 || {
|
||||
echo "CN firewall: required command is unavailable: $command_name" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
inventory_file=$1
|
||||
case $inventory_file in
|
||||
/*) ;;
|
||||
*)
|
||||
echo "CN firewall: inventory path must be absolute" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
if [ ! -f "$inventory_file" ] || [ -L "$inventory_file" ] ||
|
||||
[ "$(stat -c '%u:%g:%a' -- "$inventory_file")" != '0:0:600' ]
|
||||
then
|
||||
echo "CN firewall: inventory must be root:root mode 0600 and not a symlink" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
locator_cn_overlay="$script_dir/compose.production.cn.yaml"
|
||||
relay_cn_overlay="$script_dir/../collab-relay/compose.production.cn.yaml"
|
||||
for source_file in \
|
||||
cn-docker-user-firewall-common.sh \
|
||||
check-cn-docker-user-firewall-references.awk \
|
||||
check-cn-docker-user-firewall-interface-address.awk \
|
||||
check-cn-docker-user-firewall-managed-chain.awk \
|
||||
check-cn-docker-user-firewall-deployment-binding.sh \
|
||||
apply-cn-docker-user-firewall.sh \
|
||||
verify-cn-docker-user-firewall.sh \
|
||||
openpencil-collab-cn-firewall.service \
|
||||
openpencil-collab-cn-firewall-docker.conf
|
||||
do
|
||||
if [ ! -f "$script_dir/$source_file" ] || [ -L "$script_dir/$source_file" ]; then
|
||||
echo "CN firewall: release artifact is missing or a symlink: $source_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
for source_file in "$locator_cn_overlay" "$relay_cn_overlay"; do
|
||||
if [ ! -f "$source_file" ] || [ -L "$source_file" ]; then
|
||||
echo "CN firewall: immutable CN Compose overlay is missing or symlinked" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if systemctl is-active --quiet docker.service; then
|
||||
docker_initially_active=1
|
||||
docker_initial_pid=$(systemctl show --property MainPID --value docker.service)
|
||||
case $docker_initial_pid in
|
||||
''|0|*[!0-9]*)
|
||||
echo "CN firewall: active Docker service has no stable main PID" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
else
|
||||
docker_initially_active=0
|
||||
docker_initial_pid=0
|
||||
fi
|
||||
|
||||
install -d -o root -g root -m 0755 "$runtime_dir" "$docker_dropin_dir"
|
||||
install -o root -g root -m 0644 \
|
||||
"$script_dir/cn-docker-user-firewall-common.sh" \
|
||||
"$runtime_dir/cn-docker-user-firewall-common.sh"
|
||||
install -o root -g root -m 0644 \
|
||||
"$script_dir/check-cn-docker-user-firewall-references.awk" \
|
||||
"$runtime_dir/check-cn-docker-user-firewall-references.awk"
|
||||
install -o root -g root -m 0644 \
|
||||
"$script_dir/check-cn-docker-user-firewall-interface-address.awk" \
|
||||
"$runtime_dir/check-cn-docker-user-firewall-interface-address.awk"
|
||||
install -o root -g root -m 0644 \
|
||||
"$script_dir/check-cn-docker-user-firewall-managed-chain.awk" \
|
||||
"$runtime_dir/check-cn-docker-user-firewall-managed-chain.awk"
|
||||
install -o root -g root -m 0755 \
|
||||
"$script_dir/check-cn-docker-user-firewall-deployment-binding.sh" \
|
||||
"$runtime_dir/check-cn-docker-user-firewall-deployment-binding.sh"
|
||||
install -o root -g root -m 0755 \
|
||||
"$script_dir/apply-cn-docker-user-firewall.sh" \
|
||||
"$runtime_dir/apply-cn-docker-user-firewall.sh"
|
||||
install -o root -g root -m 0755 \
|
||||
"$script_dir/verify-cn-docker-user-firewall.sh" \
|
||||
"$runtime_dir/verify-cn-docker-user-firewall.sh"
|
||||
|
||||
. "$runtime_dir/cn-docker-user-firewall-common.sh"
|
||||
# This parent walk happens before the delayed copy. A root-owned 0600 file in
|
||||
# an attacker-writable directory is rejected, closing pathname replacement.
|
||||
cn_firewall_require_secure_config "$inventory_file"
|
||||
if [ ! -e "$config_dir" ]; then
|
||||
install -d -o root -g root -m 0755 "$config_dir"
|
||||
elif [ ! -d "$config_dir" ] || [ -L "$config_dir" ] ||
|
||||
[ "$(stat -c '%u:%g' -- "$config_dir")" != '0:0' ] ||
|
||||
! cn_firewall_mode_is_directory_safe "$(stat -c '%a' -- "$config_dir")"
|
||||
then
|
||||
echo "CN firewall: existing /etc/openpencil directory is not root-owned and safe" >&2
|
||||
exit 1
|
||||
fi
|
||||
expected_service_ipv4=$(
|
||||
"$runtime_dir/check-cn-docker-user-firewall-deployment-binding.sh" \
|
||||
"$inventory_file" "$locator_cn_overlay" "$relay_cn_overlay"
|
||||
)
|
||||
|
||||
# Copy into a root-only staging file and validate those immutable bytes. The
|
||||
# inventory is parsed as data and is never sourced or evaluated as shell code.
|
||||
umask 077
|
||||
staged_config=$(mktemp "$config_dir/.collab-cn-firewall.env.XXXXXX")
|
||||
staged_binding=$(mktemp "$config_dir/.collab-cn-firewall-compose.env.XXXXXX")
|
||||
cleanup() {
|
||||
rm -f "$staged_config"
|
||||
rm -f "$staged_binding"
|
||||
}
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
chmod 0600 "$staged_config"
|
||||
install -o root -g root -m 0600 "$inventory_file" "$staged_config"
|
||||
"$runtime_dir/verify-cn-docker-user-firewall.sh" \
|
||||
--config-only "$staged_config"
|
||||
staged_expected_service_ipv4=$(
|
||||
"$runtime_dir/check-cn-docker-user-firewall-deployment-binding.sh" \
|
||||
"$staged_config" "$locator_cn_overlay" "$relay_cn_overlay"
|
||||
)
|
||||
if [ "$staged_expected_service_ipv4" != "$expected_service_ipv4" ]; then
|
||||
echo "CN firewall: staged inventory changed during deployment binding" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' \
|
||||
"OPENPENCIL_CN_EXPECTED_SERVICE_IPV4=$expected_service_ipv4" \
|
||||
>"$staged_binding"
|
||||
chmod 0400 "$staged_binding"
|
||||
cn_firewall_load_config "$staged_config" strict
|
||||
cn_firewall_load_deployment_binding "$staged_binding"
|
||||
mv -f "$staged_config" "$config_file"
|
||||
mv -f "$staged_binding" "$binding_file"
|
||||
trap - EXIT HUP INT TERM
|
||||
|
||||
install -o root -g root -m 0644 \
|
||||
"$script_dir/openpencil-collab-cn-firewall.service" "$unit_file"
|
||||
install -o root -g root -m 0644 \
|
||||
"$script_dir/openpencil-collab-cn-firewall-docker.conf" "$docker_dropin"
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable openpencil-collab-cn-firewall.service
|
||||
if systemctl is-active --quiet docker.service; then
|
||||
docker_pid_before=$(systemctl show --property MainPID --value docker.service)
|
||||
case $docker_pid_before in
|
||||
''|0|*[!0-9]*)
|
||||
echo "CN firewall: active Docker service has no stable main PID" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
if [ "$docker_initially_active" -eq 1 ] &&
|
||||
[ "$docker_pid_before" != "$docker_initial_pid" ]
|
||||
then
|
||||
echo "CN firewall: Docker changed state during installation" >&2
|
||||
exit 1
|
||||
fi
|
||||
"$runtime_dir/apply-cn-docker-user-firewall.sh"
|
||||
"$runtime_dir/verify-cn-docker-user-firewall.sh"
|
||||
docker_pid_after=$(systemctl show --property MainPID --value docker.service)
|
||||
if ! systemctl is-active --quiet docker.service ||
|
||||
[ "$docker_pid_after" != "$docker_pid_before" ]
|
||||
then
|
||||
echo "CN firewall: active Docker PID/state changed during direct reconciliation" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if systemctl is-active --quiet openpencil-collab-cn-firewall.service; then
|
||||
"$runtime_dir/apply-cn-docker-user-firewall.sh"
|
||||
"$runtime_dir/verify-cn-docker-user-firewall.sh" --pre-docker
|
||||
else
|
||||
systemctl start openpencil-collab-cn-firewall.service
|
||||
"$runtime_dir/verify-cn-docker-user-firewall.sh" --pre-docker
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "CN Docker ingress firewall installed; Docker was not restarted"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Include exactly once in the CN front gateway's Nginx `http` block. The CN
|
||||
# application service host is the fixed private address 10.0.0.10.
|
||||
|
||||
upstream openpencil_collab_locator {
|
||||
server 10.0.0.10:8092;
|
||||
keepalive 16;
|
||||
}
|
||||
7
deploy/collab-relay-locator/nginx-http-direct.conf
Normal file
7
deploy/collab-relay-locator/nginx-http-direct.conf
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Include exactly once in the Global application host's Nginx `http` block.
|
||||
# The locator Compose service on that same host must bind 127.0.0.1:8092.
|
||||
|
||||
upstream openpencil_collab_locator {
|
||||
server 127.0.0.1:8092;
|
||||
keepalive 16;
|
||||
}
|
||||
31
deploy/collab-relay-locator/nginx-location-direct.conf
Normal file
31
deploy/collab-relay-locator/nginx-location-direct.conf
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Include inside a regional application TLS server after including
|
||||
# nginx-http-limits.conf and the matching Global-host or CN-front-gateway
|
||||
# `nginx-http-direct*.conf` once at `http` scope. This application-host
|
||||
# variant intentionally has no /healthz or catch-all location.
|
||||
|
||||
location = /v1/locator {
|
||||
if ($request_uri != "/v1/locator") {
|
||||
return 404;
|
||||
}
|
||||
if ($http_host = "") {
|
||||
return 400;
|
||||
}
|
||||
|
||||
access_log off;
|
||||
limit_req zone=openpencil_locator_per_source burst=20 nodelay;
|
||||
limit_req_status 429;
|
||||
limit_conn openpencil_locator_connections 16;
|
||||
limit_conn_status 429;
|
||||
client_max_body_size 191;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection close;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Authorization $http_authorization;
|
||||
proxy_set_header Content-Type $http_content_type;
|
||||
proxy_set_header Accept $http_accept;
|
||||
proxy_pass http://openpencil_collab_locator/v1/locator;
|
||||
proxy_connect_timeout 5s;
|
||||
proxy_send_timeout 10s;
|
||||
proxy_read_timeout 10s;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
[Unit]
|
||||
Requires=openpencil-collab-cn-firewall.service
|
||||
After=openpencil-collab-cn-firewall.service
|
||||
|
||||
[Service]
|
||||
# Reconcile after every daemon start as well as before it. A failed apply or
|
||||
# verification makes Docker startup fail instead of exposing unguarded ports.
|
||||
ExecStartPost=/usr/local/libexec/openpencil-collab-cn-firewall/apply-cn-docker-user-firewall.sh
|
||||
ExecStartPost=/usr/local/libexec/openpencil-collab-cn-firewall/verify-cn-docker-user-firewall.sh
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
[Unit]
|
||||
Description=OpenPencil CN Docker ingress firewall pre-start gate
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
Before=docker.service
|
||||
PartOf=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/libexec/openpencil-collab-cn-firewall/apply-cn-docker-user-firewall.sh
|
||||
ExecStartPost=/usr/local/libexec/openpencil-collab-cn-firewall/verify-cn-docker-user-firewall.sh --pre-docker
|
||||
ExecReload=/usr/local/libexec/openpencil-collab-cn-firewall/apply-cn-docker-user-firewall.sh
|
||||
ExecReload=/usr/local/libexec/openpencil-collab-cn-firewall/verify-cn-docker-user-firewall.sh --pre-docker
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
473
deploy/collab-relay-locator/validate-cn-docker-user-firewall.sh
Executable file
473
deploy/collab-relay-locator/validate-cn-docker-user-firewall.sh
Executable file
|
|
@ -0,0 +1,473 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
LC_ALL=C
|
||||
export LC_ALL
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
artifact_dir=${1:-$script_dir}
|
||||
release_artifact_dir=$artifact_dir
|
||||
if [ "$#" -gt 1 ]; then
|
||||
echo "usage: $0 [ARTIFACT_DIRECTORY]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
common_file=cn-docker-user-firewall-common.sh
|
||||
reference_checker=check-cn-docker-user-firewall-references.awk
|
||||
address_checker=check-cn-docker-user-firewall-interface-address.awk
|
||||
managed_chain_checker=check-cn-docker-user-firewall-managed-chain.awk
|
||||
deployment_checker=check-cn-docker-user-firewall-deployment-binding.sh
|
||||
config_checker=check-cn-docker-user-firewall-config.sh
|
||||
config_example=cn-docker-user-firewall.env.example
|
||||
apply_file=apply-cn-docker-user-firewall.sh
|
||||
verify_file=verify-cn-docker-user-firewall.sh
|
||||
install_file=install-cn-docker-user-firewall.sh
|
||||
unit_file=openpencil-collab-cn-firewall.service
|
||||
dropin_file=openpencil-collab-cn-firewall-docker.conf
|
||||
|
||||
require_literal() {
|
||||
cn_fw_pattern=$1
|
||||
cn_fw_file=$2
|
||||
grep -F -- "$cn_fw_pattern" "$artifact_dir/$cn_fw_file" >/dev/null || {
|
||||
echo "CN firewall validation: missing '$cn_fw_pattern' in $cn_fw_file" >&2
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
validate_artifacts() {
|
||||
cn_fw_dir=$1
|
||||
artifact_dir=$cn_fw_dir
|
||||
for cn_fw_file in "$common_file" "$reference_checker" "$address_checker" \
|
||||
"$managed_chain_checker" "$deployment_checker" "$config_checker" \
|
||||
"$config_example" \
|
||||
"$apply_file" "$verify_file" "$install_file" "$unit_file" \
|
||||
"$dropin_file"
|
||||
do
|
||||
[ -f "$artifact_dir/$cn_fw_file" ] || {
|
||||
echo "CN firewall validation: missing $cn_fw_file" >&2
|
||||
return 1
|
||||
}
|
||||
done
|
||||
sh -n "$artifact_dir/$common_file" "$artifact_dir/$config_checker" \
|
||||
"$artifact_dir/$deployment_checker" \
|
||||
"$artifact_dir/$apply_file" "$artifact_dir/$verify_file" \
|
||||
"$artifact_dir/$install_file" "$artifact_dir/$unit_file" \
|
||||
"$artifact_dir/$dropin_file"
|
||||
|
||||
for cn_fw_pattern in \
|
||||
'cn_firewall_load_config "$config_file" strict' \
|
||||
'cn_firewall_load_deployment_binding "$binding_file"' \
|
||||
'configured service IPv4 must be assigned exactly once to the ingress interface' \
|
||||
'foreign jump or goto reference into the managed chain is forbidden' \
|
||||
'existing unanchored managed-chain name is not an exact prior OpenPencil chain' \
|
||||
'iptables-restore --wait 10 --noflush --test' \
|
||||
'iptables-restore --wait 10 --noflush' \
|
||||
'-I DOCKER-USER 1' \
|
||||
'-F $chain_name' \
|
||||
'--ctdir ORIGINAL' \
|
||||
'--ctorigdst $OPENPENCIL_CN_SERVICE_IPV4/32' \
|
||||
'--ctorigdstport 8091' \
|
||||
'--ctorigdstport 8092' \
|
||||
'-j DROP' \
|
||||
"$fallthrough_literal"
|
||||
do
|
||||
require_literal "$cn_fw_pattern" "$apply_file"
|
||||
done
|
||||
if [ "$(grep -Fc -- '--ctorigdstport 8091' \
|
||||
"$artifact_dir/$apply_file")" -ne 2 ] ||
|
||||
[ "$(grep -Fc -- '--ctorigdstport 8092' \
|
||||
"$artifact_dir/$apply_file")" -ne 2 ] ||
|
||||
[ "$(grep -Fc -- '-j DROP' "$artifact_dir/$apply_file")" -ne 2 ]
|
||||
then
|
||||
echo "CN firewall validation: protected-port allow/drop rules are not exact" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
for cn_fw_pattern in \
|
||||
'cn_firewall_load_deployment_binding "$binding_file"' \
|
||||
'configured service IPv4 must be assigned exactly once to the ingress interface' \
|
||||
'managed chain requires one first DOCKER-USER jump and no foreign jump/goto references' \
|
||||
'Docker must place one first-position DOCKER-USER jump in FORWARD' \
|
||||
'managed chain predicates, order, or rule count are not exact' \
|
||||
'filter_snapshot=$(iptables-save -t filter)'
|
||||
do
|
||||
require_literal "$cn_fw_pattern" "$verify_file"
|
||||
done
|
||||
if grep -F 'iptables-restore' "$artifact_dir/$verify_file" >/dev/null; then
|
||||
echo "CN firewall validation: read-only verifier must not restore rules" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
for cn_fw_pattern in \
|
||||
'$i == "-j"' \
|
||||
'$i == "-g"' \
|
||||
'$i == "--goto"' \
|
||||
'source_chain == "DOCKER-USER"' \
|
||||
'foreign++'
|
||||
do
|
||||
require_literal "$cn_fw_pattern" "$reference_checker"
|
||||
done
|
||||
|
||||
for cn_fw_pattern in \
|
||||
'$3 == "inet"' \
|
||||
'address_and_prefix[1] == expected_ipv4'
|
||||
do
|
||||
require_literal "$cn_fw_pattern" "$address_checker"
|
||||
done
|
||||
for cn_fw_pattern in \
|
||||
'$1 == "-A" && $2 == managed_chain' \
|
||||
'exact_singleton_ipv4(original_destination, expected_destination)' \
|
||||
'source_ipv4 == expected_source' \
|
||||
'print total + 0, valid + 0, ordered + 0, invalid + 0'
|
||||
do
|
||||
require_literal "$cn_fw_pattern" "$managed_chain_checker"
|
||||
done
|
||||
for cn_fw_pattern in \
|
||||
'cn_firewall_load_config "$inventory_file" "$ownership_mode"' \
|
||||
'CN Compose overlay must publish exactly its protected IPv4 TCP port' \
|
||||
'inventory service IPv4 must equal both immutable CN Compose host_ip values' \
|
||||
'extract_overlay_bind "$locator_overlay" 8092' \
|
||||
'extract_overlay_bind "$relay_overlay" 8091'
|
||||
do
|
||||
require_literal "$cn_fw_pattern" "$deployment_checker"
|
||||
done
|
||||
|
||||
for cn_fw_pattern in \
|
||||
'secure data must have the required root:root ownership and mode' \
|
||||
'configuration must contain each required key exactly once' \
|
||||
'IPv4 values must use canonical dotted-decimal notation' \
|
||||
'IPv4 octets must contain at most three digits' \
|
||||
'IPv4 value is not canonical dotted-decimal' \
|
||||
'inventory service IPv4 differs from the installed Compose binding' \
|
||||
'ingress interface exceeds Linux IFNAMSIZ'
|
||||
do
|
||||
require_literal "$cn_fw_pattern" "$common_file"
|
||||
done
|
||||
if grep -Eq '^[[:space:]]*(eval|source|\.)[[:space:]]' \
|
||||
"$artifact_dir/$common_file"
|
||||
then
|
||||
echo "CN firewall validation: inventory must never be shell-evaluated" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
for cn_fw_pattern in \
|
||||
'OPENPENCIL_CN_INGRESS_INTERFACE=eth0' \
|
||||
'OPENPENCIL_CN_GATEWAY_SOURCE_IPV4=198.51.100.10' \
|
||||
'OPENPENCIL_CN_SERVICE_IPV4=203.0.113.10'
|
||||
do
|
||||
require_literal "$cn_fw_pattern" "$config_example"
|
||||
done
|
||||
|
||||
for cn_fw_pattern in \
|
||||
'inventory must be root:root mode 0600 and not a symlink' \
|
||||
'cn_firewall_require_secure_config "$inventory_file"' \
|
||||
'check-cn-docker-user-firewall-references.awk' \
|
||||
'check-cn-docker-user-firewall-deployment-binding.sh' \
|
||||
'staged inventory changed during deployment binding' \
|
||||
'existing /etc/openpencil directory is not root-owned and safe' \
|
||||
'systemctl enable openpencil-collab-cn-firewall.service' \
|
||||
'docker_pid_before=$(systemctl show --property MainPID --value docker.service)' \
|
||||
'docker_pid_after=$(systemctl show --property MainPID --value docker.service)' \
|
||||
'active Docker PID/state changed during direct reconciliation' \
|
||||
'Docker was not restarted'
|
||||
do
|
||||
require_literal "$cn_fw_pattern" "$install_file"
|
||||
done
|
||||
if grep -Eq 'systemctl[[:space:]]+(restart|stop|try-restart)' \
|
||||
"$artifact_dir/$install_file"
|
||||
then
|
||||
echo "CN firewall validation: installer must not restart or stop a unit" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
for cn_fw_pattern in \
|
||||
'Before=docker.service' \
|
||||
'PartOf=docker.service' \
|
||||
'ExecStartPost=/usr/local/libexec/openpencil-collab-cn-firewall/verify-cn-docker-user-firewall.sh --pre-docker' \
|
||||
'WantedBy=multi-user.target'
|
||||
do
|
||||
require_literal "$cn_fw_pattern" "$unit_file"
|
||||
done
|
||||
for cn_fw_pattern in \
|
||||
'Requires=openpencil-collab-cn-firewall.service' \
|
||||
'After=openpencil-collab-cn-firewall.service' \
|
||||
'ExecStartPost=/usr/local/libexec/openpencil-collab-cn-firewall/apply-cn-docker-user-firewall.sh' \
|
||||
'ExecStartPost=/usr/local/libexec/openpencil-collab-cn-firewall/verify-cn-docker-user-firewall.sh'
|
||||
do
|
||||
require_literal "$cn_fw_pattern" "$dropin_file"
|
||||
done
|
||||
|
||||
if grep -Eq '(^|[^0-9])(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)' \
|
||||
"$artifact_dir/$common_file" "$artifact_dir/$reference_checker" \
|
||||
"$artifact_dir/$address_checker" "$artifact_dir/$managed_chain_checker" \
|
||||
"$artifact_dir/$deployment_checker" \
|
||||
"$artifact_dir/$config_checker" \
|
||||
"$artifact_dir/$config_example" "$artifact_dir/$apply_file" \
|
||||
"$artifact_dir/$verify_file" "$artifact_dir/$install_file" \
|
||||
"$artifact_dir/$unit_file" "$artifact_dir/$dropin_file"
|
||||
then
|
||||
echo "CN firewall validation: private inventory addresses must not be checked in" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_reference_guard() {
|
||||
cn_fw_reference_dir=$1
|
||||
cn_fw_canonical='-A DOCKER-USER -m comment --comment openpencil-cn-ingress-v1 -j OPENPENCIL-CN-INGRESS'
|
||||
cn_fw_summary=$(printf '%s\n' '*filter' ':DOCKER-USER - [0:0]' \
|
||||
':OPENPENCIL-CN-INGRESS - [0:0]' "$cn_fw_canonical" 'COMMIT' |
|
||||
awk -v managed_chain=OPENPENCIL-CN-INGRESS \
|
||||
-v anchor_comment=openpencil-cn-ingress-v1 \
|
||||
-f "$cn_fw_reference_dir/$reference_checker")
|
||||
[ "$cn_fw_summary" = '1 1 1 0' ] || return 1
|
||||
|
||||
for cn_fw_foreign_rule in \
|
||||
'-A FORWARD -j OPENPENCIL-CN-INGRESS' \
|
||||
'-A INPUT -j OPENPENCIL-CN-INGRESS' \
|
||||
'-A FOREIGN-CUSTOM -j OPENPENCIL-CN-INGRESS' \
|
||||
'-A DOCKER-USER -g OPENPENCIL-CN-INGRESS' \
|
||||
'-A FOREIGN-CUSTOM --goto OPENPENCIL-CN-INGRESS'
|
||||
do
|
||||
cn_fw_summary=$(printf '%s\n' '*filter' ':DOCKER-USER - [0:0]' \
|
||||
':OPENPENCIL-CN-INGRESS - [0:0]' \
|
||||
':FOREIGN-CUSTOM - [0:0]' "$cn_fw_canonical" \
|
||||
"$cn_fw_foreign_rule" 'COMMIT' |
|
||||
awk -v managed_chain=OPENPENCIL-CN-INGRESS \
|
||||
-v anchor_comment=openpencil-cn-ingress-v1 \
|
||||
-f "$cn_fw_reference_dir/$reference_checker")
|
||||
[ "$cn_fw_summary" = '2 1 1 1' ] || return 1
|
||||
done
|
||||
}
|
||||
|
||||
assert_address_guard() {
|
||||
cn_fw_address_dir=$1
|
||||
cn_fw_fake_ip_output=$(printf '%s\n' \
|
||||
'2: eth0 inet 203.0.113.10/24 brd 203.0.113.255 scope global eth0' \
|
||||
'2: eth0 inet 203.0.113.20/24 brd 203.0.113.255 scope global secondary eth0')
|
||||
cn_fw_summary=$(printf '%s\n' "$cn_fw_fake_ip_output" | awk \
|
||||
-v expected_ipv4=203.0.113.10 \
|
||||
-f "$cn_fw_address_dir/$address_checker")
|
||||
[ "$cn_fw_summary" = 1 ] || return 1
|
||||
cn_fw_summary=$(printf '%s\n' "$cn_fw_fake_ip_output" | awk \
|
||||
-v expected_ipv4=203.0.113.11 \
|
||||
-f "$cn_fw_address_dir/$address_checker")
|
||||
[ "$cn_fw_summary" = 0 ] || return 1
|
||||
cn_fw_duplicate_output=$(printf '%s\n%s\n' \
|
||||
'2: eth0 inet 203.0.113.10/24 scope global eth0' \
|
||||
'2: eth0 inet 203.0.113.10/32 scope global secondary eth0')
|
||||
cn_fw_summary=$(printf '%s\n' "$cn_fw_duplicate_output" | awk \
|
||||
-v expected_ipv4=203.0.113.10 \
|
||||
-f "$cn_fw_address_dir/$address_checker")
|
||||
[ "$cn_fw_summary" = 2 ] || return 1
|
||||
}
|
||||
|
||||
managed_chain_summary() {
|
||||
cn_fw_managed_dir=$1
|
||||
cn_fw_managed_snapshot=$2
|
||||
printf '%s\n' "$cn_fw_managed_snapshot" | awk \
|
||||
-v managed_chain=OPENPENCIL-CN-INGRESS \
|
||||
-v ingress_interface=eth0 -v gateway_ipv4=198.51.100.10 \
|
||||
-v service_ipv4=203.0.113.10 \
|
||||
-v allow_relay_comment=openpencil-cn-allow-8091-v1 \
|
||||
-v drop_relay_comment=openpencil-cn-drop-8091-v1 \
|
||||
-v allow_locator_comment=openpencil-cn-allow-8092-v1 \
|
||||
-v drop_locator_comment=openpencil-cn-drop-8092-v1 \
|
||||
-v fallthrough_comment=openpencil-cn-fallthrough-v1 \
|
||||
-f "$cn_fw_managed_dir/$managed_chain_checker"
|
||||
}
|
||||
|
||||
assert_managed_chain_guard() {
|
||||
cn_fw_managed_dir=$1
|
||||
cn_fw_valid_managed=$(printf '%s\n' \
|
||||
'-A OPENPENCIL-CN-INGRESS -i eth0 -s 198.51.100.10/32 -p tcp -m conntrack --ctdir ORIGINAL --ctorigdst 203.0.113.10/32 --ctorigdstport 8091 -m comment --comment openpencil-cn-allow-8091-v1 -j RETURN' \
|
||||
'-A OPENPENCIL-CN-INGRESS -p tcp -m conntrack --ctdir ORIGINAL --ctorigdst 203.0.113.10/32 --ctorigdstport 8091 -m comment --comment openpencil-cn-drop-8091-v1 -j DROP' \
|
||||
'-A OPENPENCIL-CN-INGRESS -i eth0 -s 198.51.100.10/32 -p tcp -m conntrack --ctdir ORIGINAL --ctorigdst 203.0.113.10/32 --ctorigdstport 8092 -m comment --comment openpencil-cn-allow-8092-v1 -j RETURN' \
|
||||
'-A OPENPENCIL-CN-INGRESS -p tcp -m conntrack --ctdir ORIGINAL --ctorigdst 203.0.113.10/32 --ctorigdstport 8092 -m comment --comment openpencil-cn-drop-8092-v1 -j DROP' \
|
||||
'-A OPENPENCIL-CN-INGRESS -m comment --comment openpencil-cn-fallthrough-v1 -j RETURN')
|
||||
[ "$(managed_chain_summary "$cn_fw_managed_dir" \
|
||||
"$cn_fw_valid_managed")" = '5 5 5 0' ] || return 1
|
||||
cn_fw_live_serialized=$(printf '%s\n' \
|
||||
'-A OPENPENCIL-CN-INGRESS -s 198.51.100.10/32 -i eth0 -p tcp -m conntrack --ctorigdst 203.0.113.10 --ctorigdstport 8091 --ctdir ORIGINAL -m comment --comment openpencil-cn-allow-8091-v1 -j RETURN' \
|
||||
'-A OPENPENCIL-CN-INGRESS -p tcp -m conntrack --ctorigdst 203.0.113.10 --ctorigdstport 8091 --ctdir ORIGINAL -m comment --comment openpencil-cn-drop-8091-v1 -j DROP' \
|
||||
'-A OPENPENCIL-CN-INGRESS -s 198.51.100.10/32 -i eth0 -p tcp -m conntrack --ctorigdst 203.0.113.10 --ctorigdstport 8092 --ctdir ORIGINAL -m comment --comment openpencil-cn-allow-8092-v1 -j RETURN' \
|
||||
'-A OPENPENCIL-CN-INGRESS -p tcp -m conntrack --ctorigdst 203.0.113.10 --ctorigdstport 8092 --ctdir ORIGINAL -m comment --comment openpencil-cn-drop-8092-v1 -j DROP' \
|
||||
'-A OPENPENCIL-CN-INGRESS -m comment --comment openpencil-cn-fallthrough-v1 -j RETURN')
|
||||
[ "$(managed_chain_summary "$cn_fw_managed_dir" \
|
||||
"$cn_fw_live_serialized")" = '5 5 5 0' ] || return 1
|
||||
cn_fw_unowned='-A OPENPENCIL-CN-INGRESS -j RETURN'
|
||||
[ "$(managed_chain_summary "$cn_fw_managed_dir" "$cn_fw_unowned")" != \
|
||||
'5 5 5 0' ] || return 1
|
||||
cn_fw_weakened=$(printf '%s\n' "$cn_fw_valid_managed" |
|
||||
sed '1s/198\.51\.100\.10\/32/198.51.100.11\/32/')
|
||||
[ "$(managed_chain_summary "$cn_fw_managed_dir" "$cn_fw_weakened")" != \
|
||||
'5 5 5 0' ] || return 1
|
||||
cn_fw_wide_destination=$(printf '%s\n' "$cn_fw_live_serialized" |
|
||||
sed '1s/203\.0\.113\.10/203.0.113.10\/31/')
|
||||
[ "$(managed_chain_summary "$cn_fw_managed_dir" \
|
||||
"$cn_fw_wide_destination")" != '5 5 5 0' ] || return 1
|
||||
cn_fw_wrong_destination=$(printf '%s\n' "$cn_fw_live_serialized" |
|
||||
sed '1s/203\.0\.113\.10/203.0.113.11/')
|
||||
[ "$(managed_chain_summary "$cn_fw_managed_dir" \
|
||||
"$cn_fw_wrong_destination")" != '5 5 5 0' ] || return 1
|
||||
}
|
||||
|
||||
fallthrough_literal='-A $chain_name -m comment --comment $fallthrough_comment -j RETURN'
|
||||
validate_artifacts "$artifact_dir"
|
||||
assert_reference_guard "$artifact_dir" || {
|
||||
echo "CN firewall validation: foreign-reference guard rejected its test model" >&2
|
||||
exit 1
|
||||
}
|
||||
assert_address_guard "$artifact_dir" || {
|
||||
echo "CN firewall validation: fake ip-address guard rejected its test model" >&2
|
||||
exit 1
|
||||
}
|
||||
assert_managed_chain_guard "$artifact_dir" || {
|
||||
echo "CN firewall validation: managed-chain ownership guard rejected its test model" >&2
|
||||
exit 1
|
||||
}
|
||||
sh "$artifact_dir/$config_checker" "$artifact_dir/$config_example" >/dev/null
|
||||
|
||||
mutation_dir=$(mktemp -d "${TMPDIR:-/tmp}/openpencil-cn-firewall.XXXXXX")
|
||||
cleanup() {
|
||||
rm -rf "$mutation_dir"
|
||||
}
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
for cn_fw_file in "$common_file" "$reference_checker" "$address_checker" \
|
||||
"$managed_chain_checker" "$deployment_checker" "$config_checker" \
|
||||
"$config_example" \
|
||||
"$apply_file" "$verify_file" "$install_file" "$unit_file" "$dropin_file"
|
||||
do
|
||||
cp "$artifact_dir/$cn_fw_file" "$mutation_dir/$cn_fw_file"
|
||||
done
|
||||
|
||||
expect_config_failure() {
|
||||
cn_fw_label=$1
|
||||
if sh "$mutation_dir/$config_checker" "$mutation_dir/mutated.env" \
|
||||
>/dev/null 2>&1
|
||||
then
|
||||
echo "CN firewall validation: accepted mutation: $cn_fw_label" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
sed 's/OPENPENCIL_CN_INGRESS_INTERFACE=eth0/OPENPENCIL_CN_INGRESS_INTERFACE=eth0;id/' \
|
||||
"$artifact_dir/$config_example" >"$mutation_dir/mutated.env"
|
||||
expect_config_failure 'shell metacharacter in interface'
|
||||
sed 's/198\.51\.100\.10/gateway.internal/' "$artifact_dir/$config_example" \
|
||||
>"$mutation_dir/mutated.env"
|
||||
expect_config_failure 'hostname in numeric IPv4 field'
|
||||
sed 's/203\.0\.113\.10/203.0.113.010/' "$artifact_dir/$config_example" \
|
||||
>"$mutation_dir/mutated.env"
|
||||
expect_config_failure 'non-canonical IPv4 octet'
|
||||
sed 's/198\.51\.100\.10/198.51.100.10./' "$artifact_dir/$config_example" \
|
||||
>"$mutation_dir/mutated.env"
|
||||
expect_config_failure 'trailing empty IPv4 octet'
|
||||
sed 's/198\.51\.100\.10/999999999999999999999999999999/' \
|
||||
"$artifact_dir/$config_example" >"$mutation_dir/mutated.env"
|
||||
expect_config_failure 'overflowing IPv4 octet'
|
||||
{
|
||||
sed -n '1,$p' "$artifact_dir/$config_example"
|
||||
printf '%s\n' 'OPENPENCIL_CN_SERVICE_IPV4=203.0.113.11'
|
||||
} >"$mutation_dir/mutated.env"
|
||||
expect_config_failure 'duplicate required key'
|
||||
|
||||
printf '%s\n' \
|
||||
'services:' \
|
||||
' locator:' \
|
||||
' ports:' \
|
||||
' - target: 8092' \
|
||||
' published: "8092"' \
|
||||
' host_ip: 203.0.113.10' \
|
||||
' protocol: tcp' >"$mutation_dir/locator.cn.yaml"
|
||||
printf '%s\n' \
|
||||
'services:' \
|
||||
' relay:' \
|
||||
' ports:' \
|
||||
' - target: 8091' \
|
||||
' published: "8091"' \
|
||||
' host_ip: 203.0.113.10' \
|
||||
' protocol: tcp' >"$mutation_dir/relay.cn.yaml"
|
||||
sh "$artifact_dir/$deployment_checker" --syntax-only \
|
||||
"$artifact_dir/$config_example" "$mutation_dir/locator.cn.yaml" \
|
||||
"$mutation_dir/relay.cn.yaml" >/dev/null
|
||||
sed 's/203\.0\.113\.10/203.0.113.11/' "$mutation_dir/relay.cn.yaml" \
|
||||
>"$mutation_dir/relay.cn.mismatch.yaml"
|
||||
if sh "$artifact_dir/$deployment_checker" --syntax-only \
|
||||
"$artifact_dir/$config_example" "$mutation_dir/locator.cn.yaml" \
|
||||
"$mutation_dir/relay.cn.mismatch.yaml" >/dev/null 2>&1
|
||||
then
|
||||
echo "CN firewall validation: mismatched CN overlay bind was accepted" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
actual_locator_overlay="$script_dir/compose.production.cn.yaml"
|
||||
actual_relay_overlay="$script_dir/../collab-relay/compose.production.cn.yaml"
|
||||
actual_service_ipv4=$(awk '$1 == "host_ip:" { print $2 }' \
|
||||
"$actual_locator_overlay")
|
||||
sed "s/OPENPENCIL_CN_SERVICE_IPV4=203\.0\.113\.10/OPENPENCIL_CN_SERVICE_IPV4=$actual_service_ipv4/" \
|
||||
"$artifact_dir/$config_example" >"$mutation_dir/actual.env"
|
||||
sh "$artifact_dir/$deployment_checker" --syntax-only \
|
||||
"$mutation_dir/actual.env" "$actual_locator_overlay" \
|
||||
"$actual_relay_overlay" >/dev/null
|
||||
|
||||
sed 's/$i == "-g"/$i == "-x"/' \
|
||||
"$artifact_dir/$reference_checker" >"$mutation_dir/$reference_checker.tmp"
|
||||
mv "$mutation_dir/$reference_checker.tmp" "$mutation_dir/$reference_checker"
|
||||
if assert_reference_guard "$mutation_dir" >/dev/null 2>&1; then
|
||||
echo "CN firewall validation: lost goto-reference check was accepted" >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "$artifact_dir/$reference_checker" "$mutation_dir/$reference_checker"
|
||||
|
||||
sed 's/address_and_prefix\[1\] == expected_ipv4/address_and_prefix[1] != expected_ipv4/' \
|
||||
"$artifact_dir/$address_checker" >"$mutation_dir/$address_checker.tmp"
|
||||
mv "$mutation_dir/$address_checker.tmp" "$mutation_dir/$address_checker"
|
||||
if assert_address_guard "$mutation_dir" >/dev/null 2>&1; then
|
||||
echo "CN firewall validation: lost exact live-address check was accepted" >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "$artifact_dir/$address_checker" "$mutation_dir/$address_checker"
|
||||
|
||||
sed 's/source_ipv4 == expected_source/source_ipv4 != expected_source/' \
|
||||
"$artifact_dir/$managed_chain_checker" \
|
||||
>"$mutation_dir/$managed_chain_checker.tmp"
|
||||
mv "$mutation_dir/$managed_chain_checker.tmp" \
|
||||
"$mutation_dir/$managed_chain_checker"
|
||||
if assert_managed_chain_guard "$mutation_dir" >/dev/null 2>&1; then
|
||||
echo "CN firewall validation: weakened dormant-chain ownership check was accepted" >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "$artifact_dir/$managed_chain_checker" \
|
||||
"$mutation_dir/$managed_chain_checker"
|
||||
|
||||
sed 's/value == expected_ipv4 || value == expected_ipv4 "\/32"/value == expected_ipv4 "\/32"/' \
|
||||
"$artifact_dir/$managed_chain_checker" \
|
||||
>"$mutation_dir/$managed_chain_checker.tmp"
|
||||
mv "$mutation_dir/$managed_chain_checker.tmp" \
|
||||
"$mutation_dir/$managed_chain_checker"
|
||||
if assert_managed_chain_guard "$mutation_dir" >/dev/null 2>&1; then
|
||||
echo "CN firewall validation: lost xtables singleton normalization was accepted" >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "$artifact_dir/$managed_chain_checker" \
|
||||
"$mutation_dir/$managed_chain_checker"
|
||||
|
||||
{
|
||||
sed -n '1,$p' "$artifact_dir/$install_file"
|
||||
printf '%s\n' 'systemctl restart docker.service'
|
||||
} >"$mutation_dir/$install_file"
|
||||
if validate_artifacts "$mutation_dir" >/dev/null 2>&1; then
|
||||
echo "CN firewall validation: active-Docker restart mutation was accepted" >&2
|
||||
exit 1
|
||||
fi
|
||||
artifact_dir=$release_artifact_dir
|
||||
cp "$release_artifact_dir/$install_file" "$mutation_dir/$install_file"
|
||||
|
||||
sed 's/--ctorigdstport 8092/--dport 8092/g' \
|
||||
"$release_artifact_dir/$apply_file" >"$mutation_dir/$apply_file.tmp"
|
||||
mv "$mutation_dir/$apply_file.tmp" "$mutation_dir/$apply_file"
|
||||
if validate_artifacts "$mutation_dir" >/dev/null 2>&1; then
|
||||
echo "CN firewall validation: lost original-destination match was accepted" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "CN Docker ingress firewall artifacts validated"
|
||||
|
|
@ -3,9 +3,27 @@ set -eu
|
|||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
compose_file="$script_dir/compose.yaml"
|
||||
locator_global="$script_dir/compose.production.global.yaml"
|
||||
locator_cn="$script_dir/compose.production.cn.yaml"
|
||||
dockerfile="$script_dir/Dockerfile"
|
||||
nginx_config="$script_dir/nginx-location.conf"
|
||||
nginx_limits="$script_dir/nginx-http-limits.conf"
|
||||
nginx_direct_location="$script_dir/nginx-location-direct.conf"
|
||||
nginx_direct_global="$script_dir/nginx-http-direct.conf"
|
||||
nginx_direct_cn="$script_dir/nginx-http-direct-cn-gateway.conf"
|
||||
relay_dir="$script_dir/../collab-relay"
|
||||
relay_compose="$relay_dir/compose.yaml"
|
||||
relay_production="$relay_dir/compose.production.yaml"
|
||||
relay_global="$relay_dir/compose.production.global.yaml"
|
||||
relay_cn="$relay_dir/compose.production.cn.yaml"
|
||||
relay_direct_global="$relay_dir/nginx-http-direct.conf"
|
||||
relay_direct_cn="$relay_dir/nginx-http-direct-cn-gateway.conf"
|
||||
hsm_dir="$script_dir/../collab-relay-locator-hsm"
|
||||
hsm_compose="$hsm_dir/compose.yaml"
|
||||
hsm_overlay="$script_dir/compose.hsm.yaml"
|
||||
hsm_dockerfile="$hsm_dir/Dockerfile"
|
||||
hsm_tmpfiles="$hsm_dir/openpencil-locator-hsm.conf"
|
||||
cn_firewall_validator="$script_dir/validate-cn-docker-user-firewall.sh"
|
||||
|
||||
require_literal() {
|
||||
pattern=$1
|
||||
|
|
@ -13,6 +31,8 @@ require_literal() {
|
|||
grep -F "$pattern" "$file" >/dev/null
|
||||
}
|
||||
|
||||
sh "$cn_firewall_validator" "$script_dir"
|
||||
|
||||
for pattern in \
|
||||
'ENTRYPOINT ["/usr/local/bin/op-collab-relay-locator-server", "--production"]' \
|
||||
'FROM rust:1.94-bookworm@sha256:6ae102bdbf528294bc79ad6e1fae682f6f7c2a6e6621506ba959f9685b308a55 AS build' \
|
||||
|
|
@ -22,6 +42,29 @@ do
|
|||
require_literal "$pattern" "$dockerfile"
|
||||
done
|
||||
|
||||
for pattern in \
|
||||
'cargo build --locked --release -p op-collab-relay-locator-hsm' \
|
||||
'USER ${SIGNER_UID}:${SHARED_GID}' \
|
||||
'HEALTHCHECK --interval=30s' \
|
||||
'CMD ["serve", "--config", "/run/openpencil-config/locator-hsm.json"]'
|
||||
do
|
||||
require_literal "$pattern" "$hsm_dockerfile"
|
||||
done
|
||||
|
||||
for pattern in \
|
||||
'network_mode: none' \
|
||||
'read_only: true' \
|
||||
'no-new-privileges:true' \
|
||||
'target: /run/secrets/locator-hsm-pin' \
|
||||
'target: /var/lib/openpencil-softhsm/tokens' \
|
||||
'target: /run/openpencil-hsm'
|
||||
do
|
||||
require_literal "$pattern" "$hsm_compose"
|
||||
done
|
||||
|
||||
require_literal 'condition: service_healthy' "$hsm_overlay"
|
||||
require_literal 'd /run/openpencil/locator-hsm 0770 root 65532 -' "$hsm_tmpfiles"
|
||||
|
||||
for pattern in \
|
||||
'read_only: true' \
|
||||
'cap_drop:' \
|
||||
|
|
@ -32,25 +75,37 @@ do
|
|||
require_literal "$pattern" "$compose_file"
|
||||
done
|
||||
|
||||
require_literal 'OPENPENCIL_COLLAB_LOCATOR_HOME_REGION: global' "$locator_global"
|
||||
require_literal 'OPENPENCIL_COLLAB_LOCATOR_CLIENT_RATE_PER_SECOND: "100"' \
|
||||
"$locator_global"
|
||||
require_literal 'host_ip: 127.0.0.1' "$locator_global"
|
||||
require_literal 'target: 8092' "$locator_global"
|
||||
require_literal 'published: "8092"' "$locator_global"
|
||||
require_literal 'OPENPENCIL_COLLAB_LOCATOR_HOME_REGION: cn' "$locator_cn"
|
||||
require_literal 'OPENPENCIL_COLLAB_LOCATOR_CLIENT_RATE_PER_SECOND: "100"' \
|
||||
"$locator_cn"
|
||||
require_literal 'host_ip: 10.0.0.10' "$locator_cn"
|
||||
require_literal 'target: 8092' "$locator_cn"
|
||||
require_literal 'published: "8092"' "$locator_cn"
|
||||
|
||||
require_literal 'OPENPENCIL_COLLAB_RELAY_HOME_REGION: global' "$relay_global"
|
||||
require_literal 'OPENPENCIL_COLLAB_RELAY_MAX_PENDING_PER_SOURCE: "1024"' \
|
||||
"$relay_global"
|
||||
require_literal 'host_ip: 127.0.0.1' "$relay_global"
|
||||
require_literal 'target: 8091' "$relay_global"
|
||||
require_literal 'published: "8091"' "$relay_global"
|
||||
require_literal 'OPENPENCIL_COLLAB_RELAY_HOME_REGION: cn' "$relay_cn"
|
||||
require_literal 'OPENPENCIL_COLLAB_RELAY_MAX_PENDING_PER_SOURCE: "1024"' \
|
||||
"$relay_cn"
|
||||
require_literal 'host_ip: 10.0.0.10' "$relay_cn"
|
||||
require_literal 'target: 8091' "$relay_cn"
|
||||
require_literal 'published: "8091"' "$relay_cn"
|
||||
|
||||
for pattern in \
|
||||
'location = /v1/locator {' \
|
||||
'if ($request_uri != "/v1/locator") {' \
|
||||
'if ($http_host = "") {' \
|
||||
'client_max_body_size 191;' \
|
||||
'location = /v1/pairing-code {' \
|
||||
'if ($request_uri != "/v1/pairing-code") {' \
|
||||
'client_max_body_size 624;' \
|
||||
'client_body_buffer_size 624;' \
|
||||
'application/vnd.openpencil.relay-pairing-publish-v1' \
|
||||
'proxy_pass http://locator:8092/v1/pairing-code;' \
|
||||
'location = /v1/pairing-code/claim {' \
|
||||
'if ($request_uri != "/v1/pairing-code/claim") {' \
|
||||
'client_max_body_size 49;' \
|
||||
'client_body_buffer_size 49;' \
|
||||
'if ($content_length != "49") {' \
|
||||
'application/vnd.openpencil.relay-pairing-claim-v1' \
|
||||
'application/vnd.openpencil.relay-sealed-invite-v1' \
|
||||
'proxy_pass http://locator:8092/v1/pairing-code/claim;' \
|
||||
'proxy_set_header Authorization $http_authorization;' \
|
||||
'proxy_set_header Host $http_host;' \
|
||||
'proxy_buffering off;' \
|
||||
|
|
@ -62,29 +117,12 @@ do
|
|||
require_literal "$pattern" "$nginx_config"
|
||||
done
|
||||
|
||||
for counted_pattern in \
|
||||
'limit_except POST {' \
|
||||
'if ($http_transfer_encoding != "") {' \
|
||||
'if ($http_content_encoding != "") {' \
|
||||
'proxy_pass_request_headers off;' \
|
||||
'proxy_set_header Transfer-Encoding "";' \
|
||||
'proxy_set_header Content-Encoding "";' \
|
||||
'proxy_request_buffering on;'
|
||||
do
|
||||
if [ "$(grep -Fc -- "$counted_pattern" "$nginx_config" || true)" -ne 2 ]; then
|
||||
echo "pairing ingress must enforce $counted_pattern on both routes" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$(grep -Ec '^location = /v1/' "$nginx_config" || true)" -ne 3 ]; then
|
||||
echo "locator ingress must expose exactly three /v1 routes" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(grep -Ec '^[[:space:]]*proxy_pass[[:space:]]+http://locator:8092/' "$nginx_config" || true)" -ne 4 ]; then
|
||||
echo "locator ingress must use exactly four fixed locator upstream routes" >&2
|
||||
exit 1
|
||||
fi
|
||||
require_literal 'proxy_pass http://openpencil_collab_locator/v1/locator;' \
|
||||
"$nginx_direct_location"
|
||||
require_literal 'server 127.0.0.1:8092;' "$nginx_direct_global"
|
||||
require_literal 'server 10.0.0.10:8092;' "$nginx_direct_cn"
|
||||
require_literal 'server 127.0.0.1:8091;' "$relay_direct_global"
|
||||
require_literal 'server 10.0.0.10:8091;' "$relay_direct_cn"
|
||||
|
||||
for pattern in \
|
||||
'limit_req_zone $binary_remote_addr zone=openpencil_locator_per_source:10m rate=10r/s;' \
|
||||
|
|
@ -105,10 +143,40 @@ do
|
|||
done
|
||||
|
||||
if grep -Eq \
|
||||
'^[[:space:]]*(ports:|privileged:|network_mode:[[:space:]]*host)' \
|
||||
'^[[:space:]]*(privileged:|network_mode:[[:space:]]*host)' \
|
||||
"$compose_file"
|
||||
then
|
||||
echo "locator container must not publish a host port or gain host privileges" >&2
|
||||
echo "locator container must not gain host privileges" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -Eq '^[[:space:]]*ports:' "$compose_file" "$relay_compose" \
|
||||
"$relay_production" ||
|
||||
grep -Eq 'OPENPENCIL_COLLAB_(LOCATOR|RELAY)_(HOST_BIND|HOME_REGION)' \
|
||||
"$compose_file" "$relay_compose" "$relay_production" ||
|
||||
grep -Eq \
|
||||
'OPENPENCIL_COLLAB_LOCATOR_CLIENT_RATE_PER_SECOND|OPENPENCIL_COLLAB_RELAY_MAX_PENDING_PER_SOURCE' \
|
||||
"$compose_file" "$relay_compose" "$relay_production" ||
|
||||
grep -F '${' "$locator_global" "$locator_cn" "$relay_global" \
|
||||
"$relay_cn" >/dev/null
|
||||
then
|
||||
echo "common Compose must not publish ports; regional overlays must be immutable" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -Eq 'host_ip:[[:space:]]*(0\.0\.0\.0|::)' \
|
||||
"$locator_global" "$locator_cn" "$relay_global" "$relay_cn"
|
||||
then
|
||||
echo "regional production overlays must not bind wildcard addresses" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -Eq \
|
||||
'server[[:space:]]+(relay|locator):809[12]|proxy_pass[[:space:]]+http://(relay|locator):809[12]' \
|
||||
"$nginx_direct_location" "$nginx_direct_global" "$nginx_direct_cn" \
|
||||
"$relay_direct_global" "$relay_direct_cn"
|
||||
then
|
||||
echo "direct-host Nginx must not use Compose-only service DNS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -123,13 +191,91 @@ fi
|
|||
if command -v docker >/dev/null 2>&1 &&
|
||||
docker compose version >/dev/null 2>&1
|
||||
then
|
||||
OPENPENCIL_COLLAB_LOCATOR_HOME_REGION=cn \
|
||||
OPENPENCIL_COLLAB_LOCATOR_HSM_KEY_ID=locator-prod-2026-07 \
|
||||
OPENPENCIL_COLLAB_LOCATOR_HSM_PEER_UID=65532 \
|
||||
OPENPENCIL_COLLAB_LOCATOR_HSM_PEER_GID=65532 \
|
||||
OPENPENCIL_COLLAB_POLICY_HOST_FILE=/dev/null \
|
||||
OPENPENCIL_COLLAB_HSM_CONFIG_HOST_FILE=/dev/null \
|
||||
OPENPENCIL_COLLAB_HSM_PIN_HOST_FILE=/dev/null \
|
||||
OPENPENCIL_COLLAB_HSM_TOKEN_HOST_DIR=/tmp \
|
||||
OPENPENCIL_COLLAB_HSM_SOCKET_HOST_DIR=/tmp \
|
||||
docker compose -f "$compose_file" config -q
|
||||
docker compose -f "$hsm_compose" config -q
|
||||
|
||||
for region in global cn
|
||||
do
|
||||
case $region in
|
||||
global)
|
||||
bind=127.0.0.1
|
||||
hostile_region=cn
|
||||
locator_overlay=$locator_global
|
||||
relay_overlay=$relay_global
|
||||
;;
|
||||
cn)
|
||||
bind=10.0.0.10
|
||||
hostile_region=global
|
||||
locator_overlay=$locator_cn
|
||||
relay_overlay=$relay_cn
|
||||
;;
|
||||
esac
|
||||
locator_config=$(
|
||||
OPENPENCIL_COLLAB_LOCATOR_HOME_REGION=$hostile_region \
|
||||
OPENPENCIL_COLLAB_LOCATOR_HOST_BIND=0.0.0.0 \
|
||||
OPENPENCIL_COLLAB_LOCATOR_CLIENT_RATE_PER_SECOND=1 \
|
||||
OPENPENCIL_COLLAB_LOCATOR_HSM_KEY_ID=locator-prod-2026-07 \
|
||||
OPENPENCIL_COLLAB_LOCATOR_HSM_PEER_UID=65533 \
|
||||
OPENPENCIL_COLLAB_LOCATOR_HSM_PEER_GID=65532 \
|
||||
OPENPENCIL_COLLAB_POLICY_HOST_FILE=/dev/null \
|
||||
OPENPENCIL_COLLAB_HSM_SOCKET_HOST_DIR=/tmp \
|
||||
OPENPENCIL_COLLAB_HSM_CONFIG_HOST_FILE=/dev/null \
|
||||
OPENPENCIL_COLLAB_HSM_PIN_HOST_FILE=/dev/null \
|
||||
OPENPENCIL_COLLAB_HSM_TOKEN_HOST_DIR=/tmp \
|
||||
docker compose \
|
||||
-f "$hsm_compose" -f "$compose_file" \
|
||||
-f "$locator_overlay" -f "$hsm_overlay" config
|
||||
)
|
||||
printf '%s\n' "$locator_config" | grep -F "host_ip: $bind" >/dev/null
|
||||
printf '%s\n' "$locator_config" | grep -F 'target: 8092' >/dev/null
|
||||
printf '%s\n' "$locator_config" | grep -F 'published: "8092"' >/dev/null
|
||||
printf '%s\n' "$locator_config" |
|
||||
grep -F "OPENPENCIL_COLLAB_LOCATOR_HOME_REGION: $region" >/dev/null
|
||||
printf '%s\n' "$locator_config" |
|
||||
grep -F 'OPENPENCIL_COLLAB_LOCATOR_CLIENT_RATE_PER_SECOND: "100"' \
|
||||
>/dev/null
|
||||
if [ "$(printf '%s\n' "$locator_config" |
|
||||
grep -Ec '^[[:space:]]+host_ip:')" -ne 1 ] ||
|
||||
printf '%s\n' "$locator_config" |
|
||||
grep -E '^[[:space:]]+host_ip:' |
|
||||
grep -Fv "host_ip: $bind" >/dev/null
|
||||
then
|
||||
echo "resolved locator bind is not the one immutable regional address" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
relay_config=$(
|
||||
OPENPENCIL_COLLAB_RELAY_HOME_REGION=$hostile_region \
|
||||
OPENPENCIL_COLLAB_RELAY_HOST_BIND=0.0.0.0 \
|
||||
OPENPENCIL_COLLAB_RELAY_MAX_PENDING_PER_SOURCE=1 \
|
||||
OPENPENCIL_COLLAB_POLICY_HOST_FILE=/dev/null \
|
||||
OPENPENCIL_RELAY_LOCATOR_KEYS_HOST_FILE=/dev/null \
|
||||
OPENPENCIL_RELAY_X25519_KEYS_HOST_FILE=/dev/null \
|
||||
docker compose \
|
||||
-f "$relay_compose" -f "$relay_production" \
|
||||
-f "$relay_overlay" config
|
||||
)
|
||||
printf '%s\n' "$relay_config" | grep -F "host_ip: $bind" >/dev/null
|
||||
printf '%s\n' "$relay_config" | grep -F 'target: 8091' >/dev/null
|
||||
printf '%s\n' "$relay_config" | grep -F 'published: "8091"' >/dev/null
|
||||
printf '%s\n' "$relay_config" |
|
||||
grep -F "OPENPENCIL_COLLAB_RELAY_HOME_REGION: $region" >/dev/null
|
||||
printf '%s\n' "$relay_config" |
|
||||
grep -F 'OPENPENCIL_COLLAB_RELAY_MAX_PENDING_PER_SOURCE: "1024"' \
|
||||
>/dev/null
|
||||
if [ "$(printf '%s\n' "$relay_config" |
|
||||
grep -Ec '^[[:space:]]+host_ip:')" -ne 1 ] ||
|
||||
printf '%s\n' "$relay_config" |
|
||||
grep -E '^[[:space:]]+host_ip:' |
|
||||
grep -Fv "host_ip: $bind" >/dev/null
|
||||
then
|
||||
echo "resolved relay bind is not the one immutable regional address" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo "collab relay locator deployment validation passed"
|
||||
|
|
|
|||
151
deploy/collab-relay-locator/verify-cn-docker-user-firewall.sh
Executable file
151
deploy/collab-relay-locator/verify-cn-docker-user-firewall.sh
Executable file
|
|
@ -0,0 +1,151 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
LC_ALL=C
|
||||
PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
||||
export LC_ALL PATH
|
||||
|
||||
runtime_dir=/usr/local/libexec/openpencil-collab-cn-firewall
|
||||
config_file=/etc/openpencil/collab-cn-firewall.env
|
||||
binding_file=/etc/openpencil/collab-cn-firewall-compose.env
|
||||
chain_name=OPENPENCIL-CN-INGRESS
|
||||
anchor_comment=openpencil-cn-ingress-v1
|
||||
allow_relay_comment=openpencil-cn-allow-8091-v1
|
||||
drop_relay_comment=openpencil-cn-drop-8091-v1
|
||||
allow_locator_comment=openpencil-cn-allow-8092-v1
|
||||
drop_locator_comment=openpencil-cn-drop-8092-v1
|
||||
fallthrough_comment=openpencil-cn-fallthrough-v1
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "CN firewall: kernel verification requires root" >&2
|
||||
exit 1
|
||||
fi
|
||||
for command_name in stat ip iptables-save awk flock; do
|
||||
command -v "$command_name" >/dev/null 2>&1 || {
|
||||
echo "CN firewall: required command is unavailable: $command_name" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
common_file="$script_dir/cn-docker-user-firewall-common.sh"
|
||||
reference_checker="$script_dir/check-cn-docker-user-firewall-references.awk"
|
||||
address_checker="$script_dir/check-cn-docker-user-firewall-interface-address.awk"
|
||||
managed_chain_checker="$script_dir/check-cn-docker-user-firewall-managed-chain.awk"
|
||||
if [ "$script_dir" != "$runtime_dir" ] || [ -L "$script_dir" ] ||
|
||||
[ "$(stat -c '%u:%g:%a' -- "$script_dir")" != '0:0:755' ] ||
|
||||
[ -L "$0" ] || [ "$(stat -c '%u:%g:%a' -- "$0")" != '0:0:755' ] ||
|
||||
[ ! -f "$common_file" ] || [ -L "$common_file" ] ||
|
||||
[ "$(stat -c '%u:%g:%a' -- "$common_file")" != '0:0:644' ] ||
|
||||
[ ! -f "$reference_checker" ] || [ -L "$reference_checker" ] ||
|
||||
[ "$(stat -c '%u:%g:%a' -- "$reference_checker")" != '0:0:644' ] ||
|
||||
[ ! -f "$address_checker" ] || [ -L "$address_checker" ] ||
|
||||
[ "$(stat -c '%u:%g:%a' -- "$address_checker")" != '0:0:644' ] ||
|
||||
[ ! -f "$managed_chain_checker" ] || [ -L "$managed_chain_checker" ] ||
|
||||
[ "$(stat -c '%u:%g:%a' -- "$managed_chain_checker")" != '0:0:644' ]
|
||||
then
|
||||
echo "CN firewall: installed runtime must be immutable root-owned files" >&2
|
||||
exit 1
|
||||
fi
|
||||
. "$common_file"
|
||||
|
||||
case ${1-} in
|
||||
'') cn_fw_verify_mode=kernel; cn_fw_verify_config=$config_file ;;
|
||||
--pre-docker)
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "usage: $0 [--pre-docker|--config-only ROOT_OWNED_INVENTORY_FILE]" >&2
|
||||
exit 2
|
||||
fi
|
||||
cn_fw_verify_mode=pre-docker
|
||||
cn_fw_verify_config=$config_file
|
||||
;;
|
||||
--config-only)
|
||||
if [ "$#" -ne 2 ]; then
|
||||
echo "usage: $0 [--pre-docker|--config-only ROOT_OWNED_INVENTORY_FILE]" >&2
|
||||
exit 2
|
||||
fi
|
||||
cn_fw_verify_mode=config-only
|
||||
cn_fw_verify_config=$2
|
||||
;;
|
||||
*)
|
||||
echo "usage: $0 [--pre-docker|--config-only ROOT_OWNED_INVENTORY_FILE]" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
cn_firewall_load_config "$cn_fw_verify_config" strict
|
||||
if [ "$cn_fw_verify_mode" = config-only ]; then
|
||||
echo "root-owned CN firewall inventory is valid"
|
||||
exit 0
|
||||
fi
|
||||
cn_firewall_load_deployment_binding "$binding_file"
|
||||
if ! ip link show dev "$OPENPENCIL_CN_INGRESS_INTERFACE" >/dev/null 2>&1; then
|
||||
cn_firewall_fail "configured ingress interface does not exist"
|
||||
exit 1
|
||||
fi
|
||||
cn_fw_interface_addresses=$(ip -4 -o addr show dev \
|
||||
"$OPENPENCIL_CN_INGRESS_INTERFACE") || {
|
||||
cn_firewall_fail "could not inspect configured ingress-interface addresses"
|
||||
exit 1
|
||||
}
|
||||
cn_fw_service_assignments=$(printf '%s\n' "$cn_fw_interface_addresses" | awk \
|
||||
-v expected_ipv4="$OPENPENCIL_CN_SERVICE_IPV4" -f "$address_checker")
|
||||
if [ "$cn_fw_service_assignments" -ne 1 ]; then
|
||||
cn_firewall_fail \
|
||||
"configured service IPv4 must be assigned exactly once to the ingress interface"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec 9>/run/openpencil-collab-cn-firewall.lock
|
||||
flock -s 9
|
||||
filter_snapshot=$(iptables-save -t filter)
|
||||
anchor_summary=$(printf '%s\n' "$filter_snapshot" | awk \
|
||||
-v managed_chain="$chain_name" -v anchor_comment="$anchor_comment" \
|
||||
-f "$reference_checker")
|
||||
if [ "$anchor_summary" != '1 1 1 0' ]; then
|
||||
cn_firewall_fail \
|
||||
"managed chain requires one first DOCKER-USER jump and no foreign jump/goto references"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$cn_fw_verify_mode" = kernel ]; then
|
||||
forward_summary=$(printf '%s\n' "$filter_snapshot" | awk '
|
||||
$1 == "-A" && $2 == "FORWARD" {
|
||||
forward_rules++;
|
||||
jump = "";
|
||||
for (i = 3; i <= NF; i++) {
|
||||
if ($i == "-j" && i < NF) jump = $(i + 1);
|
||||
}
|
||||
if (jump == "DOCKER-USER") {
|
||||
targets++;
|
||||
if (NF == 4 && $3 == "-j" && $4 == "DOCKER-USER") {
|
||||
canonical++;
|
||||
}
|
||||
if (forward_rules == 1) first = 1;
|
||||
}
|
||||
}
|
||||
END { print targets + 0, canonical + 0, first + 0 }
|
||||
')
|
||||
if [ "$forward_summary" != '1 1 1' ]; then
|
||||
cn_firewall_fail \
|
||||
"Docker must place one first-position DOCKER-USER jump in FORWARD"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
managed_summary=$(printf '%s\n' "$filter_snapshot" | awk \
|
||||
-v managed_chain="$chain_name" \
|
||||
-v ingress_interface="$OPENPENCIL_CN_INGRESS_INTERFACE" \
|
||||
-v gateway_ipv4="$OPENPENCIL_CN_GATEWAY_SOURCE_IPV4" \
|
||||
-v service_ipv4="$OPENPENCIL_CN_SERVICE_IPV4" \
|
||||
-v allow_relay_comment="$allow_relay_comment" \
|
||||
-v drop_relay_comment="$drop_relay_comment" \
|
||||
-v allow_locator_comment="$allow_locator_comment" \
|
||||
-v drop_locator_comment="$drop_locator_comment" \
|
||||
-v fallthrough_comment="$fallthrough_comment" \
|
||||
-f "$managed_chain_checker")
|
||||
if [ "$managed_summary" != '5 5 5 0' ]; then
|
||||
cn_firewall_fail "managed chain predicates, order, or rule count are not exact"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "active CN Docker ingress firewall verified"
|
||||
|
|
@ -108,6 +108,12 @@ Private encodings, shared secrets, and derived keys are zeroized.
|
|||
|
||||
## Deployment
|
||||
|
||||
On Unix, the public signed-policy file may be owned either by the relay's
|
||||
effective UID or by root. It must be a regular non-symlink file and must not be
|
||||
group- or world-writable. Because the policy contains public verification
|
||||
material, root-owned mode `0440` (when the container identity has group read)
|
||||
or `0444` is accepted; writable modes fail closed.
|
||||
|
||||
Run full challenge-bound production mode:
|
||||
|
||||
```sh
|
||||
|
|
@ -119,9 +125,13 @@ sudo chmod 0400 "$OPENPENCIL_RELAY_X25519_KEYS_HOST_FILE"
|
|||
docker compose \
|
||||
-f deploy/collab-relay/compose.yaml \
|
||||
-f deploy/collab-relay/compose.production.yaml \
|
||||
-f deploy/collab-relay/compose.production.global.yaml \
|
||||
up --build
|
||||
```
|
||||
|
||||
That command is for Global. On CN, replace only the final overlay with
|
||||
`compose.production.cn.yaml`. Never combine the two regional overlays.
|
||||
|
||||
The secret is mounted read-only under `/run/secrets`. The packaged image runs
|
||||
as UID/GID `65532`, so the host file must actually be owner-readable by that
|
||||
identity and inaccessible to group/other users. Docker Compose silently
|
||||
|
|
@ -167,9 +177,86 @@ docker compose \
|
|||
|
||||
Never expose that development mode to the Internet.
|
||||
|
||||
## China relay for overseas peers
|
||||
## Direct regional public paths
|
||||
|
||||
For a locator with `home_region=cn`, domestic clients connect to the normal CN
|
||||
The default two-region deployment uses one public application host per region,
|
||||
with both collaboration paths on that host:
|
||||
|
||||
```text
|
||||
CN https://<cn-public-host>/v1/locator
|
||||
wss://<cn-public-host>/v1/tunnel
|
||||
Global https://<global-public-host>/v1/locator
|
||||
wss://<global-public-host>/v1/tunnel
|
||||
```
|
||||
|
||||
Keep the concrete public hosts in the private deployment inventory and signed
|
||||
desktop bootstrap, not in this public repository. The two Compose projects use
|
||||
separate networks, so host Nginx must never use Docker service names such as
|
||||
`relay` or `locator` as upstreams. The common Compose files publish no host
|
||||
ports. Immutable regional overlays bind both the home region and the only
|
||||
permitted host address; there is no host-bind variable or wildcard default.
|
||||
They also fix `OPENPENCIL_COLLAB_RELAY_MAX_PENDING_PER_SOURCE=1024`, equal to
|
||||
the process-wide pending ceiling. The common Compose files do not set this
|
||||
production override.
|
||||
|
||||
That equality is intentional. A connection crossing a Docker-published host
|
||||
port reaches the relay with a Docker NAT peer address, so independent Internet
|
||||
clients can collapse into one application-visible source. The relay must not
|
||||
apply its normal per-source bucket to that shared address. Host Nginx is the
|
||||
trusted real-source boundary: its handshake-rate and connection zones enforce
|
||||
per-client limits before proxying, while the relay still enforces the global
|
||||
1024-pending ceiling. If another load balancer precedes Nginx, restore the
|
||||
client address only from its fixed trusted addresses; never trust arbitrary
|
||||
forwarded-address headers.
|
||||
|
||||
On the Global application host, use only:
|
||||
|
||||
```text
|
||||
deploy/collab-relay/compose.production.global.yaml
|
||||
deploy/collab-relay-locator/compose.production.global.yaml
|
||||
```
|
||||
|
||||
They hard-code `home_region=global` and loopback ports `127.0.0.1:8091` and
|
||||
`127.0.0.1:8092`. At Global Nginx `http` scope include
|
||||
`../collab-relay-locator/nginx-http-limits.conf`,
|
||||
`nginx-http-direct.conf`, and
|
||||
`../collab-relay-locator/nginx-http-direct.conf` exactly once.
|
||||
|
||||
On the CN application service host, use only:
|
||||
|
||||
```text
|
||||
deploy/collab-relay/compose.production.cn.yaml
|
||||
deploy/collab-relay-locator/compose.production.cn.yaml
|
||||
```
|
||||
|
||||
They hard-code `home_region=cn` and private ports `10.0.0.10:8091` and
|
||||
`10.0.0.10:8092`. The CN front gateway includes the same locator limits plus
|
||||
`nginx-http-direct-cn-gateway.conf` and
|
||||
`../collab-relay-locator/nginx-http-direct-cn-gateway.conf` at `http` scope.
|
||||
Those upstreams are explicitly `10.0.0.10:8091` and `10.0.0.10:8092`.
|
||||
Restrict both ports on the service-host firewall to the configured front
|
||||
gateway source addresses. Never bind either port to `0.0.0.0` or expose it on
|
||||
a public interface. Do not copy a regional overlay to the other region, combine
|
||||
the two overlays, or add a Compose `ports` override.
|
||||
|
||||
Inside each regional application TLS virtual host include
|
||||
`../collab-relay-locator/nginx-location-direct.conf` and
|
||||
`nginx-location.conf`. These files intentionally define only the two exact
|
||||
collaboration paths, so they do not replace the application's normal routes.
|
||||
The standalone relay `nginx.conf` and dedicated-host locator
|
||||
`nginx-location.conf` must not be combined with the direct-host snippets.
|
||||
|
||||
A Global user who explicitly selects CN uses the CN application host directly
|
||||
for both paths. The signed locator remains `home_region=cn`; the Global relay
|
||||
is not a fallback and does not proxy that session. This topology does not
|
||||
require an L4 federation edge. Confirm the two externally published URL pairs
|
||||
and their certificates from the private inventory before rollout.
|
||||
|
||||
## Optional China relay for overseas peers
|
||||
|
||||
This optional topology is not part of the direct regional deployment above.
|
||||
Use it only after separately deploying and reviewing the L4 edge. For a
|
||||
locator with `home_region=cn`, domestic clients connect to the normal CN
|
||||
WSS endpoint. Overseas clients may instead resolve a Global ingress that is
|
||||
only an L4 passthrough:
|
||||
|
||||
|
|
|
|||
10
deploy/collab-relay/compose.production.cn.yaml
Normal file
10
deploy/collab-relay/compose.production.cn.yaml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
services:
|
||||
relay:
|
||||
environment:
|
||||
OPENPENCIL_COLLAB_RELAY_HOME_REGION: cn
|
||||
OPENPENCIL_COLLAB_RELAY_MAX_PENDING_PER_SOURCE: "1024"
|
||||
ports:
|
||||
- target: 8091
|
||||
published: "8091"
|
||||
host_ip: 10.0.0.10
|
||||
protocol: tcp
|
||||
10
deploy/collab-relay/compose.production.global.yaml
Normal file
10
deploy/collab-relay/compose.production.global.yaml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
services:
|
||||
relay:
|
||||
environment:
|
||||
OPENPENCIL_COLLAB_RELAY_HOME_REGION: global
|
||||
OPENPENCIL_COLLAB_RELAY_MAX_PENDING_PER_SOURCE: "1024"
|
||||
ports:
|
||||
- target: 8091
|
||||
published: "8091"
|
||||
host_ip: 127.0.0.1
|
||||
protocol: tcp
|
||||
|
|
@ -3,7 +3,6 @@ services:
|
|||
command:
|
||||
- --production
|
||||
environment:
|
||||
OPENPENCIL_COLLAB_RELAY_HOME_REGION: cn
|
||||
OPENPENCIL_COLLAB_RELAY_TICKET_POLICY_FILE: /run/secrets/openpencil-collab-policy.json
|
||||
OPENPENCIL_COLLAB_RELAY_LOCATOR_KEYS_FILE: /run/secrets/openpencil-relay-locator-keys.json
|
||||
OPENPENCIL_COLLAB_RELAY_X25519_KEYS_FILE: /run/secrets/openpencil-relay-x25519-keys.json
|
||||
|
|
|
|||
15
deploy/collab-relay/nginx-http-direct-cn-gateway.conf
Normal file
15
deploy/collab-relay/nginx-http-direct-cn-gateway.conf
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Include exactly once in the CN front gateway's Nginx `http` block. The CN
|
||||
# application service host is the fixed private address 10.0.0.10.
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
limit_req_zone $binary_remote_addr zone=relay_handshakes:10m rate=30r/m;
|
||||
limit_conn_zone $binary_remote_addr zone=relay_connections:10m;
|
||||
|
||||
upstream openpencil_collab_relay {
|
||||
server 10.0.0.10:8091;
|
||||
keepalive 64;
|
||||
}
|
||||
16
deploy/collab-relay/nginx-http-direct.conf
Normal file
16
deploy/collab-relay/nginx-http-direct.conf
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Include exactly once in the Nginx `http` block when the relay paths share an
|
||||
# application TLS virtual host. The standalone nginx.conf already declares
|
||||
# equivalent resources and must not be combined with this snippet.
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
limit_req_zone $binary_remote_addr zone=relay_handshakes:10m rate=30r/m;
|
||||
limit_conn_zone $binary_remote_addr zone=relay_connections:10m;
|
||||
|
||||
upstream openpencil_collab_relay {
|
||||
server 127.0.0.1:8091;
|
||||
keepalive 64;
|
||||
}
|
||||
27
deploy/collab-relay/nginx-location.conf
Normal file
27
deploy/collab-relay/nginx-location.conf
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Include inside the regional application TLS server after including
|
||||
# the matching Global-host or CN-front-gateway `nginx-http-direct*.conf` once
|
||||
# at `http` scope. The Compose host bind must match that topology.
|
||||
|
||||
location = /v1/tunnel {
|
||||
if ($request_uri != "/v1/tunnel") {
|
||||
return 404;
|
||||
}
|
||||
if ($http_host = "") {
|
||||
return 400;
|
||||
}
|
||||
|
||||
access_log off;
|
||||
limit_req zone=relay_handshakes burst=10 nodelay;
|
||||
limit_conn relay_connections 8;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Authorization $http_authorization;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_pass_header OpenPencil-Relay-Challenge;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 43210s;
|
||||
proxy_send_timeout 43210s;
|
||||
proxy_pass http://openpencil_collab_relay;
|
||||
}
|
||||
|
|
@ -49,6 +49,9 @@ for required in \
|
|||
crates/op-collab-relay-control-plane/LICENSE \
|
||||
crates/op-collab-policy-file/Cargo.toml \
|
||||
crates/op-collab-policy-file/LICENSE \
|
||||
crates/op-collab-relay-locator-hsm/Cargo.toml \
|
||||
crates/op-collab-relay-locator-hsm/LICENSE \
|
||||
crates/op-collab-relay-locator-hsm/tests/softhsm.rs \
|
||||
crates/op-collab-relay-locator-server/Cargo.toml \
|
||||
crates/op-collab-relay-locator-server/LICENSE \
|
||||
crates/op-collab-smoke/LICENSE \
|
||||
|
|
@ -80,6 +83,12 @@ for required in \
|
|||
deploy/collab-relay-locator/nginx-http-limits.conf \
|
||||
deploy/collab-relay-locator/nginx-location.conf \
|
||||
deploy/collab-relay-locator/validate.sh \
|
||||
deploy/collab-relay-locator-hsm/Dockerfile \
|
||||
deploy/collab-relay-locator-hsm/README.md \
|
||||
deploy/collab-relay-locator-hsm/compose.yaml \
|
||||
deploy/collab-relay-locator-hsm/config.example.json \
|
||||
deploy/collab-relay-locator-hsm/openpencil-locator-hsm.conf \
|
||||
deploy/collab-relay-locator-hsm/softhsm2.conf \
|
||||
deploy/collab-relay-locator-edge/README.md \
|
||||
deploy/collab-relay-locator-edge/global-nginx.conf \
|
||||
deploy/collab-relay-locator-edge/cn-federation-nginx.conf \
|
||||
|
|
@ -134,6 +143,7 @@ for workflow_path in \
|
|||
"crates/op-collab-relay-server/**" \
|
||||
"crates/op-collab-relay-control-plane/**" \
|
||||
"crates/op-collab-policy-file/**" \
|
||||
"crates/op-collab-relay-locator-hsm/**" \
|
||||
"crates/op-collab-relay-locator-server/**" \
|
||||
"crates/op-util/**" \
|
||||
"crates/op-editor-core/**" \
|
||||
|
|
@ -146,7 +156,10 @@ for workflow_path in \
|
|||
"deploy/collab-relay/**" \
|
||||
"deploy/collab-relay-edge/**" \
|
||||
"deploy/collab-relay-locator/**" \
|
||||
"deploy/collab-relay-locator-edge/**"; do
|
||||
"deploy/collab-relay-locator-hsm/**" \
|
||||
"deploy/collab-relay-locator-edge/**" \
|
||||
"tools/check-collab-security-boundaries-cases.sh" \
|
||||
"tools/check-collab-deployment-boundaries.sh"; do
|
||||
require_literal_count .github/workflows/collab-security.yml \
|
||||
"$workflow_path" 2 "collaboration security workflow path trigger"
|
||||
done
|
||||
|
|
@ -337,6 +350,22 @@ done
|
|||
require_literal .github/workflows/collab-security.yml \
|
||||
"bash deploy/collab-relay-locator/validate.sh" \
|
||||
"locator deployment workflow validation"
|
||||
require_literal .github/workflows/collab-security.yml \
|
||||
"cargo test --locked -p op-collab-relay-locator-hsm" \
|
||||
"locator HSM crate workflow test"
|
||||
require_literal .github/workflows/collab-security.yml \
|
||||
"docker build --target test" \
|
||||
"real SoftHSM workflow test target"
|
||||
require_literal .github/workflows/collab-security.yml \
|
||||
"-f deploy/collab-relay-locator-hsm/Dockerfile ." \
|
||||
"real SoftHSM workflow test target"
|
||||
for locator_hsm_test_anchor in \
|
||||
"FROM build AS test" \
|
||||
"apt-get install -y --no-install-recommends softhsm2" \
|
||||
"cargo test --locked -p op-collab-relay-locator-hsm --test softhsm -- --nocapture"; do
|
||||
require_literal deploy/collab-relay-locator-hsm/Dockerfile \
|
||||
"$locator_hsm_test_anchor" "real SoftHSM image test boundary"
|
||||
done
|
||||
|
||||
for locator_edge_global_anchor in \
|
||||
"listen 8443;" \
|
||||
|
|
|
|||
|
|
@ -1,6 +1,86 @@
|
|||
# Mutation cases for check-collab-security-boundaries.test.sh.
|
||||
# Sourced after the fixture and assertion helpers have been initialized.
|
||||
|
||||
write_collab_security_workflow_fixture() {
|
||||
cat > "$fixture_root/.github/workflows/collab-security.yml" <<'EOF'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.dockerignore'
|
||||
- '.gitignore'
|
||||
- 'crates/op-collab-smoke/**'
|
||||
- 'crates/op-collab-relay-protocol/**'
|
||||
- 'crates/op-collab-relay-client/**'
|
||||
- 'crates/op-collab-relay-server/**'
|
||||
- 'crates/op-collab-relay-control-plane/**'
|
||||
- 'crates/op-collab-policy-file/**'
|
||||
- 'crates/op-collab-relay-locator-hsm/**'
|
||||
- 'crates/op-collab-relay-locator-server/**'
|
||||
- 'crates/op-util/**'
|
||||
- 'crates/op-editor-core/**'
|
||||
- 'crates/op-editor-host-core/**'
|
||||
- 'crates/op-editor-ui/**'
|
||||
- 'crates/op-host-native/**'
|
||||
- 'crates/op-host-desktop/**'
|
||||
- 'crates/op-host-services/**'
|
||||
- 'crates/op-i18n/**'
|
||||
- 'deploy/collab-relay/**'
|
||||
- 'deploy/collab-relay-edge/**'
|
||||
- 'deploy/collab-relay-locator/**'
|
||||
- 'deploy/collab-relay-locator-hsm/**'
|
||||
- 'deploy/collab-relay-locator-edge/**'
|
||||
- 'tools/check-collab-security-boundaries-cases.sh'
|
||||
- 'tools/check-collab-deployment-boundaries.sh'
|
||||
- 'tools/check-op-auth-prebuilt.sh'
|
||||
- 'tools/check-op-auth-prebuilt.test.sh'
|
||||
- 'tools/package-op-auth-prebuilt.sh'
|
||||
push:
|
||||
paths:
|
||||
- '.dockerignore'
|
||||
- '.gitignore'
|
||||
- 'crates/op-collab-smoke/**'
|
||||
- 'crates/op-collab-relay-protocol/**'
|
||||
- 'crates/op-collab-relay-client/**'
|
||||
- 'crates/op-collab-relay-server/**'
|
||||
- 'crates/op-collab-relay-control-plane/**'
|
||||
- 'crates/op-collab-policy-file/**'
|
||||
- 'crates/op-collab-relay-locator-hsm/**'
|
||||
- 'crates/op-collab-relay-locator-server/**'
|
||||
- 'crates/op-util/**'
|
||||
- 'crates/op-editor-core/**'
|
||||
- 'crates/op-editor-host-core/**'
|
||||
- 'crates/op-editor-ui/**'
|
||||
- 'crates/op-host-native/**'
|
||||
- 'crates/op-host-desktop/**'
|
||||
- 'crates/op-host-services/**'
|
||||
- 'crates/op-i18n/**'
|
||||
- 'deploy/collab-relay/**'
|
||||
- 'deploy/collab-relay-edge/**'
|
||||
- 'deploy/collab-relay-locator/**'
|
||||
- 'deploy/collab-relay-locator-hsm/**'
|
||||
- 'deploy/collab-relay-locator-edge/**'
|
||||
- 'tools/check-collab-security-boundaries-cases.sh'
|
||||
- 'tools/check-collab-deployment-boundaries.sh'
|
||||
- 'tools/check-op-auth-prebuilt.sh'
|
||||
- 'tools/check-op-auth-prebuilt.test.sh'
|
||||
- 'tools/package-op-auth-prebuilt.sh'
|
||||
steps:
|
||||
- run: bash tools/check-op-auth-prebuilt.sh
|
||||
- run: bash tools/check-op-auth-prebuilt.test.sh
|
||||
- run: bash -n tools/package-op-auth-prebuilt.sh
|
||||
- run: cargo test --locked -p op-auth-bridge --test prebuilt_provenance
|
||||
- run: cargo test --locked -p op-collab-transport
|
||||
- run: cargo test --locked -p op-collab-transport config::tests
|
||||
- run: cargo test --locked -p op-collab-transport frame::tests
|
||||
- run: cargo test --locked -p op-collab-relay-locator-hsm
|
||||
- run: |
|
||||
docker build --target test \
|
||||
-f deploy/collab-relay-locator-hsm/Dockerfile .
|
||||
- run: bash deploy/collab-relay-edge/validate.sh
|
||||
- run: bash deploy/collab-relay-locator/validate.sh
|
||||
- run: bash deploy/collab-relay-locator-edge/validate.sh
|
||||
EOF
|
||||
}
|
||||
|
||||
new_fixture baseline
|
||||
expect_pass "accepts the minimal safe collaboration boundary"
|
||||
|
||||
|
|
@ -134,6 +214,45 @@ mv \
|
|||
expect_failure "requires the credential transport codec workflow test" \
|
||||
"credential transport codec workflow test"
|
||||
|
||||
new_fixture complete-transport-workflow-test-removed
|
||||
sed '/cargo test --locked -p op-collab-transport$/d' \
|
||||
"$fixture_root/.github/workflows/collab-security.yml" \
|
||||
> "$fixture_root/.github/workflows/collab-security.yml.next"
|
||||
mv \
|
||||
"$fixture_root/.github/workflows/collab-security.yml.next" \
|
||||
"$fixture_root/.github/workflows/collab-security.yml"
|
||||
expect_failure "requires the complete transport resource-limit test suite" \
|
||||
"complete transport resource-limit workflow test"
|
||||
|
||||
new_fixture locator-hsm-workflow-tests-removed
|
||||
sed \
|
||||
-e '/cargo test --locked -p op-collab-relay-locator-hsm/d' \
|
||||
-e '/docker build --target test/d' \
|
||||
"$fixture_root/.github/workflows/collab-security.yml" \
|
||||
> "$fixture_root/.github/workflows/collab-security.yml.next"
|
||||
mv \
|
||||
"$fixture_root/.github/workflows/collab-security.yml.next" \
|
||||
"$fixture_root/.github/workflows/collab-security.yml"
|
||||
expect_failure "requires locator HSM unit and real SoftHSM workflow tests" \
|
||||
"locator HSM crate workflow test"
|
||||
|
||||
new_fixture locator-hsm-soft-token-target-removed
|
||||
sed 's/docker build --target test/docker build/' \
|
||||
"$fixture_root/.github/workflows/collab-security.yml" \
|
||||
> "$fixture_root/.github/workflows/collab-security.yml.next"
|
||||
mv \
|
||||
"$fixture_root/.github/workflows/collab-security.yml.next" \
|
||||
"$fixture_root/.github/workflows/collab-security.yml"
|
||||
expect_failure "requires the real SoftHSM Docker test stage" \
|
||||
"real SoftHSM workflow test target"
|
||||
|
||||
new_fixture locator-hsm-production-seed
|
||||
printf '%s\n' \
|
||||
'const PRODUCTION_SIGNING_SEED: [u8; 32] = [9; 32];' \
|
||||
>> "$fixture_root/crates/op-collab-relay-locator-hsm/src/lib.rs"
|
||||
expect_failure "scans the locator HSM crate for deterministic production keys" \
|
||||
"deterministic signing/key seed leaked"
|
||||
|
||||
new_fixture desktop-renewal-vec-copy
|
||||
printf '%s\n' \
|
||||
'fn bad(ticket: Ticket) { let _ = ticket.expose().as_bytes().to_vec(); }' \
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ collab_scan_roots=(
|
|||
crates/op-collab-relay-server
|
||||
crates/op-collab-relay-control-plane
|
||||
crates/op-collab-policy-file
|
||||
crates/op-collab-relay-locator-hsm
|
||||
crates/op-collab-relay-locator-server
|
||||
crates/op-collab-smoke
|
||||
crates/op-auth-bridge
|
||||
|
|
@ -42,6 +43,7 @@ collab_scan_roots=(
|
|||
deploy/collab-relay
|
||||
deploy/collab-relay-edge
|
||||
deploy/collab-relay-locator
|
||||
deploy/collab-relay-locator-hsm
|
||||
deploy/collab-relay-locator-edge
|
||||
)
|
||||
|
||||
|
|
@ -602,6 +604,7 @@ untyped_errors=$(grep -RInE \
|
|||
crates/op-collab-relay-server/src \
|
||||
crates/op-collab-relay-control-plane/src \
|
||||
crates/op-collab-policy-file/src \
|
||||
crates/op-collab-relay-locator-hsm/src \
|
||||
crates/op-collab-relay-locator-server/src \
|
||||
crates/op-auth-bridge/src 2>/dev/null || true)
|
||||
if [[ -n "$untyped_errors" ]]; then
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ new_fixture() {
|
|||
"$fixture_root/crates/op-collab-relay-server/src" \
|
||||
"$fixture_root/crates/op-collab-relay-control-plane/src" \
|
||||
"$fixture_root/crates/op-collab-policy-file/src" \
|
||||
"$fixture_root/crates/op-collab-relay-locator-hsm/src" \
|
||||
"$fixture_root/crates/op-collab-relay-locator-hsm/tests" \
|
||||
"$fixture_root/crates/op-collab-relay-locator-server/src" \
|
||||
"$fixture_root/crates/op-collab-smoke/src" \
|
||||
"$fixture_root/crates/op-auth-bridge/src" \
|
||||
|
|
@ -52,19 +54,21 @@ new_fixture() {
|
|||
"$fixture_root/crates/op-editor-host-core/src/collab" \
|
||||
"$fixture_root/crates/op-editor-ui/src" \
|
||||
"$fixture_root/crates/op-host-native/src" \
|
||||
"$fixture_root/crates/op-host-desktop/src" \
|
||||
"$fixture_root/crates/op-collab-host/src/runtime/network" \
|
||||
"$fixture_root/crates/op-host-desktop/src/collab_runtime" \
|
||||
"$fixture_root/crates/op-host-desktop/src/collab_runtime/network" \
|
||||
"$fixture_root/crates/op-host-services/src" \
|
||||
"$fixture_root/crates/op-i18n/src" \
|
||||
"$fixture_root/deploy/collab-relay" \
|
||||
"$fixture_root/deploy/collab-relay-edge" \
|
||||
"$fixture_root/deploy/collab-relay-locator" \
|
||||
"$fixture_root/deploy/collab-relay-locator-hsm" \
|
||||
"$fixture_root/deploy/collab-relay-locator-edge"
|
||||
|
||||
cp "$gate_source" "$fixture_root/tools/check-collab-security-boundaries.sh"
|
||||
cp "$script_dir/check-collab-security-boundaries-cases.sh" \
|
||||
"$fixture_root/tools/check-collab-security-boundaries-cases.sh"
|
||||
cp "$script_dir/check-collab-deployment-boundaries.sh" \
|
||||
"$fixture_root/tools/check-collab-deployment-boundaries.sh"
|
||||
cp "$script_dir/check-collab-security-boundaries-cases.sh" "$fixture_root/tools/"
|
||||
cp "$script_dir/check-op-auth-prebuilt.sh" "$fixture_root/tools/check-op-auth-prebuilt.sh"
|
||||
cp "$script_dir/check-op-auth-prebuilt.test.sh" "$fixture_root/tools/check-op-auth-prebuilt.test.sh"
|
||||
cp "$script_dir/package-op-auth-prebuilt.sh" "$fixture_root/tools/package-op-auth-prebuilt.sh"
|
||||
|
|
@ -94,8 +98,7 @@ EOF
|
|||
This file exists so the executable boundary gate can verify its public contract.
|
||||
EOF
|
||||
|
||||
cp "$script_dir/../.github/workflows/collab-security.yml" \
|
||||
"$fixture_root/.github/workflows/collab-security.yml"
|
||||
write_collab_security_workflow_fixture
|
||||
|
||||
cat > "$fixture_root/deploy/collab-relay-edge/global-nginx.conf" <<'EOF'
|
||||
stream {
|
||||
|
|
@ -232,24 +235,6 @@ location = /v1/locator {
|
|||
client_max_body_size 191;
|
||||
proxy_set_header Authorization $http_authorization;
|
||||
}
|
||||
location = /v1/pairing-code {
|
||||
if ($request_uri != "/v1/pairing-code") { return 404; }
|
||||
limit_except POST { deny all; }
|
||||
client_max_body_size 624; client_body_buffer_size 624;
|
||||
if ($http_content_type != "application/vnd.openpencil.relay-pairing-publish-v1") { return 415; }
|
||||
proxy_pass_request_headers off;
|
||||
proxy_pass http://locator:8092/v1/pairing-code;
|
||||
}
|
||||
location = /v1/pairing-code/claim {
|
||||
if ($request_uri != "/v1/pairing-code/claim") { return 404; }
|
||||
limit_except POST { deny all; }
|
||||
client_max_body_size 49; client_body_buffer_size 49;
|
||||
if ($content_length != "49") { return 400; }
|
||||
if ($http_content_type != "application/vnd.openpencil.relay-pairing-claim-v1") { return 415; }
|
||||
if ($http_accept != "application/vnd.openpencil.relay-sealed-invite-v1") { return 406; }
|
||||
proxy_pass_request_headers off;
|
||||
proxy_pass http://locator:8092/v1/pairing-code/claim;
|
||||
}
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
|
|
@ -282,6 +267,22 @@ EOF
|
|||
set -eu
|
||||
EOF
|
||||
|
||||
cat > "$fixture_root/deploy/collab-relay-locator-hsm/Dockerfile" <<'EOF'
|
||||
FROM rust:1.94-bookworm@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa AS build
|
||||
FROM build AS test
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends softhsm2
|
||||
RUN cargo test --locked -p op-collab-relay-locator-hsm --test softhsm -- --nocapture
|
||||
FROM debian:bookworm-slim@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
EOF
|
||||
for locator_hsm_file in \
|
||||
README.md \
|
||||
compose.yaml \
|
||||
config.example.json \
|
||||
openpencil-locator-hsm.conf \
|
||||
softhsm2.conf; do
|
||||
: > "$fixture_root/deploy/collab-relay-locator-hsm/$locator_hsm_file"
|
||||
done
|
||||
|
||||
cat > "$fixture_root/deploy/collab-relay-locator-edge/global-nginx.conf" <<'EOF'
|
||||
stream {
|
||||
access_log off;
|
||||
|
|
@ -331,33 +332,11 @@ server {
|
|||
if ($request_uri != "/v1/locator") {
|
||||
return 404;
|
||||
}
|
||||
limit_except POST {
|
||||
deny all;
|
||||
}
|
||||
proxy_pass_request_headers off;
|
||||
proxy_set_header Authorization $http_authorization;
|
||||
proxy_set_header Transfer-Encoding "";
|
||||
proxy_set_header Content-Encoding "";
|
||||
}
|
||||
location = /v1/pairing-code {
|
||||
if ($request_uri != "/v1/pairing-code") { return 404; }
|
||||
limit_except POST { deny all; }
|
||||
client_max_body_size 624; client_body_buffer_size 624;
|
||||
if ($http_content_type != "application/vnd.openpencil.relay-pairing-publish-v1") { return 415; }
|
||||
proxy_pass_request_headers off;
|
||||
proxy_pass http://openpencil_locator/v1/pairing-code;
|
||||
}
|
||||
location = /v1/pairing-code/claim {
|
||||
if ($request_uri != "/v1/pairing-code/claim") { return 404; }
|
||||
limit_except POST { deny all; }
|
||||
client_max_body_size 49; client_body_buffer_size 49;
|
||||
if ($content_length != "49") { return 400; }
|
||||
if ($http_content_type != "application/vnd.openpencil.relay-pairing-claim-v1") { return 415; }
|
||||
if ($http_accept != "application/vnd.openpencil.relay-sealed-invite-v1") { return 406; }
|
||||
proxy_pass_request_headers off;
|
||||
proxy_pass http://openpencil_locator/v1/pairing-code/claim;
|
||||
}
|
||||
location / { return 404; }
|
||||
}
|
||||
EOF
|
||||
for locator_edge_file in \
|
||||
|
|
@ -434,6 +413,7 @@ EOF
|
|||
op-collab-relay-server \
|
||||
op-collab-relay-control-plane \
|
||||
op-collab-policy-file \
|
||||
op-collab-relay-locator-hsm \
|
||||
op-collab-relay-locator-server; do
|
||||
cat > "$fixture_root/crates/$relay_crate/Cargo.toml" <<EOF
|
||||
[package]
|
||||
|
|
@ -443,6 +423,7 @@ license.workspace = true
|
|||
EOF
|
||||
: > "$fixture_root/crates/$relay_crate/LICENSE"
|
||||
done
|
||||
: > "$fixture_root/crates/op-collab-relay-locator-hsm/tests/softhsm.rs"
|
||||
: > "$fixture_root/crates/op-collab-smoke/LICENSE"
|
||||
|
||||
cat > "$fixture_root/crates/op-auth-bridge/Cargo.toml" <<'EOF'
|
||||
|
|
@ -562,29 +543,6 @@ EOF
|
|||
fn presence_payload_limit_applies_to_encode_and_decode() {}
|
||||
#[test]
|
||||
fn oversized_snapshot_kind_cannot_raise_the_owner_inbound_ceiling() {}
|
||||
EOF
|
||||
|
||||
cat > "$fixture_root/crates/op-collab-transport/src/admission.rs" <<'EOF'
|
||||
pub enum PeerIdentityPolicy {
|
||||
ThisAccount,
|
||||
AnyIssuedAccount,
|
||||
}
|
||||
#[cfg(test)]
|
||||
#[path = "admission_tests.rs"]
|
||||
mod admission_tests;
|
||||
EOF
|
||||
|
||||
cat > "$fixture_root/crates/op-collab-transport/src/admission_tests.rs" <<'EOF'
|
||||
#[test]
|
||||
fn any_issued_account_admits_a_foreign_subject_but_keeps_every_other_check() {}
|
||||
#[test]
|
||||
fn an_unpinned_join_without_confirmation_still_requires_this_account() {}
|
||||
#[test]
|
||||
fn an_unpinned_join_admits_a_foreign_account_only_behind_the_confirmation_gate() {}
|
||||
EOF
|
||||
|
||||
cat > "$fixture_root/crates/op-auth-bridge/src/collab_relay_token.rs" <<'EOF'
|
||||
pub struct VerifiedRelayTokenClaims;
|
||||
EOF
|
||||
|
||||
cat > "$fixture_root/crates/op-collab-transport/src/config.rs" <<'EOF'
|
||||
|
|
@ -641,6 +599,18 @@ impl QueueItem {
|
|||
pub(crate) struct BoundedTransferQueue;
|
||||
pub struct SharedQueueBudget;
|
||||
pub struct TokenBucket;
|
||||
EOF
|
||||
|
||||
cat > "$fixture_root/crates/op-collab-transport/src/admission.rs" <<'EOF'
|
||||
pub enum PeerIdentityPolicy {
|
||||
ThisAccount,
|
||||
AnyIssuedAccount,
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > "$fixture_root/crates/op-collab-transport/src/admission_tests.rs" <<'EOF'
|
||||
#[test]
|
||||
fn any_issued_account_admits_a_foreign_subject_but_keeps_every_other_check() {}
|
||||
EOF
|
||||
|
||||
cat > "$fixture_root/crates/op-auth-bridge/src/collab_jwks_cache.rs" <<'EOF'
|
||||
|
|
@ -665,6 +635,10 @@ EOF
|
|||
|
||||
cat > "$fixture_root/crates/op-auth-bridge/src/collab_ticket.rs" <<'EOF'
|
||||
pub const MAX_COLLAB_TICKET_BYTES: usize = 1024;
|
||||
EOF
|
||||
|
||||
cat > "$fixture_root/crates/op-auth-bridge/src/collab_relay_token.rs" <<'EOF'
|
||||
pub struct VerifiedRelayTokenClaims;
|
||||
EOF
|
||||
|
||||
cat > "$fixture_root/crates/op-auth-bridge/build.rs" <<'EOF'
|
||||
|
|
@ -757,20 +731,28 @@ EOF
|
|||
pub const MAX_AVATAR_SOURCE_PIXELS: u64 = 1_048_576;
|
||||
EOF
|
||||
|
||||
cat > "$fixture_root/crates/op-collab-host/src/runtime/types.rs" <<'EOF'
|
||||
cat > "$fixture_root/crates/op-host-desktop/src/collab_runtime/types.rs" <<'EOF'
|
||||
assert_not_impl_any!(OwnerNetworkCommand: Clone);
|
||||
assert_not_impl_any!(GuestNetworkCommand: Clone);
|
||||
assert_not_impl_any!(PeerNetworkCommand: Clone);
|
||||
fn verification_commands_move_the_original_ticket_allocation() {}
|
||||
EOF
|
||||
|
||||
cat > "$fixture_root/crates/op-collab-host/src/runtime/relay_bootstrap_tests.rs" <<'EOF'
|
||||
cat > "$fixture_root/crates/op-host-desktop/src/collab_runtime/relay_bootstrap_tests.rs" <<'EOF'
|
||||
#[test]
|
||||
fn payload_rejects_exact_cross_region_key_reuse() {}
|
||||
|
||||
#[cfg(test)]
|
||||
fn an_unpinned_join_without_confirmation_still_requires_this_account() {}
|
||||
|
||||
#[cfg(test)]
|
||||
fn an_unpinned_join_admits_a_foreign_account_only_behind_the_confirmation_gate() {}
|
||||
EOF
|
||||
|
||||
cat > "$fixture_root/crates/op-collab-host/src/runtime/network/owner.rs" <<'EOF'
|
||||
fn admission_policy() { let _ = PeerIdentityPolicy::AnyIssuedAccount; }
|
||||
cat > "$fixture_root/crates/op-host-desktop/src/collab_runtime/network/owner.rs" <<'EOF'
|
||||
fn owner_policy() {
|
||||
let _ = PeerIdentityPolicy::AnyIssuedAccount;
|
||||
}
|
||||
EOF
|
||||
|
||||
ln -s "$script_dir/check-collab-security-boundaries.test.sh" \
|
||||
|
|
|
|||
Loading…
Reference in a new issue