Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ GULOGULO_VOLUME_PREFIX=gulogulo-local
# Set true only after the named volumes have been created by the deployment
# operator. External volumes survive container recreation and image upgrades.
GULOGULO_VOLUMES_EXTERNAL=false
GULOGULO_LP1_VOLUMES_EXTERNAL=false
GULOGULO_PROOF_NETWORK=gulogulo-local-proof
GULOGULO_PROOF_HTTP_PORT=18080
GULOGULO_PROOF_APP_HOST=::
LP1_DNS_PORT=5353
HOST=0.0.0.0
PORT=8080
APP_ENV=development
Expand Down
43 changes: 43 additions & 0 deletions .github/workflows/quality-gates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ jobs:
test -f src/observability/observability.test.mjs
test -f doc/lifecycle-backup-dr.md
test -f doc/local-proof-scope.md
test -f doc/local-proof-topology.md
test -f src/ops/acme/index.mjs
test -f src/ops/acme/index.test.mjs
test -f src/ops/abuse/index.mjs
Expand All @@ -154,7 +155,16 @@ jobs:
test -f src/release/local-proof-scope.mjs
test -f src/release/local-proof-scope.test.mjs
test -f release/local-proof-scope.json
test -f release/local-proof-topology.json
test -f scripts/lp0-scope-audit.mjs
test -f scripts/lp1-compose-audit.mjs
test -f scripts/lp1-proof-check.mjs
test -f scripts/lp1-proof-smoke.mjs
test -f docker/lp1-network/Dockerfile
test -f docker/lp1-network/entrypoint-ca.sh
test -f docker/lp1-network/entrypoint-dns.sh
test -f src/release/local-proof-topology.mjs
test -f src/release/local-proof-topology.test.mjs
test -f doc/upgrade-and-migration.md
grep -Fq 'profiles: ["local"]' compose.yaml
grep -Fq 'profiles: ["test"]' compose.yaml
Expand All @@ -175,6 +185,9 @@ jobs:
scripts/m1-fixture-smoke.ps1 \
scripts/container-patch.sh \
scripts/lp0-scope-audit.mjs \
scripts/lp1-compose-audit.mjs \
scripts/lp1-proof-check.mjs \
scripts/lp1-proof-smoke.mjs \
scripts/m10-release-audit.mjs \
.gitignore \
.github/workflows/commit-tests.yml \
Expand All @@ -184,6 +197,11 @@ jobs:
grep -Fq 'Author: Sythos (https://www.sythos.net)' "$file"
done

for file in docker/lp1-network/Dockerfile docker/lp1-network/entrypoint-ca.sh docker/lp1-network/entrypoint-dns.sh; do
grep -Fq 'SPDX-License-Identifier: MIT' "$file"
grep -Fq 'Author: Sythos (https://www.sythos.net)' "$file"
done

while IFS= read -r file; do
grep -Fq 'SPDX-License-Identifier: MIT' "$file"
grep -Fq 'Author: Sythos (https://www.sythos.net)' "$file"
Expand Down Expand Up @@ -287,6 +305,9 @@ jobs:
test -f package-lock.json
node -e "const e=require('./release/v1-release-evidence.template.json'); if (e.spdxLicenseIdentifier !== 'MIT' || e.author !== 'Sythos (https://www.sythos.net)' || e.product !== 'Gulo Gulo') process.exit(1)"
node -e "const s=require('./release/local-proof-scope.json'); if (s.spdxLicenseIdentifier !== 'MIT' || s.author !== 'Sythos (https://www.sythos.net)' || s.proofType !== 'local' || s.networkPolicy !== 'offline_runtime' || s.syntheticDataOnly !== true || s.publicDnsRequired !== false || s.publicAcmeEnabled !== false || s.externalPhaseDeferred !== true) process.exit(1)"
node -e "const t=require('./release/local-proof-topology.json'); if (t.spdxLicenseIdentifier !== 'MIT' || t.author !== 'Sythos (https://www.sythos.net)' || t.milestone !== 'LP1' || t.networkPolicy !== 'offline_runtime' || t.internalNetwork !== true || t.ipFamilies?.join(',') !== 'ipv4,ipv6' || t.publicDnsRequired !== false || t.publicAcmeEnabled !== false || t.dockerSocketMounted !== false) process.exit(1)"
bash -n docker/lp1-network/entrypoint-ca.sh
bash -n docker/lp1-network/entrypoint-dns.sh

