Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b685262
test(identity): expose credential delivery ownership without changing…
LIghtJUNction Sep 28, 2026
f015c60
refactor(identity): give one-time credential delivery a single domain…
LIghtJUNction Sep 28, 2026
26e43fc
test(identity): inspect persisted event JSON for one-time delivery ev…
LIghtJUNction Sep 28, 2026
b0b5120
Merge pull request #172 from TokenNotIncluded/fix/token-delivery-boun…
LIghtJUNction Sep 29, 2026
0942251
test(hosting): pin read-only runtime acceptance and cloud regression …
LIghtJUNction Sep 29, 2026
6d24ee8
ci(hosting): quote Docker health command for runner argument parsing
LIghtJUNction Sep 29, 2026
20399c0
test(hosting): require opt-in read-group publication without silent p…
LIghtJUNction Sep 29, 2026
246f0fd
refactor(hosting): compose an independently read-only runtime and dep…
LIghtJUNction Sep 29, 2026
df7efdb
fix(hosting): check sequence permissions only after resolving sequenc…
LIghtJUNction Sep 29, 2026
6703f3d
test(hosting): retain parallel isolation coverage and exercise a dist…
LIghtJUNction Sep 29, 2026
c314d1b
test(hosting): retain key revocation and restorable owner privilege r…
LIghtJUNction Sep 29, 2026
99a0242
fix(ci): quote the PostgreSQL health command for the Actions process …
LIghtJUNction Sep 29, 2026
2b97ee9
fix(hosting): verify complete reader schema and launch UID test after…
LIghtJUNction Sep 29, 2026
d17a287
fix(hosting): reject database object owners even after their ACLs are…
LIghtJUNction Sep 29, 2026
1138c27
docs: record verified red-to-green hosting ownership regression evidence
LIghtJUNction Sep 29, 2026
7537d6c
merge: verify hosting isolation with the existing token-delivery boun…
LIghtJUNction Sep 29, 2026
2b80a3c
merge: retain verified hosting ownership guards and parallel regressi…
LIghtJUNction Sep 29, 2026
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
68 changes: 68 additions & 0 deletions .github/workflows/hosting-authority.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: Hosting authority regression
on:
push:
branches: ['fix/hosting-authority-*']
pull_request:
paths:
- 'src/msg/hosting_runtime.py'
- 'src/msg/storage/read_only.py'
- 'tests/test_hosting_runtime_authority.py'
- '.github/workflows/hosting-authority.yml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: hosting-authority-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true
jobs:
authority:
runs-on: ubuntu-latest
timeout-minutes: 15
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: msgtest
POSTGRES_PASSWORD: msgtest
POSTGRES_DB: postgres
ports: ['5432:5432']
options: >-
--health-cmd "pg_isready -U msgtest -d postgres"
--health-interval 10s --health-timeout 5s --health-retries 5
env:
MSG_TEST_POSTGRES_URL_TEMPLATE: postgresql://msgtest:msgtest@127.0.0.1:5432/{database}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-python@v7
with:
python-version: '3.15'
allow-prereleases: true
cache: pip
- name: Bind evidence to source
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
run: |
mkdir -p artifacts
python - <<'PY'
import json, os, pathlib, platform, subprocess
git=lambda *args: subprocess.check_output(['git',*args],text=True).strip()
pathlib.Path('artifacts/source.json').write_text(json.dumps({
'head':os.environ['HEAD_SHA'], 'checkout':git('rev-parse','HEAD'),
'tree':git('rev-parse','HEAD^{tree}'), 'python':platform.python_version(),
'run_id':os.environ['GITHUB_RUN_ID'], 'attempt':os.environ['GITHUB_RUN_ATTEMPT']},indent=2)+'\n')
PY
- name: Install disposable test dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq age
python -m pip install -e '.[dev]'
- name: Exercise revoked keys and restorable ownership rights
run: python -m pytest tests/test_hosting_runtime_authority.py --junitxml=artifacts/authority.xml
- uses: actions/upload-artifact@v4
if: always()
with:
name: hosting-authority-evidence
path: artifacts/
if-no-files-found: error
75 changes: 75 additions & 0 deletions .github/workflows/hosting-readonly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Hosting read-only boundary
on:
push:
branches: ['fix/hosting-readonly-20260929']
pull_request:
paths:
- 'src/msg/**'
- 'tests/**'
- 'deploy/**'
- '.github/workflows/hosting-readonly.yml'
- 'pyproject.toml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: hosting-readonly-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true
jobs:
hosting:
runs-on: ubuntu-latest
timeout-minutes: 15
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: msgtest
POSTGRES_PASSWORD: msgtest
POSTGRES_DB: postgres
ports: ['5432:5432']
options: >-
--health-cmd "pg_isready -U msgtest"
--health-interval 10s --health-timeout 5s --health-retries 5
env:
MSG_TEST_POSTGRES_URL_TEMPLATE: postgresql://msgtest:msgtest@127.0.0.1:5432/{database}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-python@v7
with:
python-version: '3.15'
allow-prereleases: true
cache: pip
- name: Install isolated test dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq age
python -m pip install -e '.[dev]'
- name: Bind evidence to checkout and requested head
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
run: |
mkdir -p artifacts
python - <<'PY'
import json, os, platform, shutil, subprocess
from pathlib import Path
git=lambda *args: subprocess.check_output(['git',*args],text=True).strip()
Path('artifacts/source.json').write_text(json.dumps({
'head':os.environ['HEAD_SHA'], 'checkout':git('rev-parse','HEAD'),
'tree':git('rev-parse','HEAD^{tree}'), 'python':platform.python_version(),
'run_id':os.environ['GITHUB_RUN_ID'], 'attempt':os.environ['GITHUB_RUN_ATTEMPT']},indent=2))
for source in Path('tests').glob('test_hosting*.py'):
shutil.copyfile(source,Path('artifacts')/source.name)
PY
- name: Exercise isolated hosting and retain established web regressions
run: >-
python -m pytest tests/test_hosting*.py tests/test_cli_hosting.py
tests/test_extensions.py::test_hosting_publish_is_explicit_versioned_on_service_origin
--junitxml=artifacts/hosting.xml
- uses: actions/upload-artifact@v4
if: always()
with:
name: hosting-readonly-evidence
path: artifacts/
if-no-files-found: error
82 changes: 82 additions & 0 deletions .github/workflows/token-delivery-boundary.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Credential delivery boundary
on:
push:
branches: [fix/token-delivery-boundary-20260929]
pull_request:
paths:
- 'src/msg/**'
- 'tests/test_token_delivery_boundary.py'
- '.github/workflows/token-delivery-boundary.yml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: token-delivery-${{ github.event.pull_request.head.sha || github.sha }}
cancel-in-progress: false
jobs:
focused:
runs-on: ubuntu-latest
timeout-minutes: 12
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: msgtest
POSTGRES_PASSWORD: msgtest
POSTGRES_DB: postgres
ports: ['5432:5432']
options: >-
--health-cmd "pg_isready -U msgtest"
--health-interval 5s --health-timeout 5s --health-retries 10
valkey:
image: valkey/valkey:9.0.6
ports: ['6379:6379']
options: >-
--health-cmd "valkey-cli ping"
--health-interval 5s --health-timeout 5s --health-retries 10
env:
MSG_TEST_POSTGRES_URL_TEMPLATE: postgresql://msgtest:msgtest@127.0.0.1:5432/{database}
MSG_TEST_VALKEY_URL: redis://127.0.0.1:6379/0
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-python@v7
with:
python-version: '3.15'
allow-prereleases: true
cache: pip
- name: Install isolated test dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq age
python -m pip install -e '.[dev]'
- name: Record source identity
env:
EVENT_HEAD: ${{ github.event.pull_request.head.sha || github.sha }}
run: |
mkdir -p artifacts
python - <<'PY'
import json, os, platform, subprocess
from pathlib import Path
def git(ref):
return subprocess.check_output(['git', 'rev-parse', ref], text=True).strip()
Path('artifacts/source.json').write_text(json.dumps({
'head': os.environ['EVENT_HEAD'], 'checkout': git('HEAD'), 'tree': git('HEAD^{tree}'),
'run': os.environ['GITHUB_RUN_ID'], 'attempt': os.environ['GITHUB_RUN_ATTEMPT'],
'python': platform.python_version(), 'scope': 'isolated-regression-not-production'
}, indent=2) + '\n')
PY
- name: Exercise boundaries and unchanged security contracts
run: >-
python -m pytest tests/test_token_delivery_boundary.py tests/test_token_delivery.py
tests/test_credential_delivery_contract.py tests/test_credential_delivery_matrix.py
tests/test_credential_recovery_window.py tests/test_client_token_delivery.py
tests/test_batch_secret_release.py tests/test_security_patch_regressions.py
--junitxml=artifacts/token-delivery-boundary.xml
- uses: actions/upload-artifact@v4
if: always()
with:
name: token-delivery-boundary-evidence
path: artifacts/
if-no-files-found: error
16 changes: 16 additions & 0 deletions deploy/hosting-role.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- Review and run once as the installation database owner, in the chosen DB.
-- This file neither creates a database nor changes existing PUBLIC grants.
-- A password/peer map belongs in the operator's secret setup, never this file.
CREATE ROLE msgd_hosting LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE
NOINHERIT NOREPLICATION NOBYPASSRLS;
SELECT format('GRANT CONNECT ON DATABASE %I TO msgd_hosting', current_database()) \gexec
GRANT USAGE ON SCHEMA public TO msgd_hosting;
GRANT SELECT ON public.schema_version, public.resources, public.revisions,
public.identities, public.memberships, public.credentials, public.certificates,
public.settings, public.share_grants, public.share_grants_v2,
public.dm_conversations, public.system_sources TO msgd_hosting;
ALTER ROLE msgd_hosting SET default_transaction_read_only = on;
ALTER ROLE msgd_hosting SET search_path = pg_catalog, public, pg_temp;
-- No role membership, ownership, CREATE, sequence use, other-table SELECT,
-- column/table writes or callable application SECURITY DEFINER functions.
-- If PUBLIC already grants those, startup refuses; review that DB separately.
18 changes: 18 additions & 0 deletions deploy/hosting.example.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Install as /etc/msgd-hosting/msgd.toml, root:msgd-hosting 0640.
# This is NOT a copy of the writer configuration or its PostgreSQL credentials.
[server]
service_url = "https://msg.example.org"
public_web_origin = "https://msg.example.org"

