Skip to content

feat(platform): operator-provided wildcard certificate for platform and tenant ingress - #2819

Merged
Aleksei Sviridkin (lexfrei) merged 4 commits into
mainfrom
feat/operator-wildcard-tls
Jun 21, 2026
Merged

feat(platform): operator-provided wildcard certificate for platform and tenant ingress#2819
Aleksei Sviridkin (lexfrei) merged 4 commits into
mainfrom
feat/operator-wildcard-tls

Conversation

@lexfrei

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

Copy link
Copy Markdown
Contributor

What this PR does

Adds an operator-facing way to serve platform services and the root tenant's ingress under a pre-existing wildcard TLS certificate instead of minting per-host ACME certificates. Implements the "native references" design decided on #2812.

A single value, publishing.certificates.wildcardSecretName, names a TLS Secret the operator has already created in the publishing namespace (tenant-root by default). Only the name travels through the cozystack-values channel — the certificate and private key are never broadcast, unlike the public kube-root-ca that already rides it.

When set:

  • ingress-nginx path (gateway.enabled=false): the publishing tenant's ingress controller serves the Secret as its --default-ssl-certificate, and the platform service Ingresses drop their cert-manager cluster-issuer annotation and tls block. No per-host ACME.
  • Gateway API path (gateway.enabled=true): a new TenantGateway cert mode, existingSecret, references the Secret in the wildcard/apex listener certificateRefs and mints no Issuer or Certificate. Switching into this mode garbage-collects any ACME machinery left from http01/dns01.

Scope is the root tenant — everything is same-namespace, so no RBAC or NetworkPolicy changes. Extending wildcard mode to child tenants needs the Secret replicated into each tenant namespace and is left as a follow-up.

Closes #2812. Part of #2811.

Screenshots

N/A — no UI changes.

Release note

feat(platform): operators can serve platform services and the root tenant's ingress under a pre-existing wildcard TLS certificate via `publishing.certificates.wildcardSecretName`, instead of minting per-host ACME certificates

Summary by CodeRabbit

  • New Features

    • Added an "existingSecret" TLS certificate mode and a wildcardSecretRef option for TenantGateway and platform values to use a pre-existing wildcard TLS Secret.
  • Platform / Templates

    • Introduced operator "wildcard secret" mode across Gateway and many Helm charts (Ingress, apps) to skip per-host ACME provisioning and optionally wire a default SSL cert.
  • Tests

    • Added extensive chart and controller tests covering existingSecret behavior and cert-mode transitions.
  • Documentation

    • Updated gateway docs describing the new wildcard secret mode and requirements.
  • Chores

    • Added Helm unit test makefile targets to several packages.

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fbab1d0e-22bb-447c-9c6b-dae36867216b

📥 Commits

Reviewing files that changed from the base of the PR and between b1f5d95 and 9e3ae74.

📒 Files selected for processing (18)
  • packages/apps/harbor/templates/ingress.yaml
  • packages/apps/harbor/tests/ingress_wildcard_test.yaml
  • packages/extra/bootbox/templates/matchbox/ingress.yaml
  • packages/extra/bootbox/tests/ingress_wildcard_test.yaml
  • packages/extra/seaweedfs/templates/seaweedfs.yaml
  • packages/extra/seaweedfs/tests/ingress_wildcard_test.yaml
  • packages/system/bucket/templates/ingress.yaml
  • packages/system/bucket/tests/ingress_wildcard_test.yaml
  • packages/system/dashboard/templates/ingress.yaml
  • packages/system/dashboard/tests/ingress_wildcard_test.yaml
  • packages/system/keycloak/templates/ingress.yaml
  • packages/system/keycloak/tests/ingress_wildcard_test.yaml
  • packages/system/linstor-gui/templates/ingress.yaml
  • packages/system/linstor-gui/tests/ingress_wildcard_test.yaml
  • packages/system/monitoring/templates/alerta/alerta.yaml
  • packages/system/monitoring/templates/grafana/grafana.yaml
  • packages/system/monitoring/tests/alerta_ingress_wildcard_test.yaml
  • packages/system/monitoring/tests/grafana_ingress_wildcard_test.yaml
