IR-20261112: feat(monitoring): ship openbao-0 audit stdout to a 45d Loki tenant (R10 PR2) — BUILD ONLY, not for landing #959

Open
pi-landing wants to merge 1 commit from plane/IR-20261112-r10-openbao-loki into live
Collaborator

What / why

OpenBao's single audit device writes to the container's stdout
(openbao-config → audit "file" "file" { file_path = "stdout" }, HOMELAB-1424),
and the kubelet keeps a ~10 MiB × 5 ring on one node — it rotates in about 3
minutes
on prod-wk-05 (measured 2026-11-12: the earliest retained line was
12:09:36Z when read at ~12:12Z). Nothing shipped those lines anywhere, so there
was no durable audit trail and INC-RUN3's 12:07Z OpenBao record is lost.

This is PR 2 of the R10 pair — the Loki half. It ships openbao-0's stdout to
Loki tenant audit-openbao (45 d, admin-only read). It needs no OpenBao
restart
and is independent of PR 1 (which adds the PVC-backed device).

BUILD ONLY — NOT FOR LANDING. No apply, no restart, no rollout in this PR.

MERGE GATE (overseer, 12:40:04Z) — #959 merges ONLY after both legs hold

This PR must NOT be merged until (a) #953's PR-A Loki auth_enabled: true /
tenancy is LIVE, and (b) the 401 proof exists
: a writer that does NOT send
X-Scope-OrgID is REJECTED by Loki with 401. Until both hold, #959 stays
unmerged.

Land order: #953 PR-A (Loki auth/tenancy) → the 401 proof → #959.

Why the gate and not a self-check: Vector's Loki sink receives HTTP 200 whether
auth_enabled is true (data lands under audit-openbao) or false (data lands
under the DEFAULT tenant, readable by every Grafana Viewer and outside the 45-day
override), so the shipper cannot detect this state from its own responses. Order
is the only boundary — §"DEPENDENCY ORDER" below has the live evidence.