- name: Set up Node.js when package metadata exists
if: hashFiles('package.json') != ''
Expand Down Expand Up @@ -352,6 +373,23 @@ jobs:
.
test -s "$output"

- name: Validate LP1 network utility image on amd64 and arm64
if: hashFiles('docker/lp1-network/Dockerfile') != ''
shell: bash
run: |
set -euo pipefail
output="$RUNNER_TEMP/gulogulo-lp1-network-ubuntu-26.04.oci.tar"
docker buildx build \
--platform linux/amd64,linux/arm64 \
--pull \
--provenance=false \
--sbom=false \
--tag gulogulo:ci-lp1-network-ubuntu-26.04 \
--file docker/lp1-network/Dockerfile \
--output "type=oci,dest=$output" \
docker/lp1-network
test -s "$output"

- name: Run Compose test profile
if: hashFiles('compose.yaml') != '' || hashFiles('compose.yml') != '' || hashFiles('docker-compose.yml') != '' || hashFiles('docker-compose.yaml') != ''
shell: bash
Expand All @@ -370,6 +408,11 @@ jobs:
"${compose[@]}" --profile test build --pull gulogulo-test
"${compose[@]}" --profile test run --rm --no-deps gulogulo-test

- name: Run LP1 isolated local proof topology
if: hashFiles('docker/lp1-network/Dockerfile') != '' && hashFiles('scripts/lp1-proof-smoke.mjs') != ''
shell: bash
run: npm run test:lp1:docker

- name: Run disposable PostgreSQL M2 integration contract
if: hashFiles('src/integrations/postgres.integration.test.mjs') != ''
shell: bash
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ COPY --chown=gulogulo:gulogulo . .

RUN set -eux; \
install -m 0755 scripts/container-patch.sh /usr/local/sbin/gulogulo-container-patch; \
install -d -o gulogulo -g gulogulo /var/lib/gulogulo/lp1; \
if [ ! -f package.json ]; then \
echo 'Gulo Gulo requires package.json with an npm start script.' >&2; \
exit 1; \
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ where the item depends on external infrastructure.

- [x] health and metrics;
- [x] multi-architecture Docker images for Ubuntu 26.04 LTS on amd64 (x86_64) and arm64;
- [x] dual-stack IPv4 and IPv6 network support;
- [x] log rotation;
- [x] alerts;
- [x] Postfix queue visibility;
Expand Down Expand Up @@ -115,6 +116,11 @@ gulogulo/
│ └── gulo-gulo-calendar-mail.png
├── config/
│ └── schema.v1.json
├── docker/
│ └── lp1-network/
│ ├── Dockerfile
│ ├── entrypoint-ca.sh
│ └── entrypoint-dns.sh
├── doc/
│ ├── README.md
│ ├── api-and-mcp.md
Expand All @@ -126,6 +132,7 @@ gulogulo/
│ ├── identity-and-postgres.md
│ ├── lifecycle-backup-dr.md
│ ├── local-proof-scope.md
│ ├── local-proof-topology.md
│ ├── mail-core.md
│ ├── rbac-admin-mfa.md
│ ├── release-readiness.md
Expand All @@ -137,11 +144,15 @@ gulogulo/
│ ├── m0-smoke.ps1
│ ├── m1-fixture-smoke.ps1
│ ├── lp0-scope-audit.mjs
│ ├── lp1-compose-audit.mjs
│ ├── lp1-proof-check.mjs
│ ├── lp1-proof-smoke.mjs
│ ├── m10-release-audit.mjs
│ ├── container-patch.sh
│ └── runtime, fixture, and patch utilities
├── release/
│ ├── local-proof-scope.json
│ ├── local-proof-topology.json
│ └── v1-release-evidence.template.json
├── src/
│ ├── admin/
Expand All @@ -157,6 +168,8 @@ gulogulo/
│ │ ├── index.mjs
│ │ ├── local-proof-scope.mjs
│ │ ├── local-proof-scope.test.mjs
│ │ ├── local-proof-topology.mjs
│ │ ├── local-proof-topology.test.mjs
│ │ ├── release-evidence.mjs
│ │ └── release-evidence.test.mjs
│ ├── ops/
Expand Down
178 changes: 178 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,157 @@ services:
healthcheck:
disable: true