🚧 Files skipped from review as they are similar to previous changes (9)
  • packages/apps/harbor/tests/ingress_wildcard_test.yaml
  • packages/system/monitoring/templates/alerta/alerta.yaml
  • packages/system/dashboard/templates/ingress.yaml
  • packages/system/linstor-gui/tests/ingress_wildcard_test.yaml
  • packages/apps/harbor/templates/ingress.yaml
  • packages/system/monitoring/tests/grafana_ingress_wildcard_test.yaml
  • packages/extra/seaweedfs/templates/seaweedfs.yaml
  • packages/system/bucket/tests/ingress_wildcard_test.yaml
  • packages/system/monitoring/tests/alerta_ingress_wildcard_test.yaml

📝 Walkthrough

Walkthrough

Adds an operator-provided wildcard TLS path: new TenantGateway cert mode existingSecret with wildcardSecretRef, controller changes to skip ACME and clean stale issuers/certificates, platform Helm wiring to distribute wildcard-secret-name, and conditional ingress/template updates plus tests across apps and system charts.

Changes

Operator-provided wildcard TLS certificate mode

Layer / File(s) Summary
API contract for existingSecret mode
api/gateway/v1alpha1/tenantgateway_types.go, api/gateway/v1alpha1/zz_generated.deepcopy.go, packages/system/cozystack-controller/definitions/gateway.cozystack.io_tenantgateways.yaml
New CertModeExistingSecret constant and WildcardSecretRef added; CRD enum updated; deepcopy updated.
TenantGateway controller reconciliation & rendering
internal/controller/tenantgateway/reconciler.go, internal/controller/tenantgateway/renderers.go
Adds wildcardListenerCertName validation, skips hostname claims for existingSecret, cleans controller-owned Issuers/Certificates, updates reconcileIssuer to no-op for existingSecret, adjusts renderGateway to use wildcardListenerCertName, and makes buildSolver return an error for existingSecret.
TenantGateway controller tests
internal/controller/tenantgateway/reconciler_test.go
Seven new tests covering existingSecret rendering, no-ACME guarantees, missing-secret validation, HTTP-01/DNS-01→existingSecret cleanup, regression protections, and inheritance rendering.
Platform-to-gateway configuration distribution
packages/core/platform/values.yaml, packages/core/platform/templates/apps.yaml, packages/core/platform/tests/apps_wildcard_secret_test.yaml
Adds certificates.wildcardSecretName value and distributes it via cozystack-values; tests validate default and configured behavior.
TenantGateway Helm template wiring
packages/extra/gateway/templates/tenantgateway.yaml, packages/extra/gateway/tests/tenantgateway_test.yaml, packages/extra/gateway/README.md
When _cluster.wildcard-secret-name is set, template emits certMode: existingSecret and wildcardSecretRef.name, and omits ACME/dns/issuer fields; tests and docs updated.
App ingress wildcard mode
packages/apps/harbor/..., packages/extra/bootbox/..., packages/extra/seaweedfs/...
Ingress templates now conditionally omit cert-manager annotations and per-host TLS secretName when wildcard-secret-name is set; tests added/updated.
System ingress wildcard mode
packages/system/*/templates/ingress.yaml, packages/system/*/tests/*
System charts (dashboard, keycloak, linstor-gui, bucket, monitoring/grafana/alerta) conditionally skip per-host ACME annotations and omit TLS secretName while keeping TLS host entries; tests cover both modes.
Ingress-nginx default SSL certificate
packages/extra/ingress/templates/nginx-ingress.yaml, packages/extra/ingress/tests/default_ssl_certificate_test.yaml
When wildcard secret is set and controller runs in publishing namespace, sets controller.extraArgs.default-ssl-certificate to <namespace>/<secret>; tests added.
Build/test targets
packages/*/Makefile
Adds test targets that run helm unittest . across multiple package Makefiles.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #2811 — Epic: Operator-provided / shared wildcard certificate: PR implements the operator-supplied wildcard path described in the epic and distributes the wildcard-secret-name via platform values.

Suggested labels

size/XL, kind/api-change

Suggested reviewers

  • IvanHunters
  • androndo
  • sircthulhu
  • myasnikovdaniil
  • kvaps

Poem

🐰 A wildcard blooms in the operator's hand,
No more per-host certs scattered across the land,
ExistingSecret whispers to the Gateway API,
Helm templates bow and tests wink an aye,
Hop — the platform serves TLS from one secret bright.

