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
40 changes: 40 additions & 0 deletions AUDIT_OPEN.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,45 @@
# Open audit items

## 2026-11-05 — Dash auto-config Slice 5: domain health (CLI side)

Plan: `../_internal/DASH_AUTOCONFIG_PLAN_2026-11-03.md` section 8. Branch
`slice5-domain-health`.

Closed: U5 root cause and TCL-10's stale half (a release leaving caddy ingress
retires the block after the state commit, CAS + fence; rollback both directions);
reserved names under caddy ingress; compose placeholder; the three DNS bugs
(multi-host, validate/deploy disagreement, equality-only); `domains check`.
Found along the way and fixed: host-to-caddy rollback rendered an empty CAS list;
the release record dropped `tls.http_only`; rollback state kept the old domain.

Review fixes (same branch): the reserved-name rule moved out of `validate()` to
deploy admission (`ValidateDeployDomain`: deploy, multi-server, server-config,
autodeploy, plan, validate), after overlays merge, so rollback, logs and other
read and recovery commands never refuse, and Compose-only folders load; redeploy
skips it. Custom certificates and localhost names are exempt. Public address is
known per family. A wildcard's failing obtain covers its hosts. `tls_http_only` is
a `*bool`, so the record can turn the flag off. Log parsing: an empty log is
short, and a matched line that cannot be attributed makes logs `partial`.

Open:
- teploy-observe's `teploy.yml` still carries `domain:` and `server:
observe.example.com` under `ingress: host`. Legal (label-only warning); remove
it there.
- A crash between the state commit and the block removal leaves the block until
the next deploy; `domains check` reports it as `stale_managed` meanwhile.
- A managed block whose owner is not deployed is reported (`orphan_managed`), not
removed. Removal is plan P5 (`route remove`).
- The DNS lookup uses Go's resolver with a fixed dial target, which still honors
the local /etc/hosts before asking. A hosts entry for a public name would hide
the public answer.
- `tls.obtain`/`tls.renew` parsing assumes Caddy's default JSON log; another log
format reads `unknown` for the whole server (`unavailable`), never zero.
- `teploy scale` builds its deploy config without deploy admission: it resizes
what is already running, the same reasoning as redeploy.
- Public address detection: interface addresses plus a best-effort probe of
1.1.1.1's trace endpoint from the server. Without either a foreign answer reads
`unknown`. Explicit `public_ips` server fields remain plan v2.

## 2026-11-03 — Dash auto-config Slice 2: backup correctness (G6, G7)

Plan: `../_internal/DASH_AUTOCONFIG_PLAN_2026-11-03.md` (U3, U4, P1 G6/G7,
Expand Down
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,40 @@ All notable changes to teploy are documented here. Format follows [Keep a Change

## [Unreleased]

### Added

- `teploy domains check [--server X] [--app A] [--log-window D] [--json]`: DNS
from fixed public resolvers, Caddy certificate failures and route health per
(server, app, host). Read-only; advertised as `domains-check-v1`. What cannot
be established reads `unknown`, never healthy.

### Changed

- Reserved names (RFC 2606/6761: example.com/.net/.org, *.example, .test,
.invalid) are refused by `deploy`, `plan` and `validate` under caddy ingress,
after overlays merge, unless `tls.internal`, `tls.http_only` or your own
`tls: {cert, key}` is set. They are warned as label-only under host and external
ingress. The file still loads: `rollback`, `logs` and other read and recovery
commands never refuse over it. localhost names are not refused.
- Compose import no longer fabricates `<app>.example.com`; `deploy` asks for a
domain, and the folder still loads for everything else.

### Fixed

- A deploy or rollback that leaves caddy ingress removes the managed Caddy block
the earlier release left, after the state commit, under the fence and a CAS on
the block's hash, and `teploy plan` shows it. Those blocks kept Caddy retrying
certificates for names nothing served. A failed deploy leaves the block alone.
- Rollback from a host or external release to a caddy release works: it resolved
its route expectation against the wrong ingress and rendered an empty CAS.
- A release record keeps `tls.http_only`, so a rollback or a route restore follows
what that release served, in both directions.
- State after a rollback names the target release's domain.
- Multi-host domains pass the first-deploy DNS check (the comma list went to the
resolver as one name), `validate` no longer DNS-checks host-ingress apps that
`deploy` skips, addresses compare by value, and a Cloudflare answer is a note
rather than a mismatch.

## [0.1.50] - 2026-11-04

### Fixed
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,32 @@ teploy network setup <provider> # VPN mesh (tailscale, headscale, netbird)
teploy network status / join # VPN management
```

### Domains

```bash
teploy domains check # every server in servers.yml
teploy domains check --server edge --app web
teploy domains check --json # contracts/schema/domains-check-envelope.schema.json
```

One row per (server, app, host), plus hosts only a hand-written Caddy block
serves. Read-only, run from your machine over SSH, nothing new on the server:

