Skip to content

docs(platform): document publishing.certificates wildcard options#588

Open
Aleksei Sviridkin (lexfrei) wants to merge 2 commits into
mainfrom
docs/platform-certificates-wildcard
Open

docs(platform): document publishing.certificates wildcard options#588
Aleksei Sviridkin (lexfrei) wants to merge 2 commits into
mainfrom
docs/platform-certificates-wildcard

Conversation

@lexfrei

@lexfrei Aleksei Sviridkin (lexfrei) commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Document two publishing.certificates.* options in the platform-package value table that were missing from the reference docs.

  • publishing.certificates.wildcard (new): opt-in shared wildcard certificate issuance on the default ingress-nginx path. When enabled with a DNS-01 solver it issues one *.<root-host> wildcard for system services instead of a per-host ACME certificate, avoiding Let's Encrypt rate limits at scale. Documented with its default (false), the dns01 / gateway-disabled gating, and the coverage / blast-radius caveat.
  • publishing.certificates.wildcardSecretName (pre-existing, previously undocumented): operator-provided wildcard TLS Secret that platform services serve under instead of minting per-host ACME certificates.

Documents the code change in cozystack/cozystack#2988. Part of cozystack/cozystack#2811.

Summary by CodeRabbit

  • Documentation
    • Added documentation for new Platform Package Publishing certificate configuration options enabling wildcard certificate support with opt-in capability and custom TLS Secret assignment.
    • Documented wildcard certificate behavior, including precedence rules between configuration options, override conditions, and hostname coverage considerations for tenant and service host patterns.

Add reference rows for publishing.certificates.wildcard (opt-in shared
DNS-01 wildcard issuance on the default ingress-nginx path, default false,
with the gating and coverage caveat) and the previously undocumented
publishing.certificates.wildcardSecretName (operator-provided wildcard
Secret) to the platform-package value table.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
@netlify

netlify Bot commented Jun 22, 2026

Copy link
Copy Markdown

Deploy Preview for cozystack ready!

Name Link
🔨 Latest commit 7d02b6d
🔍 Latest deploy log https://app.netlify.com/projects/cozystack/deploys/6a39bd46d9d9a900086fa059
😎 Deploy Preview https://deploy-preview-588--cozystack.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@lexfrei, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 34 minutes and 7 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a77445af-6b35-4a5e-829d-2f65d1598608

📥 Commits

Reviewing files that changed from the base of the PR and between 37fbec8 and 7d02b6d.

📒 Files selected for processing (1)
  • content/en/docs/next/operations/configuration/platform-package.md
📝 Walkthrough

Walkthrough

Adds two new rows to the Platform Package "Publishing" configuration reference table: publishing.certificates.wildcard (a boolean for shared DNS-01 wildcard certificates) and publishing.certificates.wildcardSecretName (an operator-provided TLS Secret). The entries document behavior constraints, precedence rules, and coverage caveats.

Changes

Wildcard Certificate Configuration Docs

Layer / File(s) Summary
Wildcard certificate fields in publishing reference
content/en/docs/next/operations/configuration/platform-package.md
Adds documentation rows for publishing.certificates.wildcard and publishing.certificates.wildcardSecretName, covering ignored/overridden scenarios, wildcard hostname caveats, and precedence between the two options.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~2 minutes

Poem

A bunny hops through config land,
Two new fields added, oh how grand!
Wildcards dance with DNS-01 flair,
TLS Secrets float through the air.
🐇 Caveats noted, precedence clear —
The docs are updated, let us cheer! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main change: documenting the wildcard certificate configuration options for the platform package.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/platform-certificates-wildcard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the platform package configuration documentation by adding descriptions for two new parameters: publishing.certificates.wildcard and publishing.certificates.wildcardSecretName. The review feedback suggests minor wording improvements to clarify that publishing.certificates.wildcard is a boolean setting rather than a "name", and to clarify the relationship between the publishing namespace and publishing.ingressName in the description of publishing.certificates.wildcardSecretName.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