🚥 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 PR title accurately summarizes the main change: introducing an operator-provided wildcard certificate mechanism for platform and tenant ingress.
Linked Issues check ✅ Passed The PR fully satisfies issue #2812 requirements: single operator-set value (publishing.certificates.wildcardSecretName) enables wildcard Secret consumption, avoids per-host ACME issuance, and supports both ingress-nginx and Gateway API paths with proper resource cleanup.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the wildcard certificate feature: TenantGateway API extensions, reconciler logic, Helm ingress templates, CRD updates, and comprehensive tests. No unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 90.91% which is sufficient. The required threshold is 80.00%.

✏️ 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 feat/operator-wildcard-tls

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 and usage tips.

@github-actions github-actions Bot added size/XXL This PR changes 1000+ lines, ignoring generated files area/platform Issues or PRs related to platform infrastructure (bundle, flux, talos, installer) kind/feature Categorizes issue or PR as related to a new feature labels Jun 3, 2026
@lexfrei Aleksei Sviridkin (lexfrei) added the area/networking Issues or PRs related to networking (ingress, gateway, vpn, metallb, kube-ovn) label Jun 3, 2026
@lexfrei
Aleksei Sviridkin (lexfrei) marked this pull request as ready for review June 8, 2026 18:34
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request implements a native way for operators to provide their own wildcard TLS certificate for platform services and the root tenant's ingress. By referencing a pre-existing Secret, the system avoids the overhead and complexity of per-host ACME certificate issuance. The changes span the API definitions, controller reconciliation logic, and various Helm chart templates to ensure consistent behavior across the platform, including proper cleanup of legacy ACME resources during mode transitions.

Highlights

  • Wildcard Certificate Support: Introduced an operator-facing configuration publishing.certificates.wildcardSecretName to use a pre-existing wildcard TLS Secret for platform services and the root tenant's ingress, eliminating the need for per-host ACME certificates.
  • Gateway API Integration: Added CertModeExistingSecret to TenantGateway, allowing the gateway to reference an operator-supplied Secret directly without minting ACME Issuers or Certificates.
  • Cleanup Logic: Implemented automated cleanup of stale ACME machinery (Issuers and Certificates) when switching modes to ensure no orphaned resources remain.
  • Ingress Controller Configuration: Updated the publishing ingress controller to serve the provided wildcard Secret as the default SSL certificate when the feature is enabled.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Ignored Files
  • Ignored by pattern: **/zz_generated.*.go (1)
    • api/gateway/v1alpha1/zz_generated.deepcopy.go
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@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 introduces an operator-supplied wildcard certificate mode (existingSecret) across Cozystack, allowing platform services and tenant gateways to use a pre-existing TLS Secret instead of issuing certificates via ACME. The changes span the TenantGateway CRD, controller reconciler, and various application ingress templates (such as Harbor, SeaweedFS, Keycloak, and Grafana) along with corresponding unit tests. The review feedback correctly identifies a critical issue across all modified ingress templates: completely omitting the tls block in ingress-nginx disables HTTPS/TLS for the host entirely. To successfully fall back to the controller's default SSL certificate, the tls block must remain defined with the hosts list while only the secretName is omitted. The associated unit tests should also be updated to assert this correct structure.

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.

Comment on lines +46 to +51
{{- if not $wildcardSecret }}
tls:
- hosts:
- dashboard.{{ $host }}
secretName: dashboard-web-tls
{{- end }}

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.

high

In ingress-nginx, completely omitting the tls block disables HTTPS/TLS for the host entirely. To use the operator-provided default SSL certificate, the tls block must be present with the hosts list, but the secretName should be omitted. This allows ingress-nginx to fall back to the default SSL certificate configured on the controller.

  tls:
    - hosts:
        - dashboard.{{ $host }}
      {{- if not $wildcardSecret }}
      secretName: dashboard-web-tls
      {{- end }}

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.

The premise isn't quite right: without a tls section ingress-nginx still serves the host on 443 using the default SSL certificate. What is actually lost is the forced HTTP-to-HTTPS redirect — the controller only issues the 308 redirect when TLS is enabled for the Ingress. That redirect is worth keeping, so the tls section now stays with the host list and only the per-host secretName is omitted in wildcard mode. Fixed in 9e3ae74.

