Skip to content

feat(tls): support mTLS with cert-manager - #297

Merged
GatewayJ merged 1 commit into
rustfs:mainfrom
boylee1111:feat/cert-manager-mtls
Nov 12, 2026
Merged

GatewayJ merged 1 commit into
rustfs:mainfrom
boylee1111:feat/cert-manager-mtls

Conversation

@boylee1111

@boylee1111 boylee1111 commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Type of Change

  • New Feature
  • Bug Fix
  • Documentation
  • Performance Improvement
  • Test/CI
  • Refactor
  • Other:

Related Issues

Summary of Changes

Complete end-to-end mutual TLS (mTLS) support with cert-manager when caTrust.clientCaSecretRef (or legacy certManager.caSecretRef) is configured on a Tenant, covering both client-to-Pod S3/Admin API connections and Pod-to-Pod internode RPC connections on :9000 without introducing redundant CRD fields:

  • TLS Plan (src/types/v1alpha1/tls.rs, src/reconcile/pool_lifecycle.rs):
    • Add TlsConfig::is_mtls_enabled() driven by caTrust.clientCaSecretRef (and legacy certManager.caSecretRef).
    • Force internode_scheme = "https" whenever mTLS is enabled so distributed RUSTFS_VOLUMES peer URLs always use https://.
    • When mTLS is enabled, set RUSTFS_SERVER_MTLS_ENABLE=true, project client_ca.crt from clientCaSecretRef, and project the default certificate's tls.crt and tls.key to client_cert.pem and client_key.pem (alongside rustfs_cert.pem and rustfs_key.pem) for outbound Pod-to-Pod connections.
  • cert-manager Reconciliation & Validation (src/reconcile/tls.rs):
    • Block tls.mode: disabled when clientCaSecretRef is configured (Reason::CertificateInvalid).
    • Default spec.usages on managed default Certificate resources to ["server auth", "client auth"] when mTLS is enabled and certManager.usages is omitted; reject explicit certManager.usages on the default certificate that omit "client auth" when mTLS is enabled.
    • Validate the issued default certificate against the configured clientCaSecretRef trust anchors for webpki::KeyUsage::client_auth() before reporting TLS Ready.
    • Include mtls_enabled in tls_hash to trigger StatefulSet rollout on changes.
  • Workload Probes (src/types/v1alpha1/tenant/workloads.rs, src/types/v1alpha1/tenant/helper.rs):
    • Switch livenessProbe and startupProbe to tcpSocket on port 9000 and readinessProbe to an exec probe (curl -skf --cert <mountPath>/client_cert.pem --key <mountPath>/client_key.pem https://127.0.0.1:9000/health/ready) by default when mTLS is enabled.
    • Support TENANT_MTLS_READINESS_PROBE=tcp (via tenant.spec.env or operator environment) to fall back to tcpSocket on port 9000 for distroless container images without curl (tracked in [Feature] Support distroless-compatible /health/ready probing when server mTLS is enabled rustfs#9256).
    • Normalize Kubernetes API server probe defaults (timeoutSeconds, periodSeconds, successThreshold, failureThreshold, initialDelaySeconds, httpGet.scheme) when comparing container probes in statefulset_needs_update_with_tls_plan_and_cluster_domain.
  • Operator Admin / Provisioning / STS Client (crates/rustfs-admin/src/lib.rs, src/sts/rustfs_client.rs):
    • Add RustfsAdminClient::new_with_base_url_tls_material and load_tenant_tls_client_identity so the operator presents the Tenant's default certificate (status.certificates.tls.serverSecretRef) as its client identity when reconciling buckets/users/policies, pool lifecycle, monitoring, and STS against mTLS-enabled Tenants.
  • Docs, Examples & Tests:
    • Update docs/operator-user-guide.md, docs/operator-user-guide.zh-CN.md, deploy/rustfs-operator/README.md, and CHANGELOG.md.
    • Add examples/cert-manager-mtls-tenant.yaml and update examples/cert-manager-ca-trust-tenant.yaml.
    • Add unit and e2e tests covering volume projections, Certificate usages and client_auth chain verification, mTLS probes (both default exec and TENANT_MTLS_READINESS_PROBE=tcp), probe default normalization, and operator client identity loading.

Checklist

  • I have read and followed the CONTRIBUTING.md guidelines
  • Passed make pre-commit (fmt-check + clippy + test + console-lint + console-fmt-check)
  • Added/updated necessary tests
  • Documentation updated (if needed)
  • CHANGELOG.md updated under [Unreleased] (if user-visible change)
  • CI/CD passed (if applicable)

Impact

  • Breaking change (CRD/API compatibility)
  • Requires doc/config/deployment update
  • Other impact: No CRD schema changes required; caTrust.clientCaSecretRef is used as the single source of truth for enabling mTLS.

Verification

make release-metadata-check fmt-check clippy test e2e-check

Also verified live on a 4-node Kind cluster (kind-rustfs-e2e) with cert-manager (v1.16.2) and rustfs/rustfs:1.0.4, as well as a 3-node distroless appliance cluster (TENANT_MTLS_READINESS_PROBE=tcp):

  • Certificate issued with usages: ["server auth", "client auth"]
  • All pods reached 1/1 Running (Ready) with readQuorum: true and writeQuorum: true over HTTP/2 + mTLS
  • Peer HTTPS requests without a client certificate rejected with tlsv13 alert certificate required; requests with client_cert.pem/client_key.pem succeeded with HTTP/2 200
  • Operator bucket provisioning (spec.buckets) succeeded over mTLS (ProvisioningReady=True)

@boylee1111
boylee1111 force-pushed the feat/cert-manager-mtls branch from 50a1c63 to d182003 Compare November 9, 2026 20:37

@GatewayJ GatewayJ left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please address the six inline findings before merge. On d1820032, make test and make clippy passed locally. make e2e-check failed because the generated mTLS readiness probe uses TCP. I also reproduced HTTP peer URLs on a four-node mTLS Tenant configuration and a spurious StatefulSet update after applying Kubernetes probe defaults.

Comment thread src/types/v1alpha1/tls.rs Outdated

#[serde(default)]
#[x_kube(validation = Rule::new("self == oldSelf").message("enableMtls is immutable"))]
pub enable_mtls: bool,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P1] Require a TLS mode and HTTPS peer URLs when enableMtls is set. A Tenant that sets only tls.enableMtls: true gets the default mode: disabled, so the operator deploys it over HTTP. With mode: certManager, omitting enableInternodeHttps still produced http:// peer URLs in RUSTFS_VOLUMES for a four-node Tenant. Those peers cannot connect to the TLS listener. Please reject the disabled-mode combination at admission and ensure distributed mTLS Tenants use HTTPS for every peer URL.

@boylee1111 boylee1111 Nov 10, 2026 •

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.

  1. Removed redundant enableMtls: caTrust.clientCaSecretRef (was not realize that this will enable mTLS early) is now the single source of truth for enabling mTLS (is_mtls_enabled()), so the CRD schema in deploy/rustfs-operator/crds/tenant-crd.yaml remains unchanged from main.
  2. Reconciler guard: Added a reconciler guard in reconcile_tls that blocks mode: disabled with Reason::CertificateInvalid when clientCaSecretRef is configured (reconcile_tls_blocks_disabled_mode_when_mtls_is_enabled).
  3. Forced HTTPS peer URLs for mTLS: is_internode_https_enabled() and TlsPlan::rollout_certificates now force internode_scheme = "https" whenever mTLS is enabled, even if enableInternodeHttps is omitted or false. Added unit test coverage in mtls_plan_uses_https_peer_urls_even_when_enable_internode_https_is_false.

Comment thread src/types/v1alpha1/tenant/workloads.rs Outdated
http_probe("/health", tls_plan.probe_scheme)
}),
readiness_probe: Some(if tls_plan.mtls_enabled {
tcp_probe()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P1] Check /health/ready with the client certificate. A TCP connection succeeds as soon as port 9000 accepts connections, including while RustFS is still waiting for read quorum. Kubernetes can then route requests to the Pod and advance a StatefulSet rollout too early. make e2e-check fails here with mTLS readiness probe should use exec, while the new workload unit test expects TCP. Please probe /health/ready using the mounted client certificate and key, and have both tests check that behavior.

@boylee1111 boylee1111 Nov 10, 2026 •

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.

  1. Added mtls_readiness_probe in src/types/v1alpha1/tls.rs and wired readiness_probe in src/types/v1alpha1/tenant/workloads.rs to probe https://127.0.0.1:9000/health/ready via exec (curl --cert <mount_path>/client_cert.pem --key <mount_path>/client_key.pem ...) by default, while keeping liveness_probe and startup_probe on tcpSocket (:9000).
  2. Updated both the workload unit test (mtls_tls_plan_switches_probes_and_triggers_statefulset_update in src/types/v1alpha1/tenant/workloads.rs) and the e2e test (mtls_tls_plan_projects_client_identity_and_uses_tcp_and_exec_probes in e2e/tests/cert_manager_tls.rs) to verify that readiness_probe uses exec and passes --cert and --key against https://127.0.0.1:9000/health/ready.
  3. For distroless container images that do not ship curl, we also added an opt-in TENANT_MTLS_READINESS_PROBE=tcp environment variable override (with documentation in the user guide noting the caveat that tcpSocket succeeds as soon as port 9000 accepts connections and does not wait for /health/ready read quorum) and opened [Feature] Support distroless-compatible /health/ready probing when server mTLS is enabled rustfs#9256 to track native distroless /health/ready probing under mTLS.

Comment thread src/reconcile/tls.rs Outdated
if config.is_mtls_enabled()
&& ca_trust.client_ca_secret_ref.is_none()
&& explicit_ca_bytes.is_none()
&& !trust_system_ca

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P1] Require a CA bundle for inbound client verification. With enableMtls: true, caTrust.source: SystemCa, and no clientCaSecretRef, this condition lets reconciliation continue without mounting either client_ca.crt or ca.crt. RustFS 1.0.4 uses system roots for outbound connections and fails to initialize the server verifier when both files are absent. Please report CaBundleMissing before creating the workload in this configuration. RustFS verifier

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.

