mirror of
https://scm.tikali.ai/tikali/applications/monky/monky-deployd.git
synced 2026-09-18 07:36:16 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e8b2bf41d | |||
| d34189c625 |
@@ -3,6 +3,10 @@
|
|||||||
|
|
||||||
## 0.1.5 — 2026-09-07
|
## 0.1.5 — 2026-09-07
|
||||||
|
|
||||||
|
- **`tenancy.port` defaults to 443 everywhere** — the ansible role default and the `TenancyCfg` default now
|
||||||
|
match `install.sh`/`config.example.yaml` (0.1.5 only changed those two); PROTOCOL.md states the intercept
|
||||||
|
port separately from the in-pod 8081, that the broker does not add `#monky-deploy-agent` yet, that the jwt
|
||||||
|
mount keeps no `jti` state, and that `release:gitea` has not run (doc-drift DD-0523/0525/0527/0528).
|
||||||
- **`${VAR}` inside comment lines is not a reference.** The renderer's `.env.template` header literally says
|
- **`${VAR}` inside comment lines is not a reference.** The renderer's `.env.template` header literally says
|
||||||
"substitutes every ${VAR}", which the refusal check counted as an unresolved variable
|
"substitutes every ${VAR}", which the refusal check counted as an unresolved variable
|
||||||
(`ENV_INCOMPLETE: unresolved: VAR`) — the first bundle on env-qa-02 was refused for it.
|
(`ENV_INCOMPLETE: unresolved: VAR`) — the first bundle on env-qa-02 was refused for it.
|
||||||
|
|||||||
@@ -98,11 +98,13 @@ monky-deployd version
|
|||||||
## Security model
|
## Security model
|
||||||
|
|
||||||
- **Identity = the box's ziti host identity.** Only identities with `#monky-deploy-agent` can dial
|
- **Identity = the box's ziti host identity.** Only identities with `#monky-deploy-agent` can dial
|
||||||
tenancy's agent entrypoint; `#openbao-client` reaches OpenBao. The agent reads the identity
|
tenancy's agent entrypoint; `#openbao-client` reaches OpenBao — the broker does not add either
|
||||||
|
attr yet (ADR-0028 addendum; see PROTOCOL.md §Where and how), so today an operator adds them on
|
||||||
|
the controller after enrolment. The agent reads the identity
|
||||||
through an ACL (`setfacl -m u:monky-deployd:r`), never owns it.
|
through an ACL (`setfacl -m u:monky-deployd:r`), never owns it.
|
||||||
- **The bearer to tenancy is the agent's own OpenBao token**, minted by OpenBao from a
|
- **The bearer to tenancy is the agent's own OpenBao token**, minted by OpenBao from a
|
||||||
tenancy-signed ES256 deploy grant (`aud openbao-see-env`, `kind deploy-grant`, 1 h, single-use
|
tenancy-signed ES256 deploy grant (`aud openbao-see-env`, `kind deploy-grant`, 1 h; its `jti`
|
||||||
`jti`). Tenancy verifies it with `auth/token/lookup`, pins `meta.env_id`, and refuses a token
|
becomes the token's `meta.grant_jti` — the mount keeps no replay state, tenancy does). Tenancy verifies it with `auth/token/lookup`, pins `meta.env_id`, and refuses a token
|
||||||
whose `meta.grant_jti` was superseded (kit re-reveal, retire) → `401 AGENT_UNAUTHENTICATED`.
|
whose `meta.grant_jti` was superseded (kit re-reveal, retire) → `401 AGENT_UNAUTHENTICATED`.
|
||||||
**No AppRole, nothing to unwrap** (Gate 1 result, 2026-09-05).
|
**No AppRole, nothing to unwrap** (Gate 1 result, 2026-09-05).
|
||||||
- **The agent never receives a secret from tenancy.** Bundles carry placeholders; the agent reads
|
- **The agent never receives a secret from tenancy.** Bundles carry placeholders; the agent reads
|
||||||
@@ -160,7 +162,9 @@ blocking on `main`/tags (manual on MRs); the `.deb` always ships the SDK wheel.
|
|||||||
`release` uploads to the GitLab generic package registry + release (**the primary download**; the
|
`release` uploads to the GitLab generic package registry + release (**the primary download**; the
|
||||||
project is private, so the installer sends the read-only deploy token), and `release:gitea` publishes the same assets on the Gitea mirror (the `--source gitea`
|
project is private, so the installer sends the read-only deploy token), and `release:gitea` publishes the same assets on the Gitea mirror (the `--source gitea`
|
||||||
alternative; automatic when `GITEA_TOKEN` is set, manual otherwise — see `docs/OPERATIONS.md` for
|
alternative; automatic when `GITEA_TOKEN` is set, manual otherwise — see `docs/OPERATIONS.md` for
|
||||||
the by-hand recipe). Both locations keep being published.
|
the by-hand recipe). The GitLab registry is published by every tag pipeline; the Gitea mirror only
|
||||||
|
when `release:gitea` runs — automatically once `GITEA_TOKEN` is set in CI, by hand otherwise — so
|
||||||
|
check the Gitea release page before pointing an installer at it.
|
||||||
|
|
||||||
## See also
|
## See also
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ monky_deployd_disk_factor: 1.5
|
|||||||
monky_deployd_disk_headroom_bytes: 2147483648
|
monky_deployd_disk_headroom_bytes: 2147483648
|
||||||
|
|
||||||
monky_deployd_tenancy_service: monky.tenancy.deploy
|
monky_deployd_tenancy_service: monky.tenancy.deploy
|
||||||
monky_deployd_tenancy_port: 8081
|
monky_deployd_tenancy_port: 443 # the intercept port of monky.tenancy.deploy (host.v1 forwards to 8081 in the pod)
|
||||||
monky_deployd_bao_addr: "https://bao.cbs.tikali.net:8200"
|
monky_deployd_bao_addr: "https://bao.cbs.tikali.net:8200"
|
||||||
monky_deployd_bao_auth_mount: jwt-tenancy
|
monky_deployd_bao_auth_mount: jwt-tenancy
|
||||||
monky_deployd_bao_role: see-env
|
monky_deployd_bao_role: see-env
|
||||||
|
|||||||
+2
-1
@@ -108,7 +108,8 @@ download; xtrace is switched off). A 401 on the download means the token is miss
|
|||||||
lacks the scope. Inside the estate `gitea.cbs.tikali.net` is split-horizon to jump1's RED EIP
|
lacks the scope. Inside the estate `gitea.cbs.tikali.net` is split-horizon to jump1's RED EIP
|
||||||
(`10.10.0.175`), which has no HTTP ingress, so backend boxes cannot fetch from the Gitea mirror
|
(`10.10.0.175`), which has no HTTP ingress, so backend boxes cannot fetch from the Gitea mirror
|
||||||
(cbs/iac#102). Off-estate, `install.sh --source gitea` (ansible: `monky_deployd_base_url`/`_deb_url`,
|
(cbs/iac#102). Off-estate, `install.sh --source gitea` (ansible: `monky_deployd_base_url`/`_deb_url`,
|
||||||
no token) uses the Gitea release instead. The tag pipeline publishes to both (`release`, `release:gitea`).
|
no token) uses the Gitea release instead. The tag pipeline always publishes to GitLab (`release`); `release:gitea` is automatic only with
|
||||||
|
`GITEA_TOKEN` in CI and a manual job otherwise — it has not run on a tag pipeline yet.
|
||||||
|
|
||||||
## Publishing a release to Gitea by hand
|
## Publishing a release to Gitea by hand
|
||||||
|
|
||||||
|
|||||||
+18
-7
@@ -10,8 +10,16 @@ The agent side of the backend lifecycle agent protocol — [MONKY-ADR-0028](http
|
|||||||
|
|
||||||
- **Where:** tenancy's **agent entrypoint** — a second container (`app.agent_main`,
|
- **Where:** tenancy's **agent entrypoint** — a second container (`app.agent_main`,
|
||||||
`127.0.0.1:8081`) bound to the ziti service **`monky.tenancy.deploy`** by a `ziti-edge-tunnel
|
`127.0.0.1:8081`) bound to the ziti service **`monky.tenancy.deploy`** by a `ziti-edge-tunnel
|
||||||
run-host` sidecar. Unreachable from the public ingress. Only host identities carrying
|
run-host` sidecar. Unreachable from the public ingress. **The agent dials the service's
|
||||||
`#monky-deploy-agent` may dial (the broker adds the attr when the identity is created at kit reveal).
|
INTERCEPT port, `tcp/443`** (`monky.tenancy.deploy.intercept.v1` in openziti
|
||||||
|
`state/overlay/configs.json` — the authority for the port); the sidecar's `host.v1` forwards to
|
||||||
|
`127.0.0.1:8081` inside the pod. `tenancy.port` in the config, `install.sh` and the ansible role all
|
||||||
|
default to 443 (0.1.5). Only host identities carrying `#monky-deploy-agent` may dial
|
||||||
|
(`monky.tenancy.deploy.dial`, openziti). Adding that attr (and `#openbao-client`) to
|
||||||
|
`monky-host.<env>` at kit reveal is the ADR-0028 addendum and is **not implemented in monky-ziti
|
||||||
|
yet** (its host template `app/fabric.py` `host_identity_attrs` does not carry it; openziti
|
||||||
|
`docs/services.md` §Platform services records when it does) — until then an operator adds both
|
||||||
|
attrs on the controller by hand after enrolment.
|
||||||
- **Transport:** plain HTTP inside the mesh (`transport: sdk` dials by service name;
|
- **Transport:** plain HTTP inside the mesh (`transport: sdk` dials by service name;
|
||||||
`proxy` → `127.0.0.1:18443`). OpenBao is the existing `openbao` ziti service (`#openbao-client`),
|
`proxy` → `127.0.0.1:18443`). OpenBao is the existing `openbao` ziti service (`#openbao-client`),
|
||||||
dialled as `https://bao.cbs.tikali.net:8200` (an intercept name), TLS validated against the
|
dialled as `https://bao.cbs.tikali.net:8200` (an intercept name), TLS validated against the
|
||||||
@@ -29,7 +37,7 @@ The agent side of the backend lifecycle agent protocol — [MONKY-ADR-0028](http
|
|||||||
```
|
```
|
||||||
kit reveal (admin, once) tenancy signs a bootstrap DEPLOY GRANT: ES256 JWT
|
kit reveal (admin, once) tenancy signs a bootstrap DEPLOY GRANT: ES256 JWT
|
||||||
iss <tenancy issuer>, aud openbao-see-env, sub agent:<env_id>,
|
iss <tenancy issuer>, aud openbao-see-env, sub agent:<env_id>,
|
||||||
env_id, kind deploy-grant, jti (single-use, recorded), exp now+1h
|
env_id, kind deploy-grant, jti (→ token meta grant_jti; tenancy records it), exp now+1h
|
||||||
install.sh stages it at /etc/monky-deployd/bootstrap.jwt (0600)
|
install.sh stages it at /etc/monky-deployd/bootstrap.jwt (0600)
|
||||||
first tick POST https://bao…/v1/auth/jwt-tenancy/login {"role":"see-env","jwt":<grant>}
|
first tick POST https://bao…/v1/auth/jwt-tenancy/login {"role":"see-env","jwt":<grant>}
|
||||||
→ auth.client_token (ttl 24 h, max 30 d, policy see-env,
|
→ auth.client_token (ttl 24 h, max 30 d, policy see-env,
|
||||||
@@ -49,10 +57,13 @@ OpenBao side (Terraform, `tikali/services/security/openbao`): mount `jwt-tenancy
|
|||||||
`monky/data/{{identity.entity.aliases.<accessor>.metadata.env_id}}/see/*` read + metadata list +
|
`monky/data/{{identity.entity.aliases.<accessor>.metadata.env_id}}/see/*` read + metadata list +
|
||||||
`auth/token/{renew-self,lookup-self}`. **Gate 1 v2 passed 2026-09-05 07:40Z** (distinct entity per env).
|
`auth/token/{renew-self,lookup-self}`. **Gate 1 v2 passed 2026-09-05 07:40Z** (distinct entity per env).
|
||||||
|
|
||||||
**Second-reveal semantics.** A kit re-reveal (or a retire) supersedes every earlier grant of the
|
**Second-reveal semantics.** A kit re-reveal (or a retire) supersedes every earlier bootstrap grant
|
||||||
backend and revokes the token accessors tenancy knows. The old kit's grant fails at login
|
of the backend (tenancy marks their `backend_leases` rows revoked) and revokes the token accessors
|
||||||
(unknown/used `jti`), and a token already minted from it is refused at the next call with
|
tenancy knows. The `jwt-tenancy` mount keeps **no** replay state — it validates signature, `aud`,
|
||||||
`401 AGENT_UNAUTHENTICATED` (its `meta.grant_jti` is superseded) — **not** `AGENT_ENV_MISMATCH`.
|
`bound_claims` and `exp` only (openbao `terraform/jwt-tenancy.tf`, the `see_env` role) — so a
|
||||||
|
superseded grant still logs in until its 1 h `exp`; the token it yields, like a token already minted
|
||||||
|
from it, is refused by tenancy at the first bearer call with `401 AGENT_UNAUTHENTICATED` (its
|
||||||
|
`meta.grant_jti` is superseded) — **not** `AGENT_ENV_MISMATCH`.
|
||||||
The agent then deletes its token; if a fresh `bootstrap.jwt` is on disk it bootstraps again in the
|
The agent then deletes its token; if a fresh `bootstrap.jwt` is on disk it bootstraps again in the
|
||||||
same tick, otherwise it exits 1 and says "re-run the install kit".
|
same tick, otherwise it exits 1 and says "re-run the install kit".
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ def parse_yaml_subset(text: str) -> dict:
|
|||||||
class TenancyCfg:
|
class TenancyCfg:
|
||||||
service: str = "monky.tenancy.deploy"
|
service: str = "monky.tenancy.deploy"
|
||||||
host: str = "" # intercept host; defaults to `service`
|
host: str = "" # intercept host; defaults to `service`
|
||||||
port: int = 8081
|
port: int = 443 # the service's INTERCEPT port (openziti); the pod-side listener is 127.0.0.1:8081
|
||||||
scheme: str = "http"
|
scheme: str = "http"
|
||||||
proxy_addr: str = "127.0.0.1:18443"
|
proxy_addr: str = "127.0.0.1:18443"
|
||||||
timeout_s: int = 30
|
timeout_s: int = 30
|
||||||
|
|||||||
+4
-2
@@ -190,9 +190,11 @@ class FakeBao:
|
|||||||
return h._send(400, {"errors": ["error validating token: expired"]})
|
return h._send(400, {"errors": ["error validating token: expired"]})
|
||||||
if body.get("role") != self.role:
|
if body.get("role") != self.role:
|
||||||
return h._send(400, {"errors": [f"role {body.get('role')!r} could not be found"]})
|
return h._send(400, {"errors": [f"role {body.get('role')!r} could not be found"]})
|
||||||
meta = self.grants.pop(body.get("jwt", ""), None) # single-use grant
|
# the jwt mount keeps no jti/replay state: a known grant logs in until it expires;
|
||||||
|
# a SUPERSEDED grant is refused by tenancy (FakeTenancy.superseded_jtis), not here
|
||||||
|
meta = self.grants.get(body.get("jwt", ""))
|
||||||
if meta is None:
|
if meta is None:
|
||||||
return h._send(400, {"errors": ["error validating token: unknown or already used grant"]})
|
return h._send(400, {"errors": ["error validating token: unknown grant"]})
|
||||||
m = json.loads(meta)
|
m = json.loads(meta)
|
||||||
tok = self.mint(m["env_id"])
|
tok = self.mint(m["env_id"])
|
||||||
self.tokens[tok]["grant_jti"] = m["jti"]
|
self.tokens[tok]["grant_jti"] = m["jti"]
|
||||||
|
|||||||
+1
-1
@@ -93,7 +93,7 @@ def test_proxy_transport_refuses_unmapped_hosts():
|
|||||||
|
|
||||||
cfg = from_dict({"env_id": "env-dev-06", "site": "cbs", "transport": "proxy"})
|
cfg = from_dict({"env_id": "env-dev-06", "site": "cbs", "transport": "proxy"})
|
||||||
t = build(cfg)
|
t = build(cfg)
|
||||||
assert t.mapping[("monky.tenancy.deploy", 8081)] == ("127.0.0.1", 18443)
|
assert t.mapping[("monky.tenancy.deploy", 443)] == ("127.0.0.1", 18443)
|
||||||
assert t.mapping[("bao.cbs.tikali.net", 8200)] == ("127.0.0.1", 18200)
|
assert t.mapping[("bao.cbs.tikali.net", 8200)] == ("127.0.0.1", 18200)
|
||||||
try:
|
try:
|
||||||
t.connect("example.com", 443, 1)
|
t.connect("example.com", 443, 1)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ transport: sdk
|
|||||||
identity: /opt/openziti/etc/identities/monky-host.env-qa-02.json
|
identity: /opt/openziti/etc/identities/monky-host.env-qa-02.json
|
||||||
tenancy:
|
tenancy:
|
||||||
service: monky.tenancy.deploy
|
service: monky.tenancy.deploy
|
||||||
base_url: http://monky.tenancy.deploy:8081
|
base_url: http://monky.tenancy.deploy:443
|
||||||
bao:
|
bao:
|
||||||
service: openbao
|
service: openbao
|
||||||
addr: https://bao.cbs.tikali.net:8200 # intercept, not public DNS
|
addr: https://bao.cbs.tikali.net:8200 # intercept, not public DNS
|
||||||
@@ -27,7 +27,7 @@ volumes_on_absent: keep
|
|||||||
def test_yaml_subset_parses_nested_maps_and_types():
|
def test_yaml_subset_parses_nested_maps_and_types():
|
||||||
d = c.parse_yaml_subset(KIT)
|
d = c.parse_yaml_subset(KIT)
|
||||||
assert d["env_id"] == "env-qa-02"
|
assert d["env_id"] == "env-qa-02"
|
||||||
assert d["tenancy"]["base_url"] == "http://monky.tenancy.deploy:8081"
|
assert d["tenancy"]["base_url"] == "http://monky.tenancy.deploy:443"
|
||||||
assert d["bao"]["addr"] == "https://bao.cbs.tikali.net:8200"
|
assert d["bao"]["addr"] == "https://bao.cbs.tikali.net:8200"
|
||||||
assert d["interval_s"] == 60 and d["laptop_mode"] is False
|
assert d["interval_s"] == 60 and d["laptop_mode"] is False
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ def test_yaml_subset_refuses_flow_style_and_tabs():
|
|||||||
|
|
||||||
def test_config_defaults_and_derivations():
|
def test_config_defaults_and_derivations():
|
||||||
cfg = c.from_dict(c.parse_yaml_subset(KIT))
|
cfg = c.from_dict(c.parse_yaml_subset(KIT))
|
||||||
assert cfg.tenancy.host == "monky.tenancy.deploy" and cfg.tenancy.port == 8081 and cfg.tenancy.scheme == "http"
|
assert cfg.tenancy.host == "monky.tenancy.deploy" and cfg.tenancy.port == 443 and cfg.tenancy.scheme == "http"
|
||||||
assert cfg.bao_url == ("https", "bao.cbs.tikali.net", 8200)
|
assert cfg.bao_url == ("https", "bao.cbs.tikali.net", 8200)
|
||||||
assert cfg.deploy_dir == "/var/lib/monky-deployd/env-qa-02"
|
assert cfg.deploy_dir == "/var/lib/monky-deployd/env-qa-02"
|
||||||
assert cfg.compose_project == "monky-env-qa-02"
|
assert cfg.compose_project == "monky-env-qa-02"
|
||||||
|
|||||||
Reference in New Issue
Block a user