[hosting]
# The same marker checked by the write service. Never select a private copy.
recovery_marker = "/etc/msgd/recovery-drill.json"

[storage]
postgres_dsn = "service=msgd-hosting"
content = "/var/lib/msgd/git/content"
blobs = "/var/lib/msgd/blobs/sha256"
# These parser-compatible paths are deliberately inaccessible and never opened.
service_keys = "/var/lib/msgd/service"
staging = "/var/lib/msgd/transfers/staging"
repositories = "/var/lib/msgd/git/repos"
21 changes: 14 additions & 7 deletions deploy/msgd-hosting.service
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
[Unit]
Description=msg.lmm.best separate-origin static hosting
Description=msg.lmm.best read-only hosted-content process
After=network.target

[Service]
Type=simple
User=msgd
Group=msgd
WorkingDirectory=/var/lib/msgd
ExecStart=/opt/msgd/venv/bin/msgd hosting --listen 127.0.0.1 --port 8043
User=msgd-hosting
Group=msgd-hosting
SupplementaryGroups=msgd-content-read
WorkingDirectory=/
ExecStart=/opt/msgd/venv/bin/msgd --config-dir /etc/msgd-hosting hosting --listen 127.0.0.1 --port 8043
Environment=PGSERVICEFILE=/etc/msgd-hosting/pg_service.conf
Environment=PGPASSFILE=/etc/msgd-hosting/pgpass
Restart=on-failure
UMask=0077
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/msgd
InaccessiblePaths=/var/lib/msgd-root /etc/msgd/root
ReadOnlyPaths=/etc/msgd-hosting /var/lib/msgd/git/content /var/lib/msgd/blobs/sha256
InaccessiblePaths=/var/lib/msgd-root -/etc/msgd/root /var/lib/msgd/service -/var/lib/msgd/service-keys /var/lib/msgd/git/repos /var/lib/msgd/transfers -/etc/msgd/msgd.toml -/etc/msgd/server.toml -/etc/msgd/mail.toml
RestrictSUIDSGID=true
CapabilityBoundingSet=
AmbientCapabilities=
LimitCORE=0

