deploy: the running configuration, and the identity contract
STAGING-STATE recorded mechcomp.service as delivered at deploy/mechcomp.service on 11 SEP. The unit was real; the directory was not. It existed only on CT 100, so the repository claimed to hold something it did not, and the only way to answer a question about the service was to read the container. The nginx vhost had never been committed at all. Both imported verbatim as they ran on 12 SEP, with their host checksums proven equal at import. A first commit of a configuration file records reality, not intentions -- every later improvement is then a diff against a known-good starting point rather than a rewrite nobody can check. mechcomp.env is deliberately absent. It is the one file that is supposed to differ between instances, it is root:mechcomp 0640 because a deployment's bindings belong to the deployment, and a committed copy would create a second source of truth for exactly the wrong file. ENVIRONMENT.md documents the keys. Certificates and keys likewise. IDENTITY-CONTRACT.md is the boundary between this application and the membership system, and the only document here written to be read by someone who does not work on this repository. Two systems that must agree on an interface need it written once, somewhere both can point at. The compiler does not authenticate anyone; it is told. CT 101 decides, against the membership system, and sets X-Kane-Auth-Email and X-Kane-Auth-Method. They map onto Author.email and Author.method, which already exist and are tested. The parser's refusal to recover `verified` from text was built for this: a record read back is always self-declared, because a file cannot attest to its own verification. The decision belongs at CT 101 and never at wg-pk. The hub carries twenty peers and is estate infrastructure this project does not own, so authorisation there would make every future adjustment an escalation and would teach a shared transport about one project's membership roll. CT 101 already shares the service bridge with CT 100 and CT 102. One gated prefix, /m/. nginx gets one location block, written once. Gating a new endpoint afterwards is choosing a URL in Python -- no proxy change, no escalation. That cheapness makes the placement of the line reversible rather than structural. Today it sits at export: the catalogue and composer are open, and what requires membership is producing an artifact whose design record names an author. Section 6 is the part most likely to erode and is written hardest. The compiler must never learn what a building is, what a membership level is, or that group names exist -- including as a configuration value, which is how the leak arrives by the side door. The test is that the membership system can rename every level and replace its storage without a line of this repository being read. Section 9 deletes the ACL from the roadmap rather than deferring it. The compiler will not have a user table, a login form or a session. Recorded openly rather than assumed: the service is world-reachable and unauthenticated, /m/ is not yet gated, and STL export will therefore ship open. Same posture the whole service already has. The earlier justification -- "acceptable for a development name" -- was wrong and is corrected separately: dev.mechcomp.kane-il.us is production, dev abbreviates Mechanical Compiler Developers, and the name is on printed material. The inbound header strip depends on none of this and should land on its own. It costs one directive and removes a forgery that becomes possible the moment the headers mean anything.
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
# deploy/
|
||||
|
||||
The configuration that makes this application a running service, under version
|
||||
control.
|
||||
|
||||
## Why this directory exists
|
||||
|
||||
`STAGING-STATE.md` recorded `mechcomp.service` as delivered at
|
||||
`deploy/mechcomp.service` on 2026-09-11. The unit was real; the directory was
|
||||
not. The file existed only on CT 100, so the repository claimed to hold
|
||||
something it did not, and the only way to answer a question about the service
|
||||
was to read the container.
|
||||
|
||||
That is the failure mode this directory closes. The repository is the single
|
||||
source of truth. Where these files and the hosts disagree, the disagreement is
|
||||
a finding — and the direction of travel is from here outward, not from the host
|
||||
back.
|
||||
|
||||
## What is here
|
||||
|
||||
| File | Lives on | Installed at |
|
||||
|---|---|---|
|
||||
| `mechcomp.service` | CT 100 | `/etc/systemd/system/mechcomp.service` |
|
||||
| `nginx/mechanical-compiler.conf` | CT 101 | `/etc/nginx/sites-available/mechanical-compiler`, symlinked from `sites-enabled/` |
|
||||
|
||||
Both are imported **verbatim** as they ran on 2026-09-12. The first commit of a
|
||||
configuration file records reality, not intentions; any improvement is a later
|
||||
diff that can be read against a known-good starting point.
|
||||
|
||||
## What is deliberately not here
|
||||
|
||||
**`/etc/mechcomp/mechcomp.env`.** It is the deployment's own answers —
|
||||
`MECHCOMP_BIND`, `MECHCOMP_PORT`, `MECHCOMP_BASE_URL` — and it is
|
||||
`root:mechcomp 0640` because a deployment's bindings belong to the deployment.
|
||||
A committed copy would be a second source of truth for the one file that is
|
||||
supposed to differ between instances. `ENVIRONMENT.md` documents the keys; the
|
||||
values stay on the host.
|
||||
|
||||
**Certificates and keys.** `/etc/ssl/mechcomp/` on CT 101 holds a leaf signed by
|
||||
the local staging CA. Keys are never committed.
|
||||
|
||||
## Two facts about the nginx vhost worth knowing before editing it
|
||||
|
||||
**CT 101 does not know the public name.** Its `server_name` is
|
||||
`mechanical-compiler.dev.infra`, a hosts-file name resolvable on `srv-b`, CT 100
|
||||
and CT 101 only, and its certificate is signed by the local staging CA. Public
|
||||
TLS terminates at `wg-pk`, which proxies inward over the WireGuard tunnel to a
|
||||
DNAT on `srv-b`. `dev.mechcomp.kane-il.us` appears nowhere in this file and does
|
||||
not need to.
|
||||
|
||||
**Both server blocks are `default_server`.** CT 101 answers whatever `Host`
|
||||
arrives, which is why the public name works without being named. That is
|
||||
acceptable because `10.20.0.0/24` is a portless service bridge with LAN traffic
|
||||
dropped, so the only thing that can reach port 443 is the tunnel — but it means
|
||||
`server_name` is documentation here rather than a filter, and adding a second
|
||||
site to CT 101 will require that to change.
|
||||
|
||||
## Applying a change
|
||||
|
||||
Infrastructure mode (`PROCESS.md` §2). One command group, read-only first, a
|
||||
rollback copy of the file before editing, `nginx -t` before `reload`, and the
|
||||
change proven from outside the container rather than from within it.
|
||||
|
||||
A change to either file lands in the repository first and is copied outward.
|
||||
Editing on the host and back-porting reintroduces exactly the drift this
|
||||
directory was created to end.
|
||||
@@ -0,0 +1,56 @@
|
||||
[Unit]
|
||||
# The real service. Replaces mechcomp-placeholder.service, which existed only to
|
||||
# prove the nginx chain on CT 101 independently of the application
|
||||
# (STAGING-STATE.md, "Placeholder backend"). That proof is done and the
|
||||
# application now exists, so the placeholder is removed rather than left
|
||||
# competing for 10.20.0.10:8770.
|
||||
Description=Mechanical Compiler composer
|
||||
Documentation=https://gitea.barternetwork.us/TheRON/mechanical-compiler
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=mechcomp
|
||||
Group=mechcomp
|
||||
|
||||
# Binding, base URL and everything else come from here. The unit deliberately
|
||||
# passes no --host or --port: the deployment declares those, not the command
|
||||
# line, and a flag here would silently outrank the file the operator edits.
|
||||
EnvironmentFile=/etc/mechcomp/mechcomp.env
|
||||
|
||||
WorkingDirectory=/var/www/mechcomp
|
||||
ExecStart=/var/www/mechcomp/venv/bin/python -m mechcomp.web
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
|
||||
# Hardening, matching the set the placeholder already carried.
|
||||
NoNewPrivileges=yes
|
||||
PrivateTmp=yes
|
||||
PrivateDevices=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectControlGroups=yes
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||||
RestrictNamespaces=yes
|
||||
LockPersonality=yes
|
||||
MemoryDenyWriteExecute=yes
|
||||
RestrictRealtime=yes
|
||||
RestrictSUIDSGID=yes
|
||||
RemoveIPC=yes
|
||||
|
||||
# ProtectSystem=strict makes everything read-only except what is named here.
|
||||
# The composer writes nothing today; the data directory is listed because the
|
||||
# design record and any future artifact belong there and not in the working
|
||||
# tree, which is also the service user's home (F-029).
|
||||
ReadWritePaths=/var/lib/mechcomp
|
||||
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=mechcomp
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,31 @@
|
||||
server {
|
||||
listen 443 ssl default_server;
|
||||
listen [::]:443 ssl default_server;
|
||||
|
||||
server_name mechanical-compiler.dev.infra;
|
||||
|
||||
ssl_certificate /etc/ssl/mechcomp/server.crt;
|
||||
ssl_certificate_key /etc/ssl/mechcomp/server.key;
|
||||
|
||||
location / {
|
||||
proxy_pass http://10.20.0.10:8770;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_read_timeout 300s;
|
||||
client_max_body_size 8m;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
server_name mechanical-compiler.dev.infra;
|
||||
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
# IDENTITY-CONTRACT.md
|
||||
|
||||
How the Mechanical Compiler learns who a visitor is, and the boundary that keeps
|
||||
it from learning anything else.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Created | 2026-09-12 |
|
||||
| Status | **Specified, not implemented.** See §8. |
|
||||
| Audience | This project, and whoever builds the membership system |
|
||||
| Companions | `PROCESS.md`, `STAGING-STATE.md`, `deploy/README.md` |
|
||||
|
||||
---
|
||||
|
||||
## 0. Why this document exists separately
|
||||
|
||||
It is the only document in this repository written to be read by someone who
|
||||
does not work on this repository.
|
||||
|
||||
The membership system is a different application, in a different container, with
|
||||
a different release schedule and a different author. Two systems that must agree
|
||||
on an interface need that interface written down once, somewhere both of them
|
||||
can point at. Burying it in `ENVIRONMENT.md` — which describes how *this* host
|
||||
is built — guarantees the other side never finds it.
|
||||
|
||||
The compiler is expected to catalogue a very large number of applications over a
|
||||
very long time. An interface that is small enough to state on one page is the
|
||||
only kind that survives that.
|
||||
|
||||
---
|
||||
|
||||
## 1. The shape: relying party, not integration
|
||||
|
||||
**The compiler does not authenticate anyone. It is told.**
|
||||
|
||||
Authorisation is decided by the reverse proxy on CT 101, against the membership
|
||||
system. By the time a request reaches the application, the decision is already
|
||||
made. The application reads an identity and acts; it never asks a question about
|
||||
eligibility, because it has nothing to ask the question with.
|
||||
|
||||
This is the same rule the project applies to physical claims. `PRECISION.md`
|
||||
puts codes and permitting out of scope — the compiler records what is true and
|
||||
never issues a verdict. "Who may do this" is a verdict. It belongs to whoever
|
||||
holds the membership roll, and not here.
|
||||
|
||||
The practical consequence is that the two systems share a hostname's parent
|
||||
domain and nothing else. Either can be rewritten entirely without the other
|
||||
being read.
|
||||
|
||||
---
|
||||
|
||||
## 2. The chain as it actually is
|
||||
|
||||
```
|
||||
browser
|
||||
-> wg-pk public TLS for dev.mechcomp.kane-il.us
|
||||
-> WireGuard tunnel
|
||||
-> DNAT on srv-b scoped to the hub as source
|
||||
-> CT 101 nginx local staging-CA TLS; DECIDES; sets the headers
|
||||
-> CT 100 :8770 the application
|
||||
```
|
||||
|
||||
Two things about this that are easy to get wrong:
|
||||
|
||||
**CT 101 does not know the public name.** Its `server_name` is
|
||||
`mechanical-compiler.dev.infra` and its certificate is local. See
|
||||
`deploy/README.md`.
|
||||
|
||||
**The decision belongs at CT 101, never at `wg-pk`.** The hub carries twenty
|
||||
peers, is estate infrastructure this project does not own, and every change
|
||||
there is an escalation under `PROCESS.md` §7. CT 101 already shares `vmbr1` with
|
||||
CT 100 and the membership container, so the eligibility check is a local call
|
||||
that never leaves the service bridge. Putting authorisation at the hub would
|
||||
make every future adjustment to who-may-do-what an estate change, and would
|
||||
teach a shared transport about one project's membership roll.
|
||||
|
||||
---
|
||||
|
||||
## 3. What crosses the boundary
|
||||
|
||||
Two request headers, set by CT 101, and nothing else.
|
||||
|
||||
| Header | Meaning |
|
||||
|---|---|
|
||||
| `X-Kane-Auth-Email` | The identity. An email address, which is the identifier. |
|
||||
| `X-Kane-Auth-Method` | How it was established, e.g. `kane-fabric/oidc`. Free text, recorded verbatim. |
|
||||
|
||||
They map onto `design_record.Author.email` and `Author.method`, which exist and
|
||||
are tested today. When they arrive, `verified` becomes true and the rendered
|
||||
design record line changes from
|
||||
|
||||
```
|
||||
author someone@example.org (self-declared, unverified)
|
||||
```
|
||||
|
||||
to
|
||||
|
||||
```
|
||||
author someone@example.org (verified: kane-fabric/oidc)
|
||||
```
|
||||
|
||||
Nothing in `design_record.py` changes shape. The parser already refuses to
|
||||
recover `verified` from text — a record read back is always self-declared,
|
||||
because a file cannot attest to its own verification. That asymmetry was built
|
||||
for this moment.
|
||||
|
||||
**A handle is not the identity.** A person may later choose a display name. It
|
||||
does not replace the address and it does not travel in this contract.
|
||||
|
||||
---
|
||||
|
||||
## 4. Three invariants, all of them CT 101's job
|
||||
|
||||
**I-1. Inbound `X-Kane-Auth-*` is stripped at every location, before anything
|
||||
else.** nginx forwards headers it was given. Without the strip, any client can
|
||||
send `X-Kane-Auth-Email` and be whoever they like. This is the invariant the
|
||||
other two exist to support, and it is worth landing before there is anything to
|
||||
forge against.
|
||||
|
||||
**I-2. CT 100 is reachable only from CT 101.** The application binds
|
||||
`10.20.0.10:8770` on a portless service bridge with LAN traffic dropped.
|
||||
`web/app.py` defaults to loopback rather than `0.0.0.0` for the same reason:
|
||||
behind a proxy, binding too narrowly fails loudly as a 502, and binding too
|
||||
widely fails silently as an open service nobody notices.
|
||||
|
||||
**I-3. An absent header means unauthenticated. It is never an error and never a
|
||||
default identity.** The composer must run from a bare checkout on a machine with
|
||||
no proxy in front of it, and an anonymous visitor is an ordinary, expected
|
||||
caller. A missing header that produced a 500, or that silently became
|
||||
`admin@somewhere`, would be worse than no authentication at all.
|
||||
|
||||
---
|
||||
|
||||
## 5. One gated prefix
|
||||
|
||||
Everything requiring membership lives under `/m/`. Everything else is public.
|
||||
|
||||
```
|
||||
/ the composer page public
|
||||
/api/build build and view a section public
|
||||
/m/stl export members
|
||||
/m/... anything gated later members
|
||||
```
|
||||
|
||||
nginx gets **one** `location /m/` block, written once and not edited again.
|
||||
Gating a new endpoint afterwards is choosing a URL in Python — no proxy change,
|
||||
no shared-infrastructure change, no escalation. Ungating one is the same move in
|
||||
reverse.
|
||||
|
||||
That cheapness is the point. It means the placement of the line is a reversible
|
||||
decision rather than a structural one.
|
||||
|
||||
**Where the line is today: export is gated; looking is not.** The catalogue and
|
||||
the composer are open to anyone. What requires membership is producing an
|
||||
artifact that carries a design record, because the record names an author and an
|
||||
author only means something once somebody established who they are. The site is
|
||||
the shopfront; the wall goes where the claim is made.
|
||||
|
||||
---
|
||||
|
||||
## 6. What the compiler must never be told
|
||||
|
||||
This list is the boundary. It is short on purpose, and it is the part most
|
||||
likely to erode.
|
||||
|
||||
- **What a building is.** Membership attaches to buildings. The compiler has no
|
||||
representation of one and must not acquire it.
|
||||
- **What a membership level is.** `CURRENT RESIDENT`, `HOA MEMBER`, `3D PRINTER`
|
||||
— the compiler does not know these strings exist. By the time a request
|
||||
arrives, the level has already been resolved into "this request may proceed".
|
||||
- **The membership roll.** No lookup, no directory, no list of who exists.
|
||||
- **Group names in any form**, including as a configuration value. A
|
||||
`REQUIRED_GROUP` setting in the application would be the leak arriving by the
|
||||
side door.
|
||||
|
||||
If the application ever needs one of these to answer a request, the design has
|
||||
failed and the fix is at the proxy, not here.
|
||||
|
||||
The test: the membership system must be able to restructure its entire model —
|
||||
rename every level, change what a building is, replace its storage — without a
|
||||
line of this repository being read.
|
||||
|
||||
---
|
||||
|
||||
## 7. What the membership system must provide
|
||||
|
||||
Minimum, and deliberately not more. How it is implemented is that project's
|
||||
business.
|
||||
|
||||
- **An eligibility endpoint** reachable from CT 101 on the service bridge, which
|
||||
answers a request with `2xx` when the caller may proceed and `401` when not.
|
||||
Whatever session or token the caller carries is between that endpoint and the
|
||||
browser; CT 101 forwards and does not interpret.
|
||||
- **Two response headers on a `2xx`**, carrying the address and the method, for
|
||||
CT 101 to promote onto the proxied request.
|
||||
|
||||
That is the whole of it. The endpoint's path, its session mechanism, its login
|
||||
page and its storage are not this contract's concern and should not be
|
||||
constrained by it.
|
||||
|
||||
---
|
||||
|
||||
## 8. Current state
|
||||
|
||||
**Not implemented.** Specified here so that it is designed once, before the
|
||||
endpoints that will depend on it exist.
|
||||
|
||||
What is true today: the service is world-reachable and unauthenticated, and
|
||||
`/m/` is not yet gated. The first endpoint under it, STL export, will therefore
|
||||
ship open. That is a deliberate, recorded, temporary state and not an oversight
|
||||
— it is the same posture the whole service already has, and it changes when the
|
||||
membership system offers §7's endpoint.
|
||||
|
||||
An earlier version of `WORK-ORDER-004` and of `HANDOFF.md` §4 called the open
|
||||
service "acceptable for a development name". That reasoning was wrong:
|
||||
`dev.mechcomp.kane-il.us` is production, `dev` abbreviates *Mechanical Compiler
|
||||
Developers*, and the name is on printed material. The question is carried openly
|
||||
instead.
|
||||
|
||||
**I-1, the inbound strip, does not depend on any of this** and should land on
|
||||
its own. It costs one directive and removes a forgery that would otherwise
|
||||
become possible the moment §3 is implemented.
|
||||
|
||||
---
|
||||
|
||||
## 9. What this removes
|
||||
|
||||
`HANDOFF.md` §4 listed an access-control layer inside the compiler as the last
|
||||
priority. **It is deleted, not deferred.** The compiler will not have an ACL, a
|
||||
user table, a login form, or a session. Authorisation lives at the proxy against
|
||||
the membership system, and an item leaves the roadmap rather than moving down
|
||||
it.
|
||||
|
||||
Persistence — the priority above it — changes shape rather than disappearing. A
|
||||
saved design belongs to a verified person, and the verified person now comes
|
||||
from outside, so it should be built knowing the identity is external.
|
||||
Reference in New Issue
Block a user