- **dns**: what 1.1.1.1 and 8.8.8.8 say, not your local resolver: `ok`,
`nxdomain`, `wrong_target`, `proxied` (Cloudflare answers; a hint, not a
failure), `unknown` (no resolver answered, or the server's public address
could not be determined), `skipped`.
- **tls**: certificate failures per name from Caddy's logs (`--log-window`,
default 14 days), with how long the name has been failing; a wildcard's failure
covers the names under it. Logs under a day old, unreadable, or with a line
that cannot be attributed give `unknown`, never 0.
- **route**: `managed`, `manual`, `stale_managed` (the app is no longer
caddy-routed), `orphan_managed` (no such app here), `none`, and whether each
upstream resolves in Caddy's network.

`ingress: external` and an unfronted `ingress: host` domain are labels: they
carry a `skip` reason. A server that cannot be reached is reported as such.

### Accessories
```
teploy accessory list # list running accessories
Expand Down
2 changes: 2 additions & 0 deletions contracts/MANIFEST.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Neutron/Nucleus dependency and a public mirror.

| Corpus rev | Emitting CLI | Machine Interface | Notes |
|---|---|---|---|
| 17 | main (dash auto-config slice 5: domain health) | 2 | Additive. New artifact domains-check-envelope (`teploy domains check [--server] [--app] [--log-window] --json`: per (server, app, host) DNS via the fixed public resolvers, Caddy certificate failures from its logs — `failures` is null, never 0, on short or unavailable logs — and the route with upstream resolution; per-server state so an unreachable server is explicit), fixture GENERATED from the real `sweepDomains` over a mock executor. version-handshake advertises `domains-check-v1` (regenerated mi2.json). release-record's `caddy` gains optional `tls_http_only` (absent on older records; every caddy release since records true or false). No MI bump. |
| 16 | main (server-authority spec P3 / plan D4, git sources) | 2 | Additive. New artifacts source-show-envelope (`config source show --json`: repository/branch/poll, public credential facts — type, deploy-key public half + fingerprint, token username, pinned host-key fingerprints — and the per-ref admission state), source-key-envelope (`config source key --json` for ssh/token and `host-key --json`) and trigger-poll-envelope (`trigger poll --json`: one result object per app — ref, head, admitted, reason vocabulary incl. force-push/retry-limit/not-due, deployed_release), all fixtures GENERATED from the real DTOs. version-handshake advertises `git-source-v1` (git: code source, per-app credentials outside /deployments, build_secrets) and `trigger-poll-v1` (the one admitter + reset/retry/install-timer) (regenerated mi2.json). No MI bump. |
| 15 | main (server-authority spec D3: backup target) | 2 | Additive. One backup target per server, stored outside /deployments with age-encrypted credentials and read at run time (`server backup-target set/show/unset`; `backup-target-v1`). New artifacts backup-target-envelope, backup-list-envelope, backup-create-envelope, backup-restore-envelope — fixtures GENERATED from the real DTO encoders (`backup list --json` now reports parsed rows {id, kind, accessory, size_bytes, created_at, key} plus schedules discovered from teploy cron markers; the old bare-array-of-raw-`aws s3 ls`-lines `backup list --json` shape is gone — it was never a corpus artifact and pinned no consumer). `backup create/restore --json` envelopes; restore quiesces the app under its fenced lock (backup-json-v1). version-handshake advertises `backup-target-v1` and `backup-json-v1` (regenerated mi2.json). No MI bump. |
| 14 | main (server-authority plan D2, folderless reads) | 2 | Additive. New artifacts releases-envelope (`teploy releases --app X --host S --json`: container release records newest first with current/rollback_available (+reason); static apps list retained releases — both via the envelope, the folder-static legacy bare array is unchanged), rollback-result (`teploy rollback [--to X] --json`: {app, server, from, to, ok}; failures keep the error envelope) and redeploy-result (`teploy redeploy [--json]`: {app, server, home, release, image, ok}). Fixtures GENERATED from the real DTOs. version-handshake advertises `secret-stdin-v1` (secret set KEY --stdin; folderless secret set/get/list/rm/rotate), `releases-json-v1`, `rollback-json-v1`, `redeploy-v1` (fresh deploy of the current release's recorded image, env re-resolved) and `env-config-guard-v1` (env set/unset refuse keys a Teploy-home config declares) (regenerated mi2.json). No MI bump. || 13 | main (server-authority spec P2 support) | 2 | Additive. New artifact config-merge-envelope (`teploy config merge --base <commit> --file - --json`: merged config + conflicting keys, read-only), fixture GENERATED from the real configMergeDTO. version-handshake advertises `config-merge-v1`, `config-unprivileged-v1` (`config ... --unprivileged` refuses host-reaching changes) and `deploy-home-v1` (`deploy --app X --image T --home teploy` creates a new Teploy-home app) (regenerated mi2.json). No MI bump. |
Expand Down Expand Up @@ -54,6 +55,7 @@ Neutron/Nucleus dependency and a public mirror.
| backup-list-envelope | yes (rev 15) | valid x2 (app, legacy-schedule; real backupListDTO) | teploy-cli |
| backup-create-envelope | yes (rev 15) | valid x2 (volumes, accessory; real backupCreateDTO) | teploy-cli |
| backup-restore-envelope | yes (rev 15) | valid x2 (ok, unhealthy; real backupRestoreDTO) | teploy-cli |
| domains-check-envelope | yes (rev 17) | valid (one server with each finding, one unreachable; real `sweepDomains`) | teploy-cli |
| releases-envelope | yes (rev 14) | valid x2 (container app, static app; real releasesDTO) | teploy-cli |
| rollback-result | yes (rev 14) | valid (real rollbackResult) | teploy-cli |
| redeploy-result | yes (rev 14) | valid x2 (teploy-home, folder-home; real redeployDTO) | teploy-cli |
Expand Down
186 changes: 186 additions & 0 deletions contracts/fixtures/domains-check-envelope/valid/sweep.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
{
"machine_interface": 2,
"checked_at": "2026-11-04T12:00:00Z",
"log_window_seconds": 1209600,
"resolvers": [
"1.1.1.1:53",
"8.8.8.8:53"
],
"cloudflare_ranges_as_of": "2026-11-04",
"servers": [
{
"server": "down",
"host": "198.51.100.1",
"state": "unreachable",
"checked_at": "2026-11-04T12:00:00Z",
"error": "dial tcp 198.51.100.1:22: i/o timeout",
"addresses": [],
"public_address_known": false,
"public_ipv4_known": false,
"public_ipv6_known": false,
"caddy": {
"container": "unknown",
"caddyfile": "unreadable",
"logs": "unavailable",
"log_coverage_seconds": 0
},
"issues": []
},
{
"server": "edge",
"host": "192.0.2.10",
"state": "ok",
"checked_at": "2026-11-04T12:00:00Z",
"addresses": [
"172.17.0.1",
"192.0.2.10"
],
"public_address_known": true,
"public_ipv4_known": true,
"public_ipv6_known": false,
"caddy": {
"container": "present",
"caddyfile": "present",
"logs": "ok",
"log_coverage_seconds": 1209600
},
"issues": []
}
],
"domains": [
{
"server": "edge",
"app": "blog",
"host": "blog.dev",
"ingress": "caddy",
"dns": {
"status": "ok",
"answers": [
"192.0.2.10"
]
},
"tls": {
"status": "ok",
"failures": 0
},
"route": {
"kind": "managed",
"owner": "blog",
"tls": "acme",
"upstreams": [
{
"dial": "blog-web-v1:3000",
"resolves": "yes"
}
],
"upstream_resolves": "yes"
}
},
{
"server": "edge",
"app": "dash",
"host": "dash.example.com",
"ingress": "host",
"skip": "reserved_name",
"dns": {
"status": "skipped",
"answers": [],
"detail": "reserved name: never publicly resolvable"
},
"tls": {
"status": "failing",
"failures": 1,
"failing_since": "2026-10-25T11:00:00Z",
"last_failure_at": "2026-11-04T11:00:00Z",
"last_error": "[dash.example.com] Obtain: no solvers"
},
"route": {
"kind": "stale_managed",
"owner": "dash",
"tls": "acme",
"upstreams": [
{
"dial": "dash-web-old:3456",
"resolves": "no"
}
],
"upstream_resolves": "no"
}
},
{
"server": "edge",
"app": "front",
"host": "front.dev",
"ingress": "external",
"skip": "external_label",
"dns": {
"status": "skipped",
"answers": []
},
"tls": {
"status": "not_applicable",
"failures": null
},
"route": {
"kind": "none",
"upstreams": [],
"upstream_resolves": "not_applicable"
}
},
{
"server": "edge",
"app": "",
"host": "hand.dev",
"ingress": "",
"dns": {
"status": "nxdomain",
"answers": [],
"detail": "no address record at the public resolvers"
},
"tls": {
"status": "ok",
"failures": 0
},
"route": {
"kind": "manual",
"tls": "acme",
"upstreams": [
{
"dial": "127.0.0.1:9000",
"resolves": "not_applicable"
}
],
"upstream_resolves": "yes"
}
},
{
"server": "edge",
"app": "blog",
"host": "www.blog.dev",
"ingress": "caddy",
"dns": {
"status": "proxied",
"answers": [
"104.16.0.1"
],
"detail": "answers from Cloudflare: origin cannot be read from DNS"
},
"tls": {
"status": "ok",
"failures": 0
},
"route": {
"kind": "managed",
"owner": "blog",
"tls": "acme",
"upstreams": [
{
"dial": "blog-web-v1:3000",
"resolves": "yes"
}
],
"upstream_resolves": "yes"
}
}
]
}
1 change: 1 addition & 0 deletions contracts/fixtures/version-handshake/valid/mi2.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"config-unprivileged-v1",
"deploy-home-v1",
"doctor-diagnostics",
"domains-check-v1",
"env-config-guard-v1",
"env-set-stdin",
"error-envelope",
Expand Down
Loading
Loading