# LP1 is a disposable, private proof topology. It deliberately uses a
# separate internal network, loopback-only publication, and named volumes
# that can be switched to externally managed storage by the operator.
gulogulo-proof:
<<: *gulogulo-service
profiles: ["proof"]
ports:
- "127.0.0.1:${GULOGULO_PROOF_HTTP_PORT:-18080}:8080"
- "[::1]:${GULOGULO_PROOF_HTTP_PORT:-18080}:8080"
environment:
<<: *gulogulo-environment
HOST: "${GULOGULO_PROOF_APP_HOST:-::}"
GULOGULO_HOST: "${GULOGULO_PROOF_APP_HOST:-::}"
APP_ENV: local-proof
GULOGULO_ENV: local-proof
GULOGULO_FIXTURE_MODE: "true"
GULOGULO_SERVICE_NAME: gulogulo-lp1-proof
networks:
proof-runtime:
aliases:
- gulogulo-proof
- gulogulo.test
- webmail.localhost
- calendar.localhost
- contacts.localhost
volumes:
- lp1-runtime-state:/var/lib/gulogulo
- lp1-mail-data:/var/lib/gulogulo/mail
- lp1-dav-data:/var/lib/gulogulo/dav
- lp1-backup-data:/var/lib/gulogulo/backups
healthcheck:
test:
- CMD
- node
- -e
- "fetch('http://[::1]:'+(process.env.PORT||8080)+'/health/ready').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"
labels:
com.sythos.gulogulo.milestone: LP1
com.sythos.gulogulo.proof: local
com.sythos.gulogulo.network-policy: offline_runtime

local-ca:
build:
context: docker/lp1-network
dockerfile: Dockerfile
profiles: ["proof"]
user: "0:10001"
command: ["/usr/local/sbin/gulogulo-lp1-ca"]
environment:
LP1_CA_DIR: /run/gulogulo-ca
volumes:
- lp1-ca-data:/run/gulogulo-ca
networks:
proof-runtime:
aliases:
- local-ca
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=16m
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- CHOWN
init: true
restart: "no"
healthcheck:
test:
- CMD-SHELL
- test -s /run/gulogulo-ca/ca.crt && test -s /run/gulogulo-ca/gulogulo.test.crt && test -s /run/gulogulo-ca/gulogulo.test.key
interval: 2s
timeout: 2s
start_period: 2s
retries: 15
labels:
com.sythos.gulogulo.milestone: LP1
com.sythos.gulogulo.proof: local
com.sythos.gulogulo.network-policy: offline_runtime

local-dns:
build:
context: docker/lp1-network
dockerfile: Dockerfile
profiles: ["proof"]
command: ["/usr/local/sbin/gulogulo-lp1-dns"]
environment:
LP1_DNS_PORT: ${LP1_DNS_PORT:-5353}
networks:
proof-runtime:
aliases:
- local-dns
read_only: true
tmpfs:
- /run:rw,noexec,nosuid,size=16m
- /tmp:rw,noexec,nosuid,size=16m
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
Comment on lines +277 to +278

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Allow dnsmasq to enter its configured user

When the proof profile runs, local-dns starts as root with every capability removed, while its entrypoint invokes dnsmasq with --user=nobody --group=nogroup. The dnsmasq --user documentation states that dnsmasq changes its user ID after startup, but that transition requires CAP_SETUID and CAP_SETGID; without them it exits before becoming healthy, so the new live LP1 workflow cannot pass. Run the container directly as the unprivileged user, which is possible on port 5353, or retain the narrowly required capabilities.

Useful? React with 👍 / 👎.

init: true
restart: "no"
healthcheck:
test:
- CMD-SHELL
- dnsmasq --test --no-resolv --no-hosts --address=/gulogulo.test/127.0.0.1
interval: 2s
timeout: 2s
start_period: 2s
retries: 15
labels:
com.sythos.gulogulo.milestone: LP1
com.sythos.gulogulo.proof: local
com.sythos.gulogulo.network-policy: offline_runtime