[Install]
WantedBy=multi-user.target
38 changes: 38 additions & 0 deletions deploy/prepare-hosting-content.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/bin/sh
# Explicit offline OS-administrator preparation, never called by hosting.load().
# Stop writers first. This changes only OS identities and content permissions.
set -eu
[ "$(id -u)" = 0 ] || { echo 'OS administrator required' >&2; exit 1; }
if systemctl is-active --quiet msgd.service || systemctl is-active --quiet msgd-worker.service; then
echo 'Stop all writers, including any non-systemd instances, before preparation.' >&2
exit 1
fi
getent passwd msgd >/dev/null || { echo 'Prepare the write service first.' >&2; exit 1; }
for directory in /var/lib/msgd /var/lib/msgd/git /var/lib/msgd/blobs /var/lib/msgd/git/content /var/lib/msgd/blobs/sha256; do
[ -d "$directory" ] && [ ! -L "$directory" ] || { echo "Missing or symlinked directory: $directory" >&2; exit 1; }
done
for directory in /var/lib/msgd/git/content /var/lib/msgd/blobs/sha256; do
[ -z "$(find -P "$directory" -type l -print -quit)" ] || { echo 'Content symlinks require operator review.' >&2; exit 1; }
done
getent group msgd-content-read >/dev/null || groupadd --system msgd-content-read
if ! getent passwd msgd-hosting >/dev/null; then
useradd --system --user-group --no-create-home --home-dir /nonexistent --shell /usr/sbin/nologin msgd-hosting
fi
usermod -a -G msgd-content-read msgd
usermod -a -G msgd-content-read msgd-hosting
for directory in /var/lib/msgd /var/lib/msgd/git /var/lib/msgd/blobs; do
chgrp msgd-content-read "$directory"
chmod 0710 "$directory"
done
for directory in /var/lib/msgd/git/content /var/lib/msgd/blobs/sha256; do
chgrp -R msgd-content-read "$directory"
find "$directory" -type d -exec chmod 2750 {} +
find "$directory" -type f -exec chmod u+rw,g+r,g-w,o-rwx {} +
done
git -c safe.directory=/var/lib/msgd/git/content/private.git --git-dir=/var/lib/msgd/git/content/private.git config core.sharedRepository 0640
install -d -o root -g msgd-hosting -m 0750 /etc/msgd-hosting
install -d -o root -g msgd-hosting -m 0750 /etc/msgd-hosting/trust
[ -e /etc/msgd/trust/root.json ] || { echo 'Public trust is missing.' >&2; exit 1; }
[ -e /etc/msgd-hosting/trust/root.json ] || ln -s /etc/msgd/trust/root.json /etc/msgd-hosting/trust/root.json
printf '%s\n' 'Now enable hosting.content_group_read=true in the WRITER config and review the separate reader config/DB role.'
printf '%s\n' 'No configuration secrets were copied, no service started, and no database permissions changed.'
Loading
Loading