Comment on lines +99 to +104
{{- if not $wildcardSecret }}
tls:
- hosts:
- "{{ printf "grafana.%s" (.Values.host | default $host) }}"
secretName: grafana-ingress-tls
{{- end }}

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.

high

In ingress-nginx, completely omitting the tls block disables HTTPS/TLS for the host entirely. To use the operator-provided default SSL certificate, the tls block must be present with the hosts list, but the secretName should be omitted. This allows ingress-nginx to fall back to the default SSL certificate configured on the controller.

      tls:
      - hosts:
        - "{{ printf "grafana.%s" (.Values.host | default $host) }}"
        {{- if not $wildcardSecret }}
        secretName: grafana-ingress-tls
        {{- end }}

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.

The premise isn't quite right: without a tls section ingress-nginx still serves the host on 443 using the default SSL certificate. What is actually lost is the forced HTTP-to-HTTPS redirect — the controller only issues the 308 redirect when TLS is enabled for the Ingress. That redirect is worth keeping, so the tls section now stays with the host list and only the per-host secretName is omitted in wildcard mode. Fixed in 9e3ae74.

Comment on lines +27 to +32
{{- if not $wildcardSecret }}
tls:
- hosts:
- "{{ printf "bootbox.%s" (.Values.host | default $host) }}"
secretName: bootbox-tls
{{- end }}

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.

high

In ingress-nginx, completely omitting the tls block disables HTTPS/TLS for the host entirely. To use the operator-provided default SSL certificate, the tls block must be present with the hosts list, but the secretName should be omitted. This allows ingress-nginx to fall back to the default SSL certificate configured on the controller.

  tls:
    - hosts:
        - "{{ printf "bootbox.%s" (.Values.host | default $host) }}"
      {{- if not $wildcardSecret }}
      secretName: bootbox-tls
      {{- end }}

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.

The premise isn't quite right: without a tls section ingress-nginx still serves the host on 443 using the default SSL certificate. What is actually lost is the forced HTTP-to-HTTPS redirect — the controller only issues the 308 redirect when TLS is enabled for the Ingress. That redirect is worth keeping, so the tls section now stays with the host list and only the per-host secretName is omitted in wildcard mode. Fixed in 9e3ae74.

Comment on lines +196 to +201
{{- if not $wildcardSecret }}
tls:
- hosts:
- "{{ printf "alerta.%s" (.Values.host | default $host) }}"
secretName: alerta-tls
{{- end }}

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.

high

In ingress-nginx, completely omitting the tls block disables HTTPS/TLS for the host entirely. To use the operator-provided default SSL certificate, the tls block must be present with the hosts list, but the secretName should be omitted. This allows ingress-nginx to fall back to the default SSL certificate configured on the controller.

  tls:
    - hosts:
        - "{{ printf "alerta.%s" (.Values.host | default $host) }}"
      {{- if not $wildcardSecret }}
      secretName: alerta-tls
      {{- end }}

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.

The premise isn't quite right: without a tls section ingress-nginx still serves the host on 443 using the default SSL certificate. What is actually lost is the forced HTTP-to-HTTPS redirect — the controller only issues the 308 redirect when TLS is enabled for the Ingress. That redirect is worth keeping, so the tls section now stays with the host list and only the per-host secretName is omitted in wildcard mode. Fixed in 9e3ae74.

Comment on lines +54 to +59
{{- if not $wildcardSecret }}
tls:
- hosts:
- linstor-gui.{{ $host }}
secretName: linstor-gui-ingress-tls
{{- end }}

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.

high

In ingress-nginx, completely omitting the tls block disables HTTPS/TLS for the host entirely. To use the operator-provided default SSL certificate, the tls block must be present with the hosts list, but the secretName should be omitted. This allows ingress-nginx to fall back to the default SSL certificate configured on the controller.

  tls:
    - hosts:
        - linstor-gui.{{ $host }}
      {{- if not $wildcardSecret }}
      secretName: linstor-gui-ingress-tls
      {{- end }}

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.