# This is an on-demand client. It is not started by the proof runtime; the
# LP1 smoke harness runs it twice, before and after an application restart.
gulogulo-proof-check:
build:
context: .
dockerfile: Dockerfile
args:
INSTALL_DEV: "true"
profiles: ["proof-check"]
command: ["node", "scripts/lp1-proof-check.mjs"]
environment:
LP1_CA_DIR: /run/gulogulo-ca
LP1_PROOF_STATE_DIR: /var/lib/gulogulo/lp1
LP1_DNS_SERVER: local-dns
LP1_DNS_PORT: ${LP1_DNS_PORT:-5353}
LP1_APPLICATION_SERVER: gulogulo-proof
NODE_EXTRA_CA_CERTS: /run/gulogulo-ca/ca.crt
volumes:
- lp1-ca-data:/run/gulogulo-ca:ro
- lp1-proof-state:/var/lib/gulogulo/lp1
networks:
- proof-runtime
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=16m
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
init: true
restart: "no"
labels:
com.sythos.gulogulo.milestone: LP1
com.sythos.gulogulo.proof: local
com.sythos.gulogulo.network-policy: offline_runtime

volumes:
runtime-state:
name: ${GULOGULO_VOLUME_PREFIX:-gulogulo}-runtime-state
Expand All @@ -189,3 +340,30 @@ volumes:
backup-data:
name: ${GULOGULO_VOLUME_PREFIX:-gulogulo}-backup-data
external: ${GULOGULO_VOLUMES_EXTERNAL:-false}
lp1-ca-data:
name: ${GULOGULO_VOLUME_PREFIX:-gulogulo}-lp1-ca-data
external: ${GULOGULO_LP1_VOLUMES_EXTERNAL:-false}
lp1-runtime-state:
name: ${GULOGULO_VOLUME_PREFIX:-gulogulo}-lp1-runtime-state
external: ${GULOGULO_LP1_VOLUMES_EXTERNAL:-false}
lp1-mail-data:
name: ${GULOGULO_VOLUME_PREFIX:-gulogulo}-lp1-mail-data
external: ${GULOGULO_LP1_VOLUMES_EXTERNAL:-false}
lp1-dav-data:
name: ${GULOGULO_VOLUME_PREFIX:-gulogulo}-lp1-dav-data
external: ${GULOGULO_LP1_VOLUMES_EXTERNAL:-false}
lp1-backup-data:
name: ${GULOGULO_VOLUME_PREFIX:-gulogulo}-lp1-backup-data
external: ${GULOGULO_LP1_VOLUMES_EXTERNAL:-false}
lp1-proof-state:
name: ${GULOGULO_VOLUME_PREFIX:-gulogulo}-lp1-proof-state
external: ${GULOGULO_LP1_VOLUMES_EXTERNAL:-false}

networks:
proof-runtime:
name: ${GULOGULO_PROOF_NETWORK:-gulogulo-local-proof}
internal: true
enable_ipv6: true
ipam:
config:
- subnet: fd42:4755:756c:7031::/64
4 changes: 4 additions & 0 deletions doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ normal workflow.
- [LP0 local proof scope](local-proof-scope.md) — the reserved local names,
synthetic-data rule, offline runtime boundary, disposable service inventory,
architecture targets, and the explicit deferral of the external phase.
- [LP1 isolated local topology](local-proof-topology.md) — the private dual-stack
Compose network, disposable CA/DNS utilities, IPv4/IPv6 loopback-only
application bindings, external-capable named volumes, restart continuity
check, and Docker proof harness.

The project is still intentionally small. The documents describe real behavior
only: DAV, administration, lifecycle, backup, observability, ACME, abuse, and
Expand Down
4 changes: 2 additions & 2 deletions doc/local-proof-scope.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ validator and its tests live in `src/release/local-proof-scope.mjs` and
replaced with a real public domain during LP1–LP9.
- TLS is terminated with a locally trusted CA generated for the proof. Public
Let's Encrypt and generic public ACME are explicitly disabled here.
- Host-file or local DNS entries may point these names at loopback or the
private Compose/Kubernetes test network. No public DNS record is created.
- Host-file or local DNS entries may point these names at IPv4/IPv6 loopback or
the private Compose/Kubernetes test network. No public DNS record is created.

### Data and identities

Expand Down
Loading