| `publishing.exposure` | `"externalIPs"` | Exposure mode for the ingress-nginx Service. Possible values: `externalIPs`, `loadBalancer`. The default writes `Service.spec.externalIPs` from `publishing.externalIPs`; `loadBalancer` switches to `Service.type: LoadBalancer` and a `CiliumLoadBalancerIPPool` over the same IPs (with `externalTrafficPolicy: Local` to preserve client source IP). `Service.spec.externalIPs` is deprecated upstream in v1.36 (KEP-5707); plan to switch to `loadBalancer` before upgrading past Kubernetes v1.40 when the `AllowServiceExternalIPs` feature gate flips off. The `loadBalancer` mode requires Cilium L2/BGP announcements to reach the IP from outside the cluster (off by default in cozystack), and at least one address in `publishing.externalIPs` (otherwise render fails). |
| `publishing.certificates.solver` | `"http01"` | ACME challenge solver type for default letsencrypt issuer. Possible values: `http01`, `dns01`. |
| `publishing.certificates.issuerName` | `"letsencrypt-prod"` | `ClusterIssuer` name for TLS certificates used in system Helm releases. |
| `publishing.certificates.wildcard` | `false` | Opt-in shared wildcard certificate on the default ingress-nginx path (`gateway.enabled=false`). When `true` with `solver=dns01` and no `publishing.certificates.wildcardSecretName`, the platform issues one `*.<root-host>` + `<root-host>` `Certificate` via the DNS-01 `ClusterIssuer` and serves it as the ingress controller's default SSL certificate, so system services stop minting a per-host ACME certificate each — avoiding Let's Encrypt rate limits at scale, at parity with the Gateway API path. Ignored on `http01` (cannot issue wildcards) and when `gateway.enabled=true` (the `TenantGateway` controller issues the wildcard there). Coverage caveat: a single-label wildcard does not cover a custom service host outside `*.<root-host>` (e.g. a keycloak `ingress.host` on another domain) or a child tenant's nested host (`<service>.<tenant>.<root-host>`); those services fall back to the default certificate, which would not cover them. The chosen name rides the cluster values channel that child tenants inherit, so this is not root-tenant scoped — only enable it when every exposed host is covered, or supply a covering `publishing.certificates.wildcardSecretName` instead. Off by default so a `dns01` cluster is never switched silently on upgrade. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In the description for publishing.certificates.wildcard, the phrase "The chosen name" is confusing because this parameter is a boolean (true/false) rather than a name. It would be clearer to refer to "This setting" or "The chosen option" instead.

Suggested change
| `publishing.certificates.wildcard` | `false` | Opt-in shared wildcard certificate on the default ingress-nginx path (`gateway.enabled=false`). When `true` with `solver=dns01` and no `publishing.certificates.wildcardSecretName`, the platform issues one `*.<root-host>` + `<root-host>` `Certificate` via the DNS-01 `ClusterIssuer` and serves it as the ingress controller's default SSL certificate, so system services stop minting a per-host ACME certificate each — avoiding Let's Encrypt rate limits at scale, at parity with the Gateway API path. Ignored on `http01` (cannot issue wildcards) and when `gateway.enabled=true` (the `TenantGateway` controller issues the wildcard there). Coverage caveat: a single-label wildcard does not cover a custom service host outside `*.<root-host>` (e.g. a keycloak `ingress.host` on another domain) or a child tenant's nested host (`<service>.<tenant>.<root-host>`); those services fall back to the default certificate, which would not cover them. The chosen name rides the cluster values channel that child tenants inherit, so this is not root-tenant scoped — only enable it when every exposed host is covered, or supply a covering `publishing.certificates.wildcardSecretName` instead. Off by default so a `dns01` cluster is never switched silently on upgrade. |
| `publishing.certificates.wildcard` | `false` | Opt-in shared wildcard certificate on the default ingress-nginx path (`gateway.enabled=false`). When `true` with `solver=dns01` and no `publishing.certificates.wildcardSecretName`, the platform issues one `*.<root-host>` + `<root-host>` `Certificate` via the DNS-01 `ClusterIssuer` and serves it as the ingress controller's default SSL certificate, so system services stop minting a per-host ACME certificate each — avoiding Let's Encrypt rate limits at scale, at parity with the Gateway API path. Ignored on `http01` (cannot issue wildcards) and when `gateway.enabled=true` (the `TenantGateway` controller issues the wildcard there). Coverage caveat: a single-label wildcard does not cover a custom service host outside `*.<root-host>` (e.g. a keycloak `ingress.host` on another domain) or a child tenant's nested host (`<service>.<tenant>.<root-host>`); those services fall back to the default certificate, which would not cover them. This setting rides the cluster values channel that child tenants inherit, so this is not root-tenant scoped — only enable it when every exposed host is covered, or supply a covering `publishing.certificates.wildcardSecretName` instead. Off by default so a `dns01` cluster is never switched silently on upgrade. |

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworded to "Enabling it propagates the issued wildcard Secret name through the cluster values channel that child tenants inherit" — it refers to the auto-issued Secret name (set when the flag is on), now stated explicitly rather than "the chosen name". Fixed in 7d02b6d.