The premise isn't quite right: without a tls section ingress-nginx still serves the host on 443 using the default SSL certificate. What is actually lost is the forced HTTP-to-HTTPS redirect — the controller only issues the 308 redirect when TLS is enabled for the Ingress. That redirect is worth keeping, so the tls section now stays with the host list and only the per-host secretName is omitted in wildcard mode. Fixed in 9e3ae74.

Comment on lines +29 to +30
- notExists:
path: spec.tls

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.

low

Since the tls block should be kept to allow ingress-nginx to fall back to the default SSL certificate, update the test assertion to verify that spec.tls exists but spec.tls[0].secretName is absent.

      - exists:
          path: spec.tls
      - notExists:
          path: spec.tls[0].secretName

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.

Updated in 9e3ae74 — the assertion now verifies that the tls section exists and secretName is absent.

Comment on lines +34 to +35
- notExists:
path: spec.tls

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.

low

Since the tls block should be kept to allow ingress-nginx to fall back to the default SSL certificate, update the test assertion to verify that spec.tls exists but spec.tls[0].secretName is absent.

      - exists:
          path: spec.tls
      - notExists:
          path: spec.tls[0].secretName

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.

Updated in 9e3ae74 — the assertion now verifies that the tls section exists and secretName is absent.

Comment on lines +32 to +33
- notExists:
path: spec.tls

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.

low

Since the tls block should be kept to allow ingress-nginx to fall back to the default SSL certificate, update the test assertion to verify that spec.tls exists but spec.tls[0].secretName is absent.

      - exists:
          path: spec.tls
      - notExists:
          path: spec.tls[0].secretName

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.

Updated in 9e3ae74 — the assertion now verifies that the tls section exists and secretName is absent.

Comment on lines +30 to +31
- notExists:
path: spec.tls

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.

low

Since the tls block should be kept to allow ingress-nginx to fall back to the default SSL certificate, update the test assertion to verify that spec.tls exists but spec.tls[0].secretName is absent.

      - exists:
          path: spec.tls
      - notExists:
          path: spec.tls[0].secretName

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.

Updated in 9e3ae74 — the assertion now verifies that the tls section exists and secretName is absent.

Comment on lines +33 to +34
- notExists:
path: spec.ingress.spec.tls

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.

low

Since the tls block should be kept to allow ingress-nginx to fall back to the default SSL certificate, update the test assertion to verify that spec.ingress.spec.tls exists but spec.ingress.spec.tls[0].secretName is absent.

      - exists:
          path: spec.ingress.spec.tls
      - notExists:
          path: spec.ingress.spec.tls[0].secretName

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.

Updated in 9e3ae74 — the assertion now verifies that the tls section exists and secretName is absent.

Operators who already hold a wildcard TLS certificate (purchased, or
issued by a corporate CA) had no way to make a tenant Gateway serve
under it: the only cert modes were http01 (per-listener ACME) and
dns01 (controller-minted wildcard via ACME).

Add a third mode, existingSecret, that references a pre-existing TLS
Secret in the TenantGateway's own namespace via spec.wildcardSecretRef.
In this mode the controller mints no ACME Issuer and no Certificate;
the wildcard, apex, and per-child-apex HTTPS listeners reference the
supplied Secret directly. A missing wildcardSecretRef fails fast with
Ready=False rather than producing a Gateway pointing at a nonexistent
Secret. Switching into existingSecret from http01/dns01 reclaims the
now-unused Issuer and Certificates so no ACME machinery is left behind.

The non-cert listeners (http->https redirect, TLS passthrough) are
unaffected and continue to render.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Add publishing.certificates.wildcardSecretName to the platform chart
and propagate it through the cozystack-values channel as
_cluster.wildcard-secret-name. Only the Secret NAME travels — the
cert/key material is never broadcast, unlike the public kube-root-ca
that already rides this channel.

When the key is set, the gateway chart switches the rendered
TenantGateway to certMode=existingSecret with a wildcardSecretRef
pointing at the operator-supplied Secret, and skips the ACME
solver/provider/issuer validations that no longer apply. Wildcard mode
takes precedence over solver=dns01. TLS-passthrough services and
attached namespaces continue to render unchanged.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
When publishing.certificates.wildcardSecretName is set, the root
tenant's ingress-nginx controller now serves the operator-supplied
wildcard Secret as its --default-ssl-certificate, and the platform
service Ingresses stop requesting their own per-host ACME certificates.