Changes

  1. core/manifests/monitoring/log-metrics/vector-openbao-audit.yaml (new,
    self-contained: ConfigMap + DaemonSet + Service + ServiceMonitor)
    • file source, include /var/log/pods/openbao_openbao-0_*/openbao/*.log*.
      The glob is the selector: namespace openbao, pod openbao-0,
      container openbao — and nothing else in the namespace (the
      openbao-raft-snapshot-* Job pods are not matched; a namespace-wide glob
      would ship every snapshot Job into a 45-day store).
    • Allowlist, not a blocklist. The transform strips CRI framing, parses the
      JSON and ships only entries whose "type" is request or response
      (the only two the audit device emits). Ships the de-framed JSON body, so
      LogQL | json works on the stored value.
    • Filter drops are counted, never silent:
      openbao_audit_log_dropped_total{node,reason="unparsed"|"partial"}.
      partial is separate on purpose — a CRI P fragment (stdout line > 16 KiB)
      is the one case where a real audit record is lost, so it has a zero
      baseline and its own series.
    • Sink: type: loki, tenant_id: audit-openbao (Vector sends it as
      X-Scope-OrgID), gzip, unlimited retries with a capped backoff,
      acknowledgements.enabled: true so the file checkpoint advances only after
      Loki acknowledges. Labels job/ns/pod/container/node/audit_type.
    • Same image and digest as the existing Vector unit; read_from: end
      (no backfill of the pre-shipper ring); data_dir on a hostPath so a
      shipper restart resumes instead of re-reading.
  2. core/charts/platform/loki/values.yaml — runtimeConfig.overrides.audit-openbao:
    retention_period: 1080h (45 d) and deletion_mode: disabled (without the
    latter, Loki 3.x defaults to filter-and-delete and anything that can name the
    tenant by header could POST /loki/api/v1/delete). The global 30 d is
    untouched. Rendered against grafana/loki 6.29.0: the override reaches the
    loki-runtime ConfigMap.
  3. core/manifests/monitoring/log-metrics/rule-tests/ — offline,
    fail-capable cases for the allowlist + a runner that extracts the ConfigMap
    payload (single source of truth) and drives vector test with 0.61.0, the
    version the DaemonSet runs.
  4. docs/runbooks/openbao-audit-log-shipping.md — dependency order, the
    LogQL read-back, the filter semantics, the admin-only read, residual risks.

Tenancy pattern reused

Homelab/infra-core#953 — core/manifests/monitoring/log-metrics/vector-longhorn.yaml
on branch plane/R39-forgejo-access-log, sink loki_forgejo_audit at :453-500
(type: loki + tenant_id + gzip + retries + acknowledgements) and
core/charts/platform/loki/values.yaml:84-96 there (runtimeConfig.overrides.<tenant>
with retention_period: 1080h + deletion_mode: disabled). Tenant names, one per
purpose: main (human-facing), audit-forgejo (#953), audit-openbao (this PR).

DEPENDENCY ORDER — the one silent failure mode

Loki ignores X-Scope-OrgID while auth_enabled: false. The sink gets HTTP
200 either way, so it cannot detect the wrong state; ordering is the only
boundary.

Live state at build time (read-only, 2026-11-12):

$ kubectl -n monitoring get cm loki -o yaml | grep -n "auth_enabled|retention_period|deletion_mode"
5:    auth_enabled: false
41:      retention_period: 30d              # global; no per-tenant override exists
$ kubectl -n monitoring get cm loki-runtime -o jsonpath='{.data}'
{"runtime-config.yaml":"{}\n"}

auth_enabled is false; audit-openbao does not exist live; #953 is OPEN, not
merged.
Proposed order:

  1. #953 lands and its Loki Application syncs with auth_enabled: true — prove
    it (an unlabeled read is refused; loki_distributor_bytes_received_total{tenant="main"}
    moves).
  2. Then this DaemonSet syncs. If this is applied first, the audit trail lands
    in the default tenant — readable by every Grafana Viewer and outside the
    45-day override.
  3. Rollback while auth is on: scale the DaemonSet to 0. Do not revert the
    tenant override while data exists (it would age out on the global 30 d).

If El Director prefers one change, the sink stanza moves into #953's
vector-longhorn.yaml and this PR closes unmerged.

Read-back (runbook step, NOT executed)

Make one idempotent call (bao status against sys/health), then:

{job="openbao-audit"} |= "sys/health"
{job="openbao-audit", audit_type="request"} | json

Pass: the row's time matches the call, request_path is the path requested,
and a token-ish field reads hmac-sha256:<64 hex> — HMAC'd, no raw token.
Stop if: any field shows a plaintext token, or audit_type="request" is empty
while OpenBao is serving — check sum by (reason) (increase(openbao_audit_log_dropped_total[15m]))
before touching the config.

Test result (offline, fail-capable; vector 0.61.0, the DaemonSet's own binary)

$ VECTOR=…/vector ./core/manifests/monitoring/log-metrics/rule-tests/run-openbao-audit-vector-tests.sh
vector: vector 0.61.0 (x86_64-unknown-linux-musl 17e017e 2026-10-29 16:03:40.929269968)
√ Loaded ["…/vector.yaml"]
√ Transforms configuration
√ Sinks configuration
                        Validated
Running tests
test audit request ships de-framed ... passed
test audit response ships ... passed
test openbao server log line is dropped as unparsed (non-JSON) ... passed
test JSON with an unknown type is not an audit record ... passed
test a CRI partial audit fragment is counted, not shipped ... passed

Three legs keep it honest: the positive controls (request and response reach
the sink, de-framed, with the hmac-sha256: field intact), the negative controls
(OpenBao's own [INFO] server log and non-audit JSON do not), and the loss case
(CRI P fragment counted as partial, not shipped). shellcheck clean;
yamllint -d relaxed clean (warnings only, same as the existing unit).

Loki render check (helm template loki grafana/loki --version 6.29.0 -f <values>):

data:
  runtime-config.yaml: |
    overrides:
      audit-openbao:
        deletion_mode: disabled
        retention_period: 1080h

Deviations from the brief, stated rather than hidden

  • file source, not kubernetes_logs. The brief named kubernetes_logs
    scoped to ns/pod. This uses the repo's existing Vector convention
    (vector-longhorn.yaml), which is deliberately API-free: hostPath
    /var/log/pods, automountServiceAccountToken: false, no RBAC.
    kubernetes_logs needs a cluster-wide pods watch — a new ClusterRole, for a
    log shipper — to select a pod the kubelet path already names exactly. Same
    selector, less privilege. Reviewer's call if you disagree.
  • A separate Vector unit, not an extra source in vector-longhorn.yaml.
    #953 rewrites that file end to end; sharing it would make this PR unlandable
    without #953 and couple the two rollbacks. Fold-in is a follow-up.
  • No Grafana datasource here. Admin-only read is #953's org-2 mechanism;
    #953's own check-grafana-datasources.py (assertion D) fails if any other
    file under core/ declares an orgId: 2 datasource, so a second file would
    break it. The exact second datasource body is in the runbook §6, to be added to
    #953's single reviewed bootstrap file when both land. Until then, treat
    audit-openbao as readable by anything that can reach loki:3100 and set a
    header (same residual as #953's risk 3) — the CNP is a network boundary, not a
    tenant boundary.
  • No secret anywhere. tenant_id is sent as the X-Scope-OrgID header and
    IS the tenant name, not a token. Nothing to put in an ExternalSecret; if a
    per-tenant gateway is added later, its credential must come from an
    ExternalSecret by name, never inline.

Not done here (named, not papered over)

  • CI wiring for the offline cases (vector-access-log-test is #953's file;
    this needs a .forgejo/workflows/ci.yaml edit that #953 is already making).
  • An alert on openbao_audit_log_dropped_total{reason="partial"} > 0 — the
    counter exists so the rule can be written without new instrumentation.
  • Fold the two Vector units into one DaemonSet after both land.
  • The prod comment at values/openbao/values.yaml:27-32 ("collected into Loki")
    is false until this applies; it becomes true with it. Not changed here (other
    repo).

Class

Treat as dangerous: it edits a platform chart's values
(core/charts/platform/loki/values.yaml) — the file #953 also edits — plus a new
workload in monitoring. Routine-from-a-security-standpoint (no RBAC, no secrets,
no network policy, no .forgejo/**), but the retention/tenancy config is the kind
of thing two reviewers should see. Two independent reviewer sessions if it is
landed.

Head commit carries Lab-Run: IR-20261112-r10 as its last trailer. Branch cut
from origin/live (2d837e9b).

## What / why OpenBao's single audit device writes to the **container's stdout** (`openbao-config` → `audit "file" "file" { file_path = "stdout" }`, HOMELAB-1424), and the kubelet keeps a ~10 MiB × 5 ring on one node — it rotates in about **3 minutes** on prod-wk-05 (measured 2026-11-12: the earliest retained line was 12:09:36Z when read at ~12:12Z). Nothing shipped those lines anywhere, so there was **no durable audit trail** and INC-RUN3's 12:07Z OpenBao record is lost. This is **PR 2 of the R10 pair — the Loki half.** It ships openbao-0's stdout to Loki tenant `audit-openbao` (45 d, admin-only read). It needs **no OpenBao restart** and is independent of PR 1 (which adds the PVC-backed device). > **BUILD ONLY — NOT FOR LANDING.** No apply, no restart, no rollout in this PR. ## MERGE GATE (overseer, 12:40:04Z) — #959 merges ONLY after both legs hold **This PR must NOT be merged until (a) #953's PR-A Loki `auth_enabled: true` / tenancy is LIVE, and (b) the 401 proof exists**: a writer that does NOT send `X-Scope-OrgID` is REJECTED by Loki with **401**. Until both hold, #959 stays unmerged. **Land order: #953 PR-A (Loki auth/tenancy) → the 401 proof → #959.** Why the gate and not a self-check: Vector's Loki sink receives HTTP 200 whether `auth_enabled` is true (data lands under `audit-openbao`) or false (data lands under the DEFAULT tenant, readable by every Grafana Viewer and outside the 45-day override), so the shipper cannot detect this state from its own responses. Order is the only boundary — §"DEPENDENCY ORDER" below has the live evidence. ## Changes 1. **`core/manifests/monitoring/log-metrics/vector-openbao-audit.yaml`** (new, self-contained: ConfigMap + DaemonSet + Service + ServiceMonitor) - `file` source, include `/var/log/pods/openbao_openbao-0_*/openbao/*.log*`. **The glob is the selector:** namespace `openbao`, pod `openbao-0`, container `openbao` — and nothing else in the namespace (the `openbao-raft-snapshot-*` Job pods are not matched; a namespace-wide glob would ship every snapshot Job into a 45-day store). - **Allowlist, not a blocklist.** The transform strips CRI framing, parses the JSON and ships **only** entries whose `"type"` is `request` or `response` (the only two the audit device emits). Ships the **de-framed JSON body**, so LogQL `| json` works on the stored value. - **Filter drops are counted, never silent**: `openbao_audit_log_dropped_total{node,reason="unparsed"|"partial"}`. `partial` is separate on purpose — a CRI `P` fragment (stdout line > 16 KiB) is the one case where a **real** audit record is lost, so it has a zero baseline and its own series. - **Sink**: `type: loki`, `tenant_id: audit-openbao` (Vector sends it as `X-Scope-OrgID`), gzip, unlimited retries with a capped backoff, `acknowledgements.enabled: true` so the file checkpoint advances only after Loki acknowledges. Labels `job/ns/pod/container/node/audit_type`. - Same image **and digest** as the existing Vector unit; `read_from: end` (no backfill of the pre-shipper ring); `data_dir` on a **hostPath** so a shipper restart resumes instead of re-reading. 2. **`core/charts/platform/loki/values.yaml`** — `runtimeConfig.overrides.audit-openbao`: `retention_period: 1080h` (45 d) and `deletion_mode: disabled` (without the latter, Loki 3.x defaults to `filter-and-delete` and anything that can name the tenant by header could POST `/loki/api/v1/delete`). The global 30 d is untouched. Rendered against `grafana/loki` 6.29.0: the override reaches the `loki-runtime` ConfigMap. 3. **`core/manifests/monitoring/log-metrics/rule-tests/`** — offline, fail-capable cases for the allowlist + a runner that extracts the ConfigMap payload (single source of truth) and drives `vector test` with 0.61.0, the version the DaemonSet runs. 4. **`docs/runbooks/openbao-audit-log-shipping.md`** — dependency order, the LogQL read-back, the filter semantics, the admin-only read, residual risks. ## Tenancy pattern reused Homelab/infra-core#953 — `core/manifests/monitoring/log-metrics/vector-longhorn.yaml` on branch `plane/R39-forgejo-access-log`, sink `loki_forgejo_audit` at **:453-500** (`type: loki` + `tenant_id` + gzip + retries + acknowledgements) and `core/charts/platform/loki/values.yaml:84-96` there (`runtimeConfig.overrides.<tenant>` with `retention_period: 1080h` + `deletion_mode: disabled`). Tenant names, one per purpose: `main` (human-facing), `audit-forgejo` (#953), `audit-openbao` (this PR). ## DEPENDENCY ORDER — the one silent failure mode **Loki ignores `X-Scope-OrgID` while `auth_enabled: false`.** The sink gets HTTP 200 either way, so it cannot detect the wrong state; **ordering is the only boundary.** Live state at build time (read-only, 2026-11-12): ``` $ kubectl -n monitoring get cm loki -o yaml | grep -n "auth_enabled|retention_period|deletion_mode" 5: auth_enabled: false 41: retention_period: 30d # global; no per-tenant override exists $ kubectl -n monitoring get cm loki-runtime -o jsonpath='{.data}' {"runtime-config.yaml":"{}\n"} ``` **`auth_enabled` is `false`; `audit-openbao` does not exist live; #953 is OPEN, not merged.** Proposed order: 1. **#953 lands and its Loki Application syncs with `auth_enabled: true`** — prove it (an unlabeled read is refused; `loki_distributor_bytes_received_total{tenant="main"}` moves). 2. **Then** this DaemonSet syncs. If this is applied first, the audit trail lands in the **default tenant** — readable by every Grafana Viewer and outside the 45-day override. 3. Rollback while auth is on: scale the DaemonSet to 0. Do **not** revert the tenant override while data exists (it would age out on the global 30 d). If El Director prefers one change, the sink stanza moves into #953's `vector-longhorn.yaml` and this PR closes unmerged. ## Read-back (runbook step, NOT executed) Make one idempotent call (`bao status` against `sys/health`), then: ``` {job="openbao-audit"} |= "sys/health" {job="openbao-audit", audit_type="request"} | json ``` **Pass:** the row's `time` matches the call, `request_path` is the path requested, and a token-ish field reads `hmac-sha256:<64 hex>` — **HMAC'd, no raw token**. **Stop if:** any field shows a plaintext token, or `audit_type="request"` is empty while OpenBao is serving — check `sum by (reason) (increase(openbao_audit_log_dropped_total[15m]))` before touching the config. ## Test result (offline, fail-capable; vector 0.61.0, the DaemonSet's own binary) ``` $ VECTOR=…/vector ./core/manifests/monitoring/log-metrics/rule-tests/run-openbao-audit-vector-tests.sh vector: vector 0.61.0 (x86_64-unknown-linux-musl 17e017e 2026-10-29 16:03:40.929269968) √ Loaded ["…/vector.yaml"] √ Transforms configuration √ Sinks configuration Validated Running tests test audit request ships de-framed ... passed test audit response ships ... passed test openbao server log line is dropped as unparsed (non-JSON) ... passed test JSON with an unknown type is not an audit record ... passed test a CRI partial audit fragment is counted, not shipped ... passed ``` Three legs keep it honest: the positive controls (request **and** response reach the sink, de-framed, with the `hmac-sha256:` field intact), the negative controls (OpenBao's own `[INFO]` server log and non-audit JSON do not), and the loss case (CRI `P` fragment counted as `partial`, not shipped). `shellcheck` clean; `yamllint -d relaxed` clean (warnings only, same as the existing unit). Loki render check (`helm template loki grafana/loki --version 6.29.0 -f <values>`): ``` data: runtime-config.yaml: | overrides: audit-openbao: deletion_mode: disabled retention_period: 1080h ``` ## Deviations from the brief, stated rather than hidden - **`file` source, not `kubernetes_logs`.** The brief named `kubernetes_logs` scoped to ns/pod. This uses the repo's existing Vector convention (`vector-longhorn.yaml`), which is deliberately **API-free**: hostPath `/var/log/pods`, `automountServiceAccountToken: false`, no RBAC. `kubernetes_logs` needs a cluster-wide pods watch — a new ClusterRole, for a log shipper — to select a pod the kubelet path already names exactly. Same selector, less privilege. Reviewer's call if you disagree. - **A separate Vector unit, not an extra source in `vector-longhorn.yaml`.** #953 rewrites that file end to end; sharing it would make this PR unlandable without #953 and couple the two rollbacks. Fold-in is a follow-up. - **No Grafana datasource here.** Admin-only read is #953's org-2 mechanism; #953's own `check-grafana-datasources.py` (assertion D) **fails** if any other file under `core/` declares an `orgId: 2` datasource, so a second file would break it. The exact second datasource body is in the runbook §6, to be added to #953's single reviewed bootstrap file when both land. Until then, treat `audit-openbao` as readable by anything that can reach `loki:3100` and set a header (same residual as #953's risk 3) — the CNP is a network boundary, not a tenant boundary. - **No secret anywhere.** `tenant_id` is sent as the `X-Scope-OrgID` header and IS the tenant name, not a token. Nothing to put in an ExternalSecret; if a per-tenant gateway is added later, its credential must come from an ExternalSecret **by name**, never inline. ## Not done here (named, not papered over) - **CI wiring** for the offline cases (`vector-access-log-test` is #953's file; this needs a `.forgejo/workflows/ci.yaml` edit that #953 is already making). - **An alert** on `openbao_audit_log_dropped_total{reason="partial"} > 0` — the counter exists so the rule can be written without new instrumentation. - **Fold the two Vector units into one DaemonSet** after both land. - The prod comment at `values/openbao/values.yaml:27-32` ("collected into Loki") is false until this applies; it becomes true with it. Not changed here (other repo). ## Class **Treat as dangerous**: it edits a platform chart's values (`core/charts/platform/loki/values.yaml`) — the file #953 also edits — plus a new workload in `monitoring`. Routine-from-a-security-standpoint (no RBAC, no secrets, no network policy, no `.forgejo/**`), but the retention/tenancy config is the kind of thing two reviewers should see. Two independent reviewer sessions if it is landed. Head commit carries `Lab-Run: IR-20261112-r10` as its **last** trailer. Branch cut from `origin/live` (`2d837e9b`).
IR-20261112: feat(monitoring): ship openbao-0 audit stdout to a 45d Loki tenant (R10 PR2)
All checks were successful
CI Review / receiver-test (pull_request) Successful in 4s
CI Review / helm-validate (pull_request) Successful in 9s
CI Review / runner-manifest-test (pull_request) Successful in 9s
CI Review / alert-receiver-test (pull_request) Successful in 20s
Lint & Validate / shellcheck (pull_request) Successful in 5s
Lint & Validate / yaml-lint (pull_request) Successful in 26s
CI Review / record-sql-test (pull_request) Successful in 1m0s
Lint & Validate / terraform-validate (pull_request) Successful in 1m59s
CI Review / cockpit-test (pull_request) Successful in 2m47s
CI Review / cockpit-e2e (pull_request) Successful in 2m50s
CI Review / dispatcher-test (pull_request) Successful in 3m12s
4ded085478
OpenBao's audit device writes to the container's stdout, and the kubelet
rotates that ring in about 3 minutes, so no durable trail existed: INC-RUN3's
12:07Z record is gone. This is the Loki half of the R10 pair (PR 1 adds the
PVC-backed audit device); it needs no OpenBao restart.

- vector-openbao-audit.yaml: a NEW, self-contained Vector unit (ConfigMap,
  DaemonSet, Service, ServiceMonitor) that reads
  /var/log/pods/openbao_openbao-0_*/openbao/*.log* — namespace=openbao,
  pod=openbao-0, container=openbao, and nothing else in the namespace — through
  the repo's existing API-free convention (hostPath, no RBAC, no SA token).
  The allowlist is a `type` test on the parsed JSON: only audit
  request/response entries ship, de-framed; server-log and non-JSON lines are
  dropped and COUNTED (openbao_audit_log_dropped_total{reason}), CRI partials
  under their own reason.
- Sink: loki, tenant_id audit-openbao (sent as X-Scope-OrgID), gzip, unlimited
  retries with capped backoff, end-to-end acknowledgements. Same pattern as
  Homelab/infra-core#953's audit-forgejo sink.
- loki values: runtimeConfig.overrides.audit-openbao with retention_period
  1080h (45d) and deletion_mode disabled. Rendered against grafana/loki 6.29.0;
  the override reaches the loki-runtime ConfigMap.
- Offline, fail-capable cases for the allowlist, run with vector 0.61.0 (the
  DaemonSet's own version) via run-openbao-audit-vector-tests.sh; 5/5 pass,
  config validates.
- docs/runbooks/openbao-audit-log-shipping.md: the dependency order, the LogQL
  read-back, the filter semantics, and the admin-only read.

DEPENDENCY, stated because it is the one silent failure mode: Loki ignores
X-Scope-OrgID while auth_enabled is false, and it IS false on live today
(kubectl -n monitoring get cm loki -o yaml -> auth_enabled: false; loki-runtime
is {}). #953 is open, not merged. Order: #953's tenancy flip live first, then
this tenant's writer; applied first, the audit trail lands in the default
tenant. No secrets: the tenant header is not a credential and no
ExternalSecret is needed.

BUILD ONLY — not for landing. No apply, no restart.

Lab-Run: IR-20261112-r10
All checks were successful
CI Review / receiver-test (pull_request) Successful in 4s
CI Review / helm-validate (pull_request) Successful in 9s
Required
Details
CI Review / runner-manifest-test (pull_request) Successful in 9s
CI Review / alert-receiver-test (pull_request) Successful in 20s
Lint & Validate / shellcheck (pull_request) Successful in 5s
Required
Details
Lint & Validate / yaml-lint (pull_request) Successful in 26s
Required
Details
CI Review / record-sql-test (pull_request) Successful in 1m0s
Lint & Validate / terraform-validate (pull_request) Successful in 1m59s
Required
Details
CI Review / cockpit-test (pull_request) Successful in 2m47s
CI Review / cockpit-e2e (pull_request) Successful in 2m50s
CI Review / dispatcher-test (pull_request) Successful in 3m12s
This pull request can be merged automatically.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin plane/IR-20261112-r10-openbao-loki:plane/IR-20261112-r10-openbao-loki
git switch plane/IR-20261112-r10-openbao-loki
Sign in to join this conversation.
No reviewers
No labels
operator-review
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Homelab/infra-core!959
No description provided.