Addressed by removing the separate enableMtls flag and keeping caTrust.clientCaSecretRef as the single source of truth for enabling mTLS. Because mTLS is enabled if and only if clientCaSecretRef is configured, reconcile_cert_manager_tls always validates and projects client_ca.crt before creating the workload (and reports CaBundleMissing / CaBundleInvalid if the referenced client CA Secret is missing or invalid).

Comment thread src/types/v1alpha1/tenant.rs Outdated
crates(serde_json = "k8s_openapi::serde_json")
)]
#[serde(rename_all = "camelCase")]
#[x_kube(validation = Rule::new("(has(self.tls) && has(self.tls.enableMtls) && self.tls.enableMtls) == (has(oldSelf.tls) && has(oldSelf.tls.enableMtls) && oldSelf.tls.enableMtls)").message("tls.enableMtls is immutable"))]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P1] Protect transitions of the effective mTLS mode. With enableMtls left at false, adding caTrust.clientCaSecretRef makes is_mtls_enabled() return true, and removing it switches mTLS off again. Both updates pass this rule. During a rolling update, new Pods can require client certificates that the old Pods do not present. Please validate transitions using the effective mode, including the legacy CA configuration, while permitting CA rotation on a Tenant that already enforces mTLS.

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.

Addressed by reverting the spec.tls.enableMtls field and the enableMtls CEL rule on TenantSpec, keeping caTrust.clientCaSecretRef as the sole mechanism for enabling mTLS without introducing a second conflicting flag in the CRD schema.

