Basic Auth / custom labels are API-settable on applications at 4.1.2 — cast just can't say them; rebuilt resources stay unprotected #76
Labels
No labels
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-triage
ready
release
scope:apply
scope:capture
scope:coolify-api
scope:fleet
scope:manifest
scope:secrets
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/cast#76
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
UNCAPTURED.md's
NO_API_COVERAGErow says Basic Auth / custom Traefik labels are "carried as raw container labels. cast's manifest has no field for them, so a rebuilt resource is UNPROTECTED where the original was not." The #72 audit found that for applications this is a cast vocabulary gap, not a Coolify gap, at v4.1.2:custom_labels(base64-validated,ApplicationsController.php:3836-3854),is_http_basic_auth_enabled,http_basic_auth_username,http_basic_auth_passwordare all in both create and PATCH allowlists (:914,:2368).:2446-2463).So a manifest could declare basic auth (and optionally raw labels) and
applycould set them — closing the "rebuilt resource is silently unprotected" hole for the app case.Caveats that shape the design
custom_labelsback requires a sensitive-data-enabled token at 4.1.2, and onnextthis hardens into theread:sensitivetoken ability (see the v4.2 tracker) — the diff story depends on what the token can see; fail honest when it can't.generateLabelsApplication()which clobberscustom_labelsunlessis_container_label_readonly_enabled— which is itself NOT API-settable at 4.1.2 (it is onnext). Declaring bothcustom_labelsand domains/basic-auth on one app is therefore a footgun until v4.2; basic-auth-only is the safe first slice.ServicesController, on either release. The UNCAPTURED row stays true for services and should say so specifically instead of blanket-covering both. (Unclaimed upstream; #72 feature-request list item 4.)http_basic_auth_passwordbelongs in the age store as a${REF}, never a manifest literal.What to do
custom_labelsonly if a real use appears (the overwrite caveat makes it hostile until v4.2)Optional/low-priority — filed so #72's finding isn't lost. Found in #72 (finding 7).
Deferred by operator decision (2026-07-16) — leaving open. Research from attempting it, so the next attempt starts ahead:
1.
custom_labelsis unsound to manage declaratively at 4.1.2 — recommend dropping it from this issue's scope. It is API-settable, but Coolify regenerates container labels on any domain or basic-auth change and overwritescustom_labels; the flag that would prevent that (is_container_label_readonly_enabled) is itself not API-settable in 4.1.2. So cast would write it and Coolify would clobber it — perpetual drift, or a value silently reverted. That is precisely the shape cast legislates against (#12/#14/#17/#18). It only becomes tractable ifis_container_label_readonly_enabledgains API surface — worth checking onnextas part of #77.2. Basic Auth is the sound half, but it forces a design decision.
is_http_basic_auth_enabled/http_basic_auth_username/http_basic_auth_passwordare in both create and PATCH allowlists, and Coolify owns the generated label when basic auth is enabled — so there is no clobbering problem. The snag is that the password is a secret, and cast's iron rule is no secret values in the manifest. The clean design:usernameplus a${REF}for the password, resolved from the age store exactly as env secrets are (sorequiredSecretsmust collect it andcapturemust store it — that is the real cost: it touches the secret-collection path, not just a schema);is_http_basic_auth_enabled+usernameare readable (removeSensitiveDatahides neither) → diffed normally;http_basic_auth_passwordis hidden behindread:sensitive→ declared-not-compared (the house pattern frombackup/is_static/destination), which also means a password-only rotation isn't detectable. Worth saying out loud in the draft/semantics if built.So the audit's framing holds — this is a cast vocabulary gap for applications — but the sound fix is a secret-machinery change, not a field addition. Hence deferring rather than rushing it.