| `publishing.certificates.solver` | `"http01"` | ACME challenge solver type for default letsencrypt issuer. Possible values: `http01`, `dns01`. |
| `publishing.certificates.issuerName` | `"letsencrypt-prod"` | `ClusterIssuer` name for TLS certificates used in system Helm releases. |
| `publishing.certificates.wildcard` | `false` | Opt-in shared wildcard certificate on the default ingress-nginx path (`gateway.enabled=false`). When `true` with `solver=dns01` and no `publishing.certificates.wildcardSecretName`, the platform issues one `*.<root-host>` + `<root-host>` `Certificate` via the DNS-01 `ClusterIssuer` and serves it as the ingress controller's default SSL certificate, so system services stop minting a per-host ACME certificate each — avoiding Let's Encrypt rate limits at scale, at parity with the Gateway API path. Ignored on `http01` (cannot issue wildcards) and when `gateway.enabled=true` (the `TenantGateway` controller issues the wildcard there). Coverage caveat: a single-label wildcard does not cover a custom service host outside `*.<root-host>` (e.g. a keycloak `ingress.host` on another domain) or a child tenant's nested host (`<service>.<tenant>.<root-host>`); those services fall back to the default certificate, which would not cover them. The chosen name rides the cluster values channel that child tenants inherit, so this is not root-tenant scoped — only enable it when every exposed host is covered, or supply a covering `publishing.certificates.wildcardSecretName` instead. Off by default so a `dns01` cluster is never switched silently on upgrade. |
| `publishing.certificates.wildcardSecretName` | `""` | Operator-provided wildcard TLS Secret. When set, platform services and the root tenant's ingress/Gateway serve under this pre-existing Secret instead of minting per-host ACME certificates; only the NAME travels through the platform values channel, never the certificate or private key. The Secret must already exist in the publishing namespace (`publishing.ingressName`, `tenant-root` by default), be of type `kubernetes.io/tls`, and cover the served hosts (typically `*.<root-host>` + `<root-host>`). Takes precedence over `publishing.certificates.wildcard` — when set, no ACME issuance happens. Leave empty to keep ACME issuance. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The phrase "publishing namespace (publishing.ingressName, tenant-root by default)" is slightly confusing because publishing.ingressName is defined elsewhere as the ingress controller name, not a namespace. Clarifying that the namespace matches or is derived from this value would improve readability.

Suggested change
| `publishing.certificates.wildcardSecretName` | `""` | Operator-provided wildcard TLS Secret. When set, platform services and the root tenant's ingress/Gateway serve under this pre-existing Secret instead of minting per-host ACME certificates; only the NAME travels through the platform values channel, never the certificate or private key. The Secret must already exist in the publishing namespace (`publishing.ingressName`, `tenant-root` by default), be of type `kubernetes.io/tls`, and cover the served hosts (typically `*.<root-host>` + `<root-host>`). Takes precedence over `publishing.certificates.wildcard` — when set, no ACME issuance happens. Leave empty to keep ACME issuance. |
| `publishing.certificates.wildcardSecretName` | `""` | Operator-provided wildcard TLS Secret. When set, platform services and the root tenant's ingress/Gateway serve under this pre-existing Secret instead of minting per-host ACME certificates; only the NAME travels through the platform values channel, never the certificate or private key. The Secret must already exist in the publishing namespace (which matches `publishing.ingressName`, `tenant-root` by default), be of type `kubernetes.io/tls`, and cover the served hosts (typically `*.<root-host>` + `<root-host>`). Takes precedence over `publishing.certificates.wildcard` — when set, no ACME issuance happens. Leave empty to keep ACME issuance. |

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworded to point at the publishing namespace directly: "tenant-root by default — the namespace running the root ingress controller selected by publishing.ingressName". It no longer reads as if publishing.ingressName were itself the namespace. Fixed in 7d02b6d.

Reword the publishing.certificates.wildcard row so it refers to the
issued wildcard Secret name (not 'the chosen name', which read as if the
boolean had a name), and the wildcardSecretName row so it points at the
publishing namespace directly (tenant-root) instead of citing
publishing.ingressName, which is the ingress controller name.

Signed-off-by: Aleksei Sviridkin <f@lex.la>

@myasnikovdaniil myasnikovdaniil left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accurate and well-scoped. I cross-checked every documented claim against the chart source on the implementation branch (packages/core/platform/values.yaml, templates/apps.yaml, and packages/extra/ingress/templates/{wildcard-certificate,nginx-ingress}.yaml):

  • Key names match exactly — publishing.certificates.wildcard and publishing.certificates.wildcardSecretName, no casing typo.
  • Defaults match: wildcard: false, wildcardSecretName: "".
  • Gating is correct: issuance only on solver=dns01 + gateway.enabled=false + no BYO secret (apps.yaml computes wildcard-issue exactly this way).
  • Precedence is correct: wildcardSecretName wins and suppresses ACME issuance.
  • The coverage footgun is real and correctly stated — the issued Certificate's dnsNames are <root-host> + *.<root-host> (single label), so custom/nested hosts fall back to the default cert. Neither overstated nor understated.
  • The _cluster-channel / child-tenant blast-radius note and the tenant-root publishing-namespace default both check out.