The flag is set only on the publishing controller (Release.Namespace ==
expose-ingress) because ingress-nginx loads the Secret from its own
namespace; child-tenant controllers cannot reach a Secret in another
namespace, so they keep the existing ACME path.

Each service Ingress (dashboard, keycloak, grafana, alerta, bucket,
linstor-gui, harbor, seaweedfs S3, bootbox) drops its cert-manager
cluster-issuer annotation, the http01 challenge-class annotation, and
its tls block in wildcard mode; nginx then falls back to the default
certificate for those hosts. Other annotations and routing are
unchanged. Each file keeps its existing render guard intact, and the
annotations block collapses cleanly where the cert annotations were its
only content.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Dropping the whole tls block in wildcard mode disabled the forced
HTTP-to-HTTPS redirect: ingress-nginx only issues the 308 redirect when
TLS is enabled for the Ingress, while a host without a tls section is
still served on 443 with the default certificate but stays reachable
over plain HTTP. Keep the tls section with the host list and omit only
the per-host secretName, so ingress-nginx falls back to the operator
wildcard configured as the default SSL certificate and the redirect
stays enforced, matching the non-wildcard behaviour.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
api/gateway/v1alpha1/tenantgateway_types.go (1)

170-177: ⚡ Quick win

Enforce existingSecret requiredness in the CRD schema (not only in comments).

WildcardSecretRef is documented as required for certMode=existingSecret, but admission currently allows missing/empty values. Add a kubebuilder XValidation so invalid specs are rejected before reconcile.

Suggested marker
 type TenantGatewaySpec struct {
@@
 	// +optional
 	WildcardSecretRef *corev1.LocalObjectReference `json:"wildcardSecretRef,omitempty"`
+
+	// +kubebuilder:validation:XValidation:rule="self.certMode != 'existingSecret' || (has(self.wildcardSecretRef) && self.wildcardSecretRef.name != '')",message="spec.wildcardSecretRef.name is required when certMode=existingSecret"

As per coding guidelines, api/**: “Define Kubernetes CRDs in the api/ directory following kubebuilder patterns and controller-runtime conventions.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/gateway/v1alpha1/tenantgateway_types.go` around lines 170 - 177, The CRD
currently documents that WildcardSecretRef on the TenantGateway is required when
certMode == "existingSecret" but does not enforce it; add a kubebuilder
XValidation marker on the TenantGateway spec type to reject empty/missing
WildcardSecretRef when CertMode equals "existingSecret" (use an XValidation rule
referencing .spec.certMode and .spec.wildcardSecretRef such that when
.spec.certMode == "existingSecret" then .spec.wildcardSecretRef must be present
and not null/empty). Update the struct tags/markers near the WildcardSecretRef /
TenantGateway spec declaration so the generated CRD enforces this constraint at
admission time.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/controller/tenantgateway/reconciler.go`:
- Around line 66-73: The helper wildcardListenerCertName only validates the
wildcardSecretRef.name string but does not verify the referenced Secret exists
or is of type kubernetes.io/tls, so reconcile may render listeners referencing a
missing/invalid Secret; update wildcardListenerCertName (or add a new
validateWildcardSecret helper) to fetch the Secret via the controller client
(using the TenantGateway namespace and tgw.Spec.WildcardSecretRef.Name), return
an error if the Secret is not found or its Type != corev1.SecretTypeTLS, and
propagate that error back to the reconciler so reconciliation fails fast when
CertModeExistingSecret is configured.

---

Nitpick comments:
In `@api/gateway/v1alpha1/tenantgateway_types.go`:
- Around line 170-177: The CRD currently documents that WildcardSecretRef on the
TenantGateway is required when certMode == "existingSecret" but does not enforce
it; add a kubebuilder XValidation marker on the TenantGateway spec type to
reject empty/missing WildcardSecretRef when CertMode equals "existingSecret"
(use an XValidation rule referencing .spec.certMode and .spec.wildcardSecretRef
such that when .spec.certMode == "existingSecret" then .spec.wildcardSecretRef
must be present and not null/empty). Update the struct tags/markers near the
WildcardSecretRef / TenantGateway spec declaration so the generated CRD enforces
this constraint at admission time.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4d986e0e-81dd-458d-8b3a-5a68e722d38a

📥 Commits

Reviewing files that changed from the base of the PR and between c6f6fee and b1f5d95.

📒 Files selected for processing (38)
  • api/gateway/v1alpha1/tenantgateway_types.go
  • api/gateway/v1alpha1/zz_generated.deepcopy.go
  • internal/controller/tenantgateway/reconciler.go
  • internal/controller/tenantgateway/reconciler_test.go
  • internal/controller/tenantgateway/renderers.go
  • packages/apps/harbor/Makefile
  • packages/apps/harbor/templates/ingress.yaml
  • packages/apps/harbor/tests/ingress_wildcard_test.yaml
  • packages/core/platform/templates/apps.yaml
  • packages/core/platform/tests/apps_wildcard_secret_test.yaml
  • packages/core/platform/values.yaml
  • packages/extra/bootbox/Makefile
  • packages/extra/bootbox/templates/matchbox/ingress.yaml
  • packages/extra/bootbox/tests/ingress_wildcard_test.yaml
  • packages/extra/gateway/README.md
  • packages/extra/gateway/templates/tenantgateway.yaml
  • packages/extra/gateway/tests/tenantgateway_test.yaml
  • packages/extra/ingress/templates/nginx-ingress.yaml
  • packages/extra/ingress/tests/default_ssl_certificate_test.yaml
  • packages/extra/seaweedfs/Makefile
  • packages/extra/seaweedfs/templates/seaweedfs.yaml
  • packages/extra/seaweedfs/tests/ingress_wildcard_test.yaml
  • packages/system/bucket/Makefile
  • packages/system/bucket/templates/ingress.yaml
  • packages/system/bucket/tests/ingress_wildcard_test.yaml
  • packages/system/cozystack-controller/definitions/gateway.cozystack.io_tenantgateways.yaml
  • packages/system/dashboard/templates/ingress.yaml
  • packages/system/dashboard/tests/ingress_wildcard_test.yaml
  • packages/system/keycloak/Makefile
  • packages/system/keycloak/templates/ingress.yaml
  • packages/system/keycloak/tests/ingress_wildcard_test.yaml
  • packages/system/linstor-gui/templates/ingress.yaml
  • packages/system/linstor-gui/tests/ingress_wildcard_test.yaml
  • packages/system/monitoring/Makefile
  • packages/system/monitoring/templates/alerta/alerta.yaml
  • packages/system/monitoring/templates/grafana/grafana.yaml
  • packages/system/monitoring/tests/alerta_ingress_wildcard_test.yaml
  • packages/system/monitoring/tests/grafana_ingress_wildcard_test.yaml

Comment on lines +66 to +73
func wildcardListenerCertName(tgw *gatewayv1alpha1.TenantGateway) (string, error) {
if tgw.Spec.CertMode == gatewayv1alpha1.CertModeExistingSecret {
if tgw.Spec.WildcardSecretRef == nil || tgw.Spec.WildcardSecretRef.Name == "" {
return "", fmt.Errorf("certMode=existingSecret requires spec.wildcardSecretRef.name to be set")
}
return tgw.Spec.WildcardSecretRef.Name, nil
}
return gatewayCertificateName(tgw), nil

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

existingSecret mode does not fail fast on missing/invalid Secret objects.

The helper only checks spec.wildcardSecretRef.name; reconcile can still render Gateway listeners that point to a Secret that does not exist or is not kubernetes.io/tls.

Minimal hardening sketch
 func (r *Reconciler) runReconcileSteps(ctx context.Context, tgw *gatewayv1alpha1.TenantGateway) error {
+	if err := r.validateExistingSecret(ctx, tgw); err != nil {
+		return fmt.Errorf("validate existingSecret wildcard secret: %w", err)
+	}
 	claims, err := r.collectHostnameClaims(ctx, tgw)
@@
 }
+
+func (r *Reconciler) validateExistingSecret(ctx context.Context, tgw *gatewayv1alpha1.TenantGateway) error {
+	if tgw.Spec.CertMode != gatewayv1alpha1.CertModeExistingSecret {
+		return nil
+	}
+	if tgw.Spec.WildcardSecretRef == nil || tgw.Spec.WildcardSecretRef.Name == "" {
+		return fmt.Errorf("certMode=existingSecret requires spec.wildcardSecretRef.name to be set")
+	}
+	sec := &corev1.Secret{}
+	if err := r.Get(ctx, types.NamespacedName{
+		Namespace: tgw.Namespace,
+		Name:      tgw.Spec.WildcardSecretRef.Name,
+	}, sec); err != nil {
+		return err
+	}
+	if sec.Type != corev1.SecretTypeTLS {
+		return fmt.Errorf("secret %s/%s must be type %q", tgw.Namespace, sec.Name, corev1.SecretTypeTLS)
+	}
+	return nil
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/tenantgateway/reconciler.go` around lines 66 - 73, The
helper wildcardListenerCertName only validates the wildcardSecretRef.name string
but does not verify the referenced Secret exists or is of type
kubernetes.io/tls, so reconcile may render listeners referencing a
missing/invalid Secret; update wildcardListenerCertName (or add a new
validateWildcardSecret helper) to fetch the Secret via the controller client
(using the TenantGateway namespace and tgw.Spec.WildcardSecretRef.Name), return
an error if the Secret is not found or its Type != corev1.SecretTypeTLS, and
propagate that error back to the reconciler so reconciliation fails fast when
CertModeExistingSecret is configured.

@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.

LGTM. Reviewed the full change end-to-end — the design is clean and the implementation is careful:

  • Only the Secret name travels the cozystack-values channel, never the cert/key material.
  • All references are same-namespace (no ReferenceGrant / RBAC / NetworkPolicy changes), matching the documented root-tenant scope.
  • The dns01 → existingSecret switch garbage-collects both the owned Issuer (reconcileIssuer) and the owned wildcard Certificate (reconcileWildcardCertificate's != dns01 branch), both ownership-guarded — so cert-manager can't overwrite the operator-supplied wildcard Secret.
  • Completeness: every ingress template referencing cert-manager is gated on wildcard-secret-name; nothing is left minting per-host ACME in wildcard mode.
  • Keeping the tls: block without secretName (commit fix(ingress): keep tls section…) is the right call — nginx then enforces the HTTPS redirect and falls back to --default-ssl-certificate.

Verified locally: go test ./internal/controller/tenantgateway/... passes, and helm unittest is green across every affected package (ingress, gateway, dashboard, keycloak, bucket, harbor, linstor-gui, monitoring, seaweedfs, bootbox, platform). Nice touch adding the test: targets so those packages actually execute in the unit-test job.

Two minor, non-blocking nits inline.

// existingSecret mode references an operator-supplied Secret and
// mints no ACME Issuer. Delete any owned Issuer left from a
// previous http01/dns01 phase so the mode switch doesn't leak
// ACME machinery. Same ownership-guarded cleanup contract as

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.

This references "reconcileWildcardCertificate's HTTP-01 branch", but that branch is the if tgw.Spec.CertMode != CertModeDNS01 check — which now also runs for existingSecret. Its inline comment ("HTTP-01 mode: …") and especially its log line ("deleted stale wildcard Certificate after switch to HTTP-01") will mis-state the mode on a dns01 → existingSecret switch. Consider generalizing both to e.g. "non-DNS-01 mode" / "after leaving DNS-01". Purely cosmetic — the behavior is correct.

// and cover the tenant apex (and *.apex). Cross-namespace refs are
// intentionally unsupported to avoid requiring a ReferenceGrant.
// +optional
WildcardSecretRef *corev1.LocalObjectReference `json:"wildcardSecretRef,omitempty"`

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.

existingSecret requiring this field is enforced at reconcile time in wildcardListenerCertName (good fail-fast). Optional enhancement: a CRD +kubebuilder:validation:XValidation rule on the spec (e.g. self.certMode != 'existingSecret' || has(self.wildcardSecretRef)) would reject the misconfiguration at kubectl apply time instead of surfacing it as a degraded reconcile. Non-blocking.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/networking Issues or PRs related to networking (ingress, gateway, vpn, metallb, kube-ovn) area/platform Issues or PRs related to platform infrastructure (bundle, flux, talos, installer) kind/feature Categorizes issue or PR as related to a new feature size/XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Operator-provided / shared wildcard certificate for platform and tenant ingress

3 participants