Comment thread src/types/v1alpha1/tenant/workloads.rs Outdated
}

// Check container probes (mTLS switches HTTP probes to TCP/exec probes)
if serde_json::to_value(&existing_container.liveness_probe)?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Account for Kubernetes defaults when comparing probes. I reproduced this on a Tenant with TLS disabled. Starting with the generated StatefulSet, setting only the readiness probe's timeoutSeconds=1, periodSeconds=10, successThreshold=1, and failureThreshold=3 changed statefulset_needs_update from false to true. The API server supplies those values, so each reconciliation can submit the same StatefulSet again. Please normalize the defaults before comparing and cover an API-defaulted probe in the regression test.

@boylee1111 boylee1111 Nov 10, 2026 •

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.

  • Added normalize_probe_for_comparison in src/types/v1alpha1/tenant/workloads.rs to normalize Kubernetes API server defaults (timeoutSeconds: 1, periodSeconds: 10, successThreshold: 1, failureThreshold: 3, initialDelaySeconds: 0, and httpGet.scheme: "HTTP") before comparing liveness_probe, readiness_probe, and startup_probe.
  • Added regression coverage in api_server_defaulted_probes_do_not_trigger_statefulset_update.

Comment thread src/types/v1alpha1/tls.rs
items.push(key_to_path("tls.key", &server_key_path(domain.as_deref())));
}
if mtls_enabled && certificate.domains.iter().any(Option::is_none) {
items.push(key_to_path("tls.crt", RUSTFS_CLIENT_CERT_FILE));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Verify that the default certificate can authenticate as a client. This projects the server certificate as the peer identity, and the operator also uses it for admin and STS calls. An explicit usages: ["server auth"] or a clientCaSecretRef that does not trust the certificate's issuer still lets TLS reach Ready. Peers and the operator then fail the client-auth handshake. Please check the issued certificate's client-auth eligibility and chain against the effective client CA before reporting TLS Ready, and cover both failure cases.

@boylee1111 boylee1111 Nov 10, 2026 •

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.

  1. In validate_certificate_entries, if explicit certManager.usages are configured on the default certificate while mTLS is enabled and do not include "client auth", reconciliation blocks with Reason::CertificateInvalid.
  2. Added validate_default_certificate_client_auth in src/reconcile/tls.rs, which parses the issued default certificate with webpki::EndEntityCert and calls verify_for_usage(..., webpki::KeyUsage::client_auth(), ...) against the trust anchors parsed from the configured clientCaSecretRef bundle before reporting TLS Ready.
  3. Added unit test coverage for both failure modes in certificate_entries_rejects_default_cert_missing_client_auth_usage_when_mtls_enabled and validate_default_certificate_client_auth_checks_eku_and_issuer_chain.

@GatewayJ
GatewayJ requested a balanced review from Copilot November 10, 2026 02:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@boylee1111
boylee1111 force-pushed the feat/cert-manager-mtls branch 2 times, most recently from c561411 to d182003 Compare November 10, 2026 03:03
boylee1111 added a commit to boylee1111/operator that referenced this pull request Nov 10, 2026
boylee1111 added a commit to boylee1111/operator that referenced this pull request Nov 10, 2026
boylee1111 added a commit to boylee1111/operator that referenced this pull request Nov 10, 2026
boylee1111 added a commit to boylee1111/operator that referenced this pull request Nov 10, 2026
boylee1111 added a commit to boylee1111/operator that referenced this pull request Nov 10, 2026
@boylee1111
boylee1111 force-pushed the feat/cert-manager-mtls branch from d182003 to 33fd84a Compare November 10, 2026 05:03
@boylee1111
boylee1111 marked this pull request as draft November 10, 2026 05:23
@boylee1111

boylee1111 commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor Author

@GatewayJ I will need to do more tests before publishing this to review further. Thanks

@boylee1111
boylee1111 requested a review from GatewayJ November 10, 2026 07:25
@boylee1111

Copy link
Copy Markdown
Contributor Author

@GatewayJ I've addressed comments and ready to review, PTAL. Thank you

Copy link
Copy Markdown
Member

@boylee1111, do you consider the current changes ready to merge, or do you still have changes or tests planned? You mentioned that the PR is ready for another review. If so, could you mark it as Ready for review on GitHub?

@boylee1111
boylee1111 marked this pull request as ready for review November 11, 2026 02:35
@boylee1111

Copy link
Copy Markdown
Contributor Author

@GatewayJ yes, marked as ready to review

@GatewayJ
GatewayJ added this pull request to the merge queue Nov 12, 2026
Merged via the queue into rustfs:main with commit 5e58593 Nov 12, 2026
3 checks passed
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.

3 participants