CI is green (Netlify deploy preview, CodeRabbit, DCO).

Optional nit (non-blocking): the wildcard row describes issuance as "when true with solver=dns01 and no wildcardSecretName" — the fourth gating condition (gateway.enabled=false) is conveyed later in the same cell ("Ignored … when gateway.enabled=true"), so the full condition is present, just split across two sentences. Folding it into one would read a touch cleaner. LGTM.

Aleksei Sviridkin (lexfrei) added a commit to cozystack/cozystack that referenced this pull request Jun 23, 2026
…x path (#2988)

## What this PR does

On the default ingress-nginx path (`gateway.enabled=false`) every
published hostname still minted its own per-host ACME certificate via
ingress-shim, even with a DNS-01 solver configured. That hits the Let's
Encrypt rate limit (50 certs per registered domain per week) once a
deployment exceeds ~50 endpoints. The Gateway API path already issues a
single per-apex wildcard in DNS-01 mode; this brings the ingress-nginx
path to parity. It implements the remaining ingress-nginx gap that #2400
was narrowed to in its triage comment (DNS-01 is already multi-provider,
and the Gateway path already issues wildcards).

The new `publishing.certificates.wildcard` toggle is opt-in and OFF by
default. When set to `true` with `solver=dns01` and no operator-provided
wildcard Secret, the platform issues one shared wildcard `Certificate`
for `<root-host>` + `*.<root-host>` and serves the resulting Secret as
the publishing controller's `--default-ssl-certificate`. The system
service Ingresses (dashboard, grafana, keycloak, harbor, …) then stop
requesting a per-host cert.

How it works:

- `core/platform` `apps.yaml` computes the effective wildcard Secret
name and a new `_cluster.wildcard-issue` signal. An operator-provided
`wildcardSecretName` (the BYO path) always wins and never triggers
issuance; HTTP-01 cannot issue wildcards; and when Gateway API is
enabled the TenantGateway controller issues the wildcard instead — so
all three are excluded from auto-issuance.
- `extra/ingress` renders the wildcard `Certificate` only on the
publishing controller (`Release.Namespace == expose-ingress`), because
the Secret must be same-namespace for ingress-nginx to read it. The
Certificate references the DNS-01 `ClusterIssuer` that
`cert-manager-issuers` already renders in `dns01` mode, so no per-tenant
Issuer is minted.

This reuses the existing wildcard-secret consumption path, so no
per-service Ingress template changes were needed — only the issuance
side is new.

Why opt-in (default off): the chosen wildcard Secret name rides the
`_cluster` channel, which every child tenant inherits verbatim, so this
is not root-tenant scoped. A single-label wildcard `*.<root-host>`
covers `<service>.<root-host>` but not a custom service host outside it
(a keycloak `ingress.host`, harbor `host`, or grafana `host` pointed at
another domain) nor a child tenant's nested host
(`<service>.<tenant>.<root-host>`). Enabling issuance makes every such
service drop its per-host ACME cert and fall back to the default
certificate, which does not cover it. Leaving it off by default means a
dns01 cluster is never silently switched on upgrade; an operator who
enables it accepts the same coverage responsibility as the
operator-provided wildcard path, which propagates identically. The
tradeoff is pinned by contract tests (keycloak custom host, bucket
nested child-tenant host). Per-tenant wildcards remain on the Gateway
path and are tracked separately.

Docs: cozystack/website#588

Closes #2400. Part of #2811.

### Screenshots

N/A — no UI changes.

### Release note

```release-note
feat(platform): add opt-in shared wildcard certificate issuance on the default ingress-nginx path via publishing.certificates.wildcard (default false). When enabled with a DNS-01 solver, the platform issues one *.<root-host> wildcard for system services instead of a per-host ACME certificate, avoiding Let's Encrypt rate limits at scale.
```



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added automatic wildcard certificate issuance support for DNS-01 ACME
solver.
* New `certificates.wildcard` configuration option to enable shared
wildcard TLS certificates.
* Ingress rules now properly utilize wildcard certificates when enabled.

* **Tests**
* Added comprehensive test suites validating wildcard certificate
generation and ingress integration across multiple scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants