From cfd23a1bf1d49b1cfa43bc8c8f19c2b51dc5dfed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Fernando=20Carri=C3=B3n?= Date: Wed, 22 Jul 2026 12:51:12 +0200 Subject: [PATCH 1/3] DND-1415: add native in-pod HTTPS/TLS support (GH #14) Add config.tls to terminate HTTPS in the pod via S3Proxy's secure-endpoint, using a PKCS12 (or JKS) keystore + password supplied by an existing Secret (recommended; also the cert-manager path) or inline. When enabled, the proxy serves HTTPS only on service.targetPort (port named https); tcpSocket probes are unchanged (they do not perform a TLS handshake). The keystore password is kept out of the ConfigMap: it is merged from the Secret by the config-merge initContainer (inline via secret-common.properties, existingSecret via a mounted file). - values/configmap/secret/deployment/service: config.tls block, secure-endpoint + keystore-path, keystore mount at /tls, password via secret-merge, https port. - CI: test-values/tls.yaml + tls-existing-secret.yaml (lint-render) and a kind functional TLS leg that builds a self-signed keystore Secret and round-trips over HTTPS. smoke-test.sh gains an optional SCHEME arg. - README / override example: TLS / HTTPS section. Chart 0.2.0 -> 0.3.0. Verified: helm lint, kubeconform -strict, helm-polish, shellcheck, actionlint, non-TLS render unchanged, and a kind end-to-end HTTPS round-trip. --- .github/workflows/functional-test.yaml | 28 ++++- .github/workflows/lint-render.yaml | 2 + README.md | 116 +++++++++++++++++++- charts/s3proxy/Chart.yaml | 2 +- charts/s3proxy/README.md.gotmpl | 70 ++++++++++++ charts/s3proxy/override-values.example.yaml | 16 +++ charts/s3proxy/templates/configmap.yaml | 8 ++ charts/s3proxy/templates/deployment.yaml | 51 ++++++++- charts/s3proxy/templates/secret.yaml | 17 ++- charts/s3proxy/templates/service.yaml | 3 +- charts/s3proxy/values.yaml | 18 +++ ci/functional/smoke-test.sh | 16 ++- ci/functional/values/tls.yaml | 30 +++++ test-values/tls-existing-secret.yaml | 18 +++ test-values/tls.yaml | 17 +++ 15 files changed, 398 insertions(+), 14 deletions(-) create mode 100644 ci/functional/values/tls.yaml create mode 100644 test-values/tls-existing-secret.yaml create mode 100644 test-values/tls.yaml diff --git a/.github/workflows/functional-test.yaml b/.github/workflows/functional-test.yaml index d6b8020..61b4865 100644 --- a/.github/workflows/functional-test.yaml +++ b/.github/workflows/functional-test.yaml @@ -19,6 +19,10 @@ name: Functional Test (kind) # physically lands on its intended backend (MinIO for # s3, Azurite for azureblob, filesystem by elimination) # and that no bucket leaks into another backend. +# tls -> native in-pod HTTPS. A self-signed PKCS12 keystore is +# generated and stored in a Secret; s3proxy is installed +# with config.tls (existingSecret path) over filesystem, +# then the smoke test round-trips over HTTPS. # gcs/b2/openstack-swift/rackspace are render-only (covered by lint-render + helm-diff): # gcs has no jclouds.endpoint override in the chart, and the others have no # lightweight in-cluster emulator. @@ -59,6 +63,10 @@ jobs: values: ci/functional/values/multi-backend.yaml mock: ci/functional/mocks/minio.yaml ci/functional/mocks/azurite.yaml verify: routing + - backend: tls + values: ci/functional/values/tls.yaml + tls: true + scheme: https env: NAMESPACE: s3proxy-test RELEASE: s3proxy @@ -82,6 +90,21 @@ jobs: - name: Create namespace run: kubectl create namespace "${NAMESPACE}" + - name: Generate self-signed TLS keystore Secret (tls leg) + if: matrix.tls + # Build a self-signed PKCS12 keystore + password and store both in the + # Secret the chart references via config.tls.*.existingSecret. Exercises the + # production existingSecret path (also how cert-manager delivers a keystore). + run: | + openssl req -x509 -newkey rsa:2048 -nodes -days 1 \ + -keyout /tmp/tls.key -out /tmp/tls.crt -subj "/CN=s3proxy" + openssl pkcs12 -export \ + -inkey /tmp/tls.key -in /tmp/tls.crt \ + -out /tmp/keystore.p12 -passout pass:changeit + kubectl -n "${NAMESPACE}" create secret generic s3proxy-tls \ + --from-file=keystore.p12=/tmp/keystore.p12 \ + --from-literal=keystore-password=changeit + - name: Deploy mock backends (${{ matrix.backend }}) if: matrix.mock # matrix.mock is a space-separated list of manifests (multi-backend @@ -101,7 +124,8 @@ jobs: --wait --timeout 5m - name: Verify (${{ matrix.verify || 'round-trip' }}) - # Single-backend legs: one S3 round-trip through s3proxy. + # Single-backend legs: one S3 round-trip through s3proxy (over matrix.scheme, + # default http; the tls leg sets https). # multi-backend leg (verify=routing): assert each bucket physically lands # on its intended backend (needs aws + az; both are on ubuntu-latest). run: | @@ -111,7 +135,7 @@ jobs: ci/functional/assert-routing.sh "${RELEASE}" "${NAMESPACE}" else chmod +x ci/functional/smoke-test.sh - ci/functional/smoke-test.sh "${RELEASE}" "${NAMESPACE}" 9000 test-access-key test-secret-key + ci/functional/smoke-test.sh "${RELEASE}" "${NAMESPACE}" 9000 test-access-key test-secret-key smoke "${{ matrix.scheme || 'http' }}" fi - name: Diagnostics on failure diff --git a/.github/workflows/lint-render.yaml b/.github/workflows/lint-render.yaml index 07d2c75..76f9879 100644 --- a/.github/workflows/lint-render.yaml +++ b/.github/workflows/lint-render.yaml @@ -36,6 +36,8 @@ jobs: - test-values/multi-backend.yaml - test-values/ingress.yaml - test-values/autoscaling.yaml + - test-values/tls.yaml + - test-values/tls-existing-secret.yaml steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 diff --git a/README.md b/README.md index 6d94a16..25accca 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # s3proxy -![Version: 0.2.0](https://img.shields.io/badge/Version-0.2.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 3.3.0](https://img.shields.io/badge/AppVersion-3.3.0-informational?style=flat-square) +![Version: 0.3.0](https://img.shields.io/badge/Version-0.3.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 3.3.0](https://img.shields.io/badge/AppVersion-3.3.0-informational?style=flat-square) A Helm chart for deploying S3Proxy - Access other storage backends via the S3 API @@ -583,6 +583,48 @@ The following section lists the configurable parameters of the s3proxy chart and bool false + + config.tls.enabled + Enable native in-pod HTTPS (S3Proxy secure-endpoint). When enabled, S3Proxy serves HTTPS only on service.targetPort (the plaintext endpoint is not bound), so TLS is terminated in the pod rather than at the ingress. Requires a PKCS12 (or JKS) keystore and its password. tcpSocket health probes are unaffected (they do not perform a TLS handshake). + bool + false + + + config.tls.keystore.existingSecret + Name of an existing Secret holding the keystore file (binary PKCS12/JKS). Takes precedence over value. Use this for a customer-provided keystore or a cert-manager-issued one (Certificate.spec.keystores.pkcs12). + string + "" + + + config.tls.keystore.secretKey + Key within keystore.existingSecret (or, when value is used, within the chart's own Secret) that holds the keystore file + string + "keystore.p12" + + + config.tls.keystore.value + Inline base64-encoded keystore, stored in the chart's own Secret and mounted as a file. Used only when keystore.existingSecret is empty. Convenient for testing; prefer existingSecret in production. + string + "" + + + config.tls.keystorePassword.existingSecret + Name of an existing Secret holding the keystore password. Takes precedence over value. + string + "" + + + config.tls.keystorePassword.secretKey + Key within keystorePassword.existingSecret that holds the keystore password + string + "keystore-password" + + + config.tls.keystorePassword.value + Inline keystore password, stored in the chart's own Secret and merged into the backend properties by the secret-merge initContainer (kept out of the ConfigMap). Used only when keystorePassword.existingSecret is empty. + string + "" + config.virtualHost Virtual Host configuration @@ -958,6 +1000,76 @@ aws --endpoint-url http://localhost:8080 s3 cp test.txt s3://test-bucket/ aws --endpoint-url http://localhost:8080 s3 ls s3://test-bucket/ ``` +## TLS / HTTPS (native, in-pod) + +By default S3Proxy binds plain HTTP and TLS is expected to be terminated at the +ingress. Set `config.tls.enabled=true` to have **S3Proxy itself terminate HTTPS in +the pod** (S3Proxy's `secure-endpoint`). When enabled: + +- S3Proxy serves **HTTPS only** on `service.targetPort` (the plaintext endpoint is + not bound). The container/Service port is named `https`. +- The health probes are `tcpSocket`, which only check the TCP accept (no TLS + handshake), so they keep working unchanged against the TLS port. +- A **PKCS12** keystore is expected (Jetty's default keystore type; JKS also works + if supplied). S3Proxy exposes only the keystore path and password, so the chart + does not set a keystore type. +- The keystore **password is never written to the ConfigMap**; it is merged into + the backend properties from the Secret by the config-merge initContainer. + +### Option A: existing Secret (recommended; also the cert-manager path) + +Reference a Secret that already holds the keystore file and the password. This is +also how a cert-manager `Certificate` with `spec.keystores.pkcs12` delivers a +keystore (point `keystore.existingSecret` at that Secret, and +`keystorePassword.existingSecret` at the password Secret it references): + +```yaml +config: + tls: + enabled: true + keystore: + existingSecret: my-tls # holds the PKCS12 archive + secretKey: keystore.p12 + keystorePassword: + existingSecret: my-tls # holds the password (may be the same Secret) + secretKey: keystore-password +``` + +### Option B: inline keystore + password + +Provide the base64-encoded keystore and the password inline; both are stored in the +chart's own Secret. Convenient for testing; prefer Option A in production. + +```yaml +config: + tls: + enabled: true + keystore: + value: "" + keystorePassword: + value: "changeit" +``` + +Create a PKCS12 keystore, for example: + +```bash +# From an existing cert + key: +openssl pkcs12 -export -inkey tls.key -in tls.crt \ + -out keystore.p12 -passout pass:changeit +# Inline value: +base64 -w0 keystore.p12 +# Or as an existing Secret (Option A): +kubectl create secret generic my-tls \ + --from-file=keystore.p12=keystore.p12 \ + --from-literal=keystore-password=changeit +``` + +Testing over HTTPS (the CI keystore above is self-signed, so skip verification): + +```bash +aws --endpoint-url https://localhost:9000 --no-verify-ssl s3 ls +``` + ## CORS Configuration To enable CORS support: @@ -1074,5 +1186,3 @@ This will remove all resources created by the chart. If using persistence, the P This Helm chart is provided as-is. S3Proxy itself is licensed under the Apache License 2.0. ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/charts/s3proxy/Chart.yaml b/charts/s3proxy/Chart.yaml index 51e12b7..19b4e17 100644 --- a/charts/s3proxy/Chart.yaml +++ b/charts/s3proxy/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.2.0 +version: 0.3.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/charts/s3proxy/README.md.gotmpl b/charts/s3proxy/README.md.gotmpl index 1f34330..25fb8e2 100644 --- a/charts/s3proxy/README.md.gotmpl +++ b/charts/s3proxy/README.md.gotmpl @@ -224,6 +224,76 @@ aws --endpoint-url http://localhost:8080 s3 cp test.txt s3://test-bucket/ aws --endpoint-url http://localhost:8080 s3 ls s3://test-bucket/ ``` +## TLS / HTTPS (native, in-pod) + +By default S3Proxy binds plain HTTP and TLS is expected to be terminated at the +ingress. Set `config.tls.enabled=true` to have **S3Proxy itself terminate HTTPS in +the pod** (S3Proxy's `secure-endpoint`). When enabled: + +- S3Proxy serves **HTTPS only** on `service.targetPort` (the plaintext endpoint is + not bound). The container/Service port is named `https`. +- The health probes are `tcpSocket`, which only check the TCP accept (no TLS + handshake), so they keep working unchanged against the TLS port. +- A **PKCS12** keystore is expected (Jetty's default keystore type; JKS also works + if supplied). S3Proxy exposes only the keystore path and password, so the chart + does not set a keystore type. +- The keystore **password is never written to the ConfigMap**; it is merged into + the backend properties from the Secret by the config-merge initContainer. + +### Option A: existing Secret (recommended; also the cert-manager path) + +Reference a Secret that already holds the keystore file and the password. This is +also how a cert-manager `Certificate` with `spec.keystores.pkcs12` delivers a +keystore (point `keystore.existingSecret` at that Secret, and +`keystorePassword.existingSecret` at the password Secret it references): + +```yaml +config: + tls: + enabled: true + keystore: + existingSecret: my-tls # holds the PKCS12 archive + secretKey: keystore.p12 + keystorePassword: + existingSecret: my-tls # holds the password (may be the same Secret) + secretKey: keystore-password +``` + +### Option B: inline keystore + password + +Provide the base64-encoded keystore and the password inline; both are stored in the +chart's own Secret. Convenient for testing; prefer Option A in production. + +```yaml +config: + tls: + enabled: true + keystore: + value: "" + keystorePassword: + value: "changeit" +``` + +Create a PKCS12 keystore, for example: + +```bash +# From an existing cert + key: +openssl pkcs12 -export -inkey tls.key -in tls.crt \ + -out keystore.p12 -passout pass:changeit +# Inline value: +base64 -w0 keystore.p12 +# Or as an existing Secret (Option A): +kubectl create secret generic my-tls \ + --from-file=keystore.p12=keystore.p12 \ + --from-literal=keystore-password=changeit +``` + +Testing over HTTPS (the CI keystore above is self-signed, so skip verification): + +```bash +aws --endpoint-url https://localhost:9000 --no-verify-ssl s3 ls +``` + ## CORS Configuration To enable CORS support: diff --git a/charts/s3proxy/override-values.example.yaml b/charts/s3proxy/override-values.example.yaml index 4878908..af9fa60 100644 --- a/charts/s3proxy/override-values.example.yaml +++ b/charts/s3proxy/override-values.example.yaml @@ -27,6 +27,22 @@ config: identity: "admin" secret: "changeme123" + # Native in-pod HTTPS (S3Proxy secure-endpoint). When enabled, S3Proxy serves + # HTTPS only on service.targetPort; TLS is terminated in the pod rather than at + # the ingress. Requires a PKCS12 (or JKS) keystore + password. Disabled by default. + # tls: + # enabled: true + # keystore: + # # Option A: reference an existing Secret (recommended; cert-manager path) + # existingSecret: "my-tls" + # secretKey: "keystore.p12" + # # Option B: inline base64-encoded keystore (testing) + # # value: "" + # keystorePassword: + # existingSecret: "my-tls" + # secretKey: "keystore-password" + # # value: "changeit" + # Enable CORS for web applications cors: enabled: true diff --git a/charts/s3proxy/templates/configmap.yaml b/charts/s3proxy/templates/configmap.yaml index 1dddf89..e0d8803 100644 --- a/charts/s3proxy/templates/configmap.yaml +++ b/charts/s3proxy/templates/configmap.yaml @@ -1,6 +1,14 @@ {{- define "s3proxy.main.config" -}} # S3Proxy configuration + {{- if .Values.config.tls.enabled }} +# Native TLS: S3Proxy terminates HTTPS in the pod (secure-endpoint only; no +# plaintext endpoint is bound). The keystore password is not emitted here; it is +# merged in from the Secret by the merge-configs initContainer. +s3proxy.secure-endpoint=https://0.0.0.0:{{ .Values.service.targetPort }} +s3proxy.keystore-path=/tls/keystore.p12 + {{- else }} s3proxy.endpoint=http://0.0.0.0:{{ .Values.service.targetPort }} + {{- end }} s3proxy.authorization={{ .Values.config.auth.type }} {{- if .Values.config.virtualHost }} s3proxy.virtual-host={{ .Values.config.virtualHost }} diff --git a/charts/s3proxy/templates/deployment.yaml b/charts/s3proxy/templates/deployment.yaml index 4ce926d..26dae0b 100644 --- a/charts/s3proxy/templates/deployment.yaml +++ b/charts/s3proxy/templates/deployment.yaml @@ -1,3 +1,4 @@ +{{ $portName := ternary "https" "http" .Values.config.tls.enabled -}} apiVersion: apps/v1 kind: Deployment metadata: @@ -81,6 +82,15 @@ spec: echo "" >> "$output_file" # Add newline separator cat "$backend_secret" >> "$output_file" fi + + # TLS keystore password from an existing Secret (kept out of the + # ConfigMap). Mounted only when config.tls.keystorePassword.existingSecret + # is set; an inline password arrives via secret-common.properties instead. + if [ -f "/tls-password/keystore-password" ] + then + echo "" >> "$output_file" # Add newline separator + echo "s3proxy.keystore-password=$(cat /tls-password/keystore-password)" >> "$output_file" + fi fi done @@ -94,6 +104,11 @@ spec: readOnly: true - name: merged-config mountPath: /merged-config +{{- if and .Values.config.tls.enabled .Values.config.tls.keystorePassword.existingSecret }} + - name: tls-keystore-password + mountPath: /tls-password + readOnly: true +{{- end }} containers: - name: {{ .Chart.Name }} securityContext: @@ -143,12 +158,14 @@ spec: {{- toYaml . | nindent 12 }} {{- end }} ports: - - name: http + - name: {{ $portName }} containerPort: {{ .Values.service.targetPort }} protocol: TCP + # tcpSocket probes only check the TCP accept, so they work unchanged + # against a TLS-only port (no handshake performed). livenessProbe: tcpSocket: - port: http + port: {{ $portName }} initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 @@ -156,7 +173,7 @@ spec: failureThreshold: 3 readinessProbe: tcpSocket: - port: http + port: {{ $portName }} initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 3 @@ -168,6 +185,11 @@ spec: - name: merged-config mountPath: /merged-config readOnly: true +{{- if .Values.config.tls.enabled }} + - name: tls-keystore + mountPath: /tls + readOnly: true +{{- end }} {{- if .Values.config.backends.filesystem.enabled }} {{- if .Values.persistence.enabled }} - name: data @@ -192,6 +214,29 @@ spec: secretName: {{ include "comet-common.names.fullname" . }} - name: merged-config emptyDir: {} +{{- if .Values.config.tls.enabled }} + - name: tls-keystore + secret: + {{- if .Values.config.tls.keystore.existingSecret }} + secretName: {{ .Values.config.tls.keystore.existingSecret }} + items: + - key: {{ .Values.config.tls.keystore.secretKey }} + path: keystore.p12 + {{- else }} + secretName: {{ include "comet-common.names.fullname" . }} + items: + - key: {{ .Values.config.tls.keystore.secretKey }} + path: keystore.p12 + {{- end }} + {{- if .Values.config.tls.keystorePassword.existingSecret }} + - name: tls-keystore-password + secret: + secretName: {{ .Values.config.tls.keystorePassword.existingSecret }} + items: + - key: {{ .Values.config.tls.keystorePassword.secretKey }} + path: keystore-password + {{- end }} +{{- end }} {{- if .Values.config.backends.filesystem.enabled }} {{- if .Values.persistence.enabled }} - name: data diff --git a/charts/s3proxy/templates/secret.yaml b/charts/s3proxy/templates/secret.yaml index 38b1b0f..87a6295 100644 --- a/charts/s3proxy/templates/secret.yaml +++ b/charts/s3proxy/templates/secret.yaml @@ -5,6 +5,13 @@ metadata: labels: {{- include "comet-common.labels.base" . | nindent 4 }} type: Opaque +{{- if and .Values.config.tls.enabled .Values.config.tls.keystore.value (not .Values.config.tls.keystore.existingSecret) }} +data: + # Inline TLS keystore (binary PKCS12/JKS), stored base64 exactly as provided in + # config.tls.keystore.value. Mounted read-only at /tls/keystore.p12; never merged + # into the text properties. Used only when keystore.existingSecret is empty. + {{ .Values.config.tls.keystore.secretKey }}: {{ .Values.config.tls.keystore.value }} +{{- end }} stringData: # Sensitive properties merged into the backend properties files by the # merge-configs initContainer: @@ -13,11 +20,19 @@ stringData: # each backend gets its own jclouds.credential and never another backend's. # Local backends (filesystem/transient) need no secret file; their placeholder # jclouds.identity/credential come from the ConfigMap. -{{- if and .Values.config.auth.identity .Values.config.auth.secret }} +{{- $hasClientAuth := and .Values.config.auth.identity .Values.config.auth.secret }} +{{- $inlineKeystorePassword := and .Values.config.tls.enabled .Values.config.tls.keystorePassword.value (not .Values.config.tls.keystorePassword.existingSecret) }} +{{- if or $hasClientAuth $inlineKeystorePassword }} secret-common.properties: | + {{- if $hasClientAuth }} # S3Proxy client authentication credentials (shared by all backends) s3proxy.identity={{ .Values.config.auth.identity }} s3proxy.credential={{ .Values.config.auth.secret }} + {{- end }} + {{- if $inlineKeystorePassword }} + # TLS keystore password (shared by all backends; kept out of the ConfigMap) + s3proxy.keystore-password={{ .Values.config.tls.keystorePassword.value }} + {{- end }} {{- end }} {{- if and .Values.config.backends.s3.enabled .Values.config.backends.s3.secretAccessKey.value }} secret-s3.properties: | diff --git a/charts/s3proxy/templates/service.yaml b/charts/s3proxy/templates/service.yaml index e4868c2..64aad01 100644 --- a/charts/s3proxy/templates/service.yaml +++ b/charts/s3proxy/templates/service.yaml @@ -1,3 +1,4 @@ +{{ $portName := ternary "https" "http" .Values.config.tls.enabled -}} apiVersion: v1 kind: Service metadata: @@ -14,6 +15,6 @@ spec: - port: {{ .Values.service.port }} targetPort: {{ .Values.service.targetPort }} protocol: TCP - name: http + name: {{ $portName }} selector: {{- include "s3proxy.selectorLabels" . | nindent 4 }} diff --git a/charts/s3proxy/values.yaml b/charts/s3proxy/values.yaml index f53a9a4..a22fc65 100644 --- a/charts/s3proxy/values.yaml +++ b/charts/s3proxy/values.yaml @@ -124,6 +124,24 @@ config: # -- S3 Secret Access Key for client authentication secret: "" + tls: + # -- Enable native in-pod HTTPS (S3Proxy `secure-endpoint`). When enabled, S3Proxy serves HTTPS only on `service.targetPort` (the plaintext endpoint is not bound), so TLS is terminated in the pod rather than at the ingress. Requires a PKCS12 (or JKS) keystore and its password. `tcpSocket` health probes are unaffected (they do not perform a TLS handshake). + enabled: false + keystore: + # -- Name of an existing Secret holding the keystore file (binary PKCS12/JKS). Takes precedence over `value`. Use this for a customer-provided keystore or a cert-manager-issued one (`Certificate.spec.keystores.pkcs12`). + existingSecret: "" + # -- Key within `keystore.existingSecret` (or, when `value` is used, within the chart's own Secret) that holds the keystore file + secretKey: "keystore.p12" + # -- (string) Inline base64-encoded keystore, stored in the chart's own Secret and mounted as a file. Used only when `keystore.existingSecret` is empty. Convenient for testing; prefer `existingSecret` in production. + value: "" + keystorePassword: + # -- Name of an existing Secret holding the keystore password. Takes precedence over `value`. + existingSecret: "" + # -- Key within `keystorePassword.existingSecret` that holds the keystore password + secretKey: "keystore-password" + # -- Inline keystore password, stored in the chart's own Secret and merged into the backend properties by the secret-merge initContainer (kept out of the ConfigMap). Used only when `keystorePassword.existingSecret` is empty. + value: "" + # -- Virtual Host configuration virtualHost: "" diff --git a/ci/functional/smoke-test.sh b/ci/functional/smoke-test.sh index d7aa44d..c922415 100755 --- a/ci/functional/smoke-test.sh +++ b/ci/functional/smoke-test.sh @@ -4,12 +4,16 @@ # delete. Uses the AWS CLI over a kubectl port-forward, authenticating with the # s3proxy client credentials (config.auth.identity / config.auth.secret). # -# Usage: smoke-test.sh RELEASE NAMESPACE PORT IDENTITY SECRET [BUCKET_PREFIX] +# Usage: smoke-test.sh RELEASE NAMESPACE PORT IDENTITY SECRET [BUCKET_PREFIX] [SCHEME] # # BUCKET_PREFIX (default "smoke") names the test bucket; multi-backend scenarios # call this script once per backend with a distinct prefix so each round-trip # targets a bucket routed to that backend. # +# SCHEME (default "http") is the endpoint scheme. Set to "https" for the native-TLS +# leg: the round-trip then goes over HTTPS and TLS cert verification is disabled +# (--no-verify-ssl / curl -k) because the CI keystore is self-signed. +# # Exit non-zero on any failure (a broken backend wiring makes the round-trip fail). set -euo pipefail @@ -19,10 +23,11 @@ PORT="${3:-9000}" IDENTITY="${4:?s3proxy client identity required}" SECRET="${5:?s3proxy client secret required}" BUCKET_PREFIX="${6:-smoke}" +SCHEME="${7:-http}" LOCAL_PORT=9900 BUCKET="${BUCKET_PREFIX}-$(date +%s)" -ENDPOINT="http://127.0.0.1:${LOCAL_PORT}" +ENDPOINT="${SCHEME}://127.0.0.1:${LOCAL_PORT}" SRC="$(mktemp)" DL="$(mktemp)" @@ -59,7 +64,7 @@ echo "==> Waiting for the port-forward to accept connections" ready=false for _ in $(seq 1 30) do - if curl -s -o /dev/null "$ENDPOINT" + if curl -sk -o /dev/null "$ENDPOINT" then ready=true break @@ -74,6 +79,11 @@ then fi AWS=(aws --endpoint-url "$ENDPOINT") +if [ "$SCHEME" = "https" ] +then + # CI keystore is self-signed; skip cert verification for the round-trip. + AWS+=(--no-verify-ssl) +fi echo "==> Creating bucket: $BUCKET" "${AWS[@]}" s3api create-bucket --bucket "$BUCKET" diff --git a/ci/functional/values/tls.yaml b/ci/functional/values/tls.yaml new file mode 100644 index 0000000..66f666a --- /dev/null +++ b/ci/functional/values/tls.yaml @@ -0,0 +1,30 @@ +# Functional-test values: native in-pod HTTPS (S3Proxy secure-endpoint) over the +# filesystem backend. The workflow generates a self-signed PKCS12 keystore and +# stores it (plus its password) in the Secret "s3proxy-tls" BEFORE install, so this +# exercises the production existingSecret path end to end. The smoke test then runs +# a full S3 round-trip over HTTPS (aws --no-verify-ssl against the self-signed cert). +config: + auth: + type: aws-v4 + identity: test-access-key + secret: test-secret-key + tls: + enabled: true + keystore: + existingSecret: s3proxy-tls + secretKey: keystore.p12 + keystorePassword: + existingSecret: s3proxy-tls + secretKey: keystore-password + backends: + filesystem: + enabled: true + nio2: true + basedir: /data/s3proxy +persistence: + enabled: true + size: 1Gi +resources: + requests: + cpu: 100m + memory: 256Mi diff --git a/test-values/tls-existing-secret.yaml b/test-values/tls-existing-secret.yaml new file mode 100644 index 0000000..4692cac --- /dev/null +++ b/test-values/tls-existing-secret.yaml @@ -0,0 +1,18 @@ +# Native in-pod HTTPS (S3Proxy secure-endpoint) with the keystore and password +# supplied via a pre-existing Secret (the production / cert-manager path). +config: + auth: + type: "aws-v4" + identity: "testkey" + secret: "testsecret" + tls: + enabled: true + keystore: + existingSecret: "my-tls" + secretKey: "keystore.p12" + keystorePassword: + existingSecret: "my-tls" + secretKey: "keystore-password" + backends: + filesystem: + enabled: true diff --git a/test-values/tls.yaml b/test-values/tls.yaml new file mode 100644 index 0000000..a5b442c --- /dev/null +++ b/test-values/tls.yaml @@ -0,0 +1,17 @@ +# Native in-pod HTTPS (S3Proxy secure-endpoint) with an INLINE keystore + password. +# Render-only: the keystore value is dummy base64, not a real PKCS12 archive. +config: + auth: + type: "aws-v4" + identity: "testkey" + secret: "testsecret" + tls: + enabled: true + keystore: + # dummy base64 ("MOCK-KEYSTORE"); a real deployment supplies a PKCS12 archive + value: "TU9DSy1LRVlTVE9SRQ==" + keystorePassword: + value: "changeit" + backends: + filesystem: + enabled: true From 20b8767a599ee9e5c1d21546b3ca2fbee84dcccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Fernando=20Carri=C3=B3n?= Date: Wed, 22 Jul 2026 14:17:15 +0200 Subject: [PATCH 2/3] DND-1415: extract s3proxy.portName helper (http/https) Replace the duplicated ternary at the top of deployment.yaml and service.yaml with a shared s3proxy.portName helper in _helpers.tpl. Render is byte-identical for both TLS-on and TLS-off. --- charts/s3proxy/templates/_helpers.tpl | 9 +++++++++ charts/s3proxy/templates/deployment.yaml | 7 +++---- charts/s3proxy/templates/service.yaml | 3 +-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/charts/s3proxy/templates/_helpers.tpl b/charts/s3proxy/templates/_helpers.tpl index 62b0688..2389513 100644 --- a/charts/s3proxy/templates/_helpers.tpl +++ b/charts/s3proxy/templates/_helpers.tpl @@ -18,3 +18,12 @@ name value is sourced from comet-common for consistency. app.kubernetes.io/name: {{ include "comet-common.names.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} + +{{- /* +Container/Service port name: "https" when native TLS is enabled (S3Proxy binds a +secure-endpoint), otherwise "http". Shared by service.yaml and deployment.yaml +(port + tcpSocket probes) so the port name tracks the actual protocol. +*/}} +{{- define "s3proxy.portName" -}} +{{- ternary "https" "http" .Values.config.tls.enabled -}} +{{- end }} diff --git a/charts/s3proxy/templates/deployment.yaml b/charts/s3proxy/templates/deployment.yaml index 26dae0b..a7a79bb 100644 --- a/charts/s3proxy/templates/deployment.yaml +++ b/charts/s3proxy/templates/deployment.yaml @@ -1,4 +1,3 @@ -{{ $portName := ternary "https" "http" .Values.config.tls.enabled -}} apiVersion: apps/v1 kind: Deployment metadata: @@ -158,14 +157,14 @@ spec: {{- toYaml . | nindent 12 }} {{- end }} ports: - - name: {{ $portName }} + - name: {{ include "s3proxy.portName" . }} containerPort: {{ .Values.service.targetPort }} protocol: TCP # tcpSocket probes only check the TCP accept, so they work unchanged # against a TLS-only port (no handshake performed). livenessProbe: tcpSocket: - port: {{ $portName }} + port: {{ include "s3proxy.portName" . }} initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 @@ -173,7 +172,7 @@ spec: failureThreshold: 3 readinessProbe: tcpSocket: - port: {{ $portName }} + port: {{ include "s3proxy.portName" . }} initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 3 diff --git a/charts/s3proxy/templates/service.yaml b/charts/s3proxy/templates/service.yaml index 64aad01..5a296b7 100644 --- a/charts/s3proxy/templates/service.yaml +++ b/charts/s3proxy/templates/service.yaml @@ -1,4 +1,3 @@ -{{ $portName := ternary "https" "http" .Values.config.tls.enabled -}} apiVersion: v1 kind: Service metadata: @@ -15,6 +14,6 @@ spec: - port: {{ .Values.service.port }} targetPort: {{ .Values.service.targetPort }} protocol: TCP - name: {{ $portName }} + name: {{ include "s3proxy.portName" . }} selector: {{- include "s3proxy.selectorLabels" . | nindent 4 }} From d552ea677ebffb4546ce694d0794225924bcbb99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Fernando=20Carri=C3=B3n?= Date: Wed, 22 Jul 2026 14:46:47 +0200 Subject: [PATCH 3/3] DND-1415: repo rules + README handling + helper indent - Convert .cursorrules to CLAUDE.md and strengthen the README rule: never hand-edit or regenerate README.md in a PR. Edit README.md.gotmpl + values.yaml; the release workflow renders and commits README.md on merge, and preview-readme previews the diff on the PR. - Revert the generated README.md in this PR back to base (let the release action render it). - Indent the ternary in the s3proxy.portName helper body (cosmetic; render identical for TLS on and off). --- .cursorrules | 29 ------- CLAUDE.md | 36 ++++++++ README.md | 116 +------------------------- charts/s3proxy/templates/_helpers.tpl | 2 +- 4 files changed, 40 insertions(+), 143 deletions(-) delete mode 100644 .cursorrules create mode 100644 CLAUDE.md diff --git a/.cursorrules b/.cursorrules deleted file mode 100644 index 1ca452e..0000000 --- a/.cursorrules +++ /dev/null @@ -1,29 +0,0 @@ -# Cursor Rules for S3Proxy Helm Chart Project - -## Project Structure -This is a Helm chart project for deploying S3Proxy to Kubernetes. The chart is located in `charts/s3proxy/`. - -## Important File Handling Rules - -### README.md Files - DO NOT EDIT DIRECTLY -**CRITICAL:** The following README.md files are auto-generated and must NEVER be edited directly: -- `/README.md` (root project README) -- `/charts/s3proxy/README.md` (chart README, if it exists) - -These files are generated from their corresponding template files using helm-docs. - -**When asked to modify README content, you MUST:** -1. ONLY edit the template file: `charts/s3proxy/README.md.gotmpl` -2. NEVER directly edit any `README.md` files -3. Inform the user that README.md is auto-generated and changes should be made to the template -4. After editing the template, remind the user to regenerate README.md using helm-docs - -**Template file location:** -- Template: `charts/s3proxy/README.md.gotmpl` -- Generated: `README.md` (root) - auto-generated from the template - -## Helm Chart Best Practices -- Always validate YAML syntax when editing chart files -- Preserve the existing indentation style (spaces for YAML files as per YAML spec) -- When modifying values.yaml, ensure documentation comments are kept in sync -- Test template rendering with `helm template` after making changes diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c5f941d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,36 @@ +# S3Proxy Helm Chart + +A Helm chart for deploying [S3Proxy](https://github.com/gaul/s3proxy) to Kubernetes. +The chart lives in `charts/s3proxy/`. + +## README.md is generated: never hand-edit it, and never commit it in a PR + +`README.md` (repo root) is generated from `charts/s3proxy/README.md.gotmpl` by +helm-docs. The values table is generated from the `# --` comments in +`charts/s3proxy/values.yaml`. + +**When changing documentation:** + +1. Edit **only** the source: `charts/s3proxy/README.md.gotmpl` (prose/sections) and + `charts/s3proxy/values.yaml` (the `# --` comments that drive the values table). +2. **Never edit `README.md` by hand.** +3. **Never regenerate or commit `README.md` in a pull request.** The + `release.yaml` workflow renders it from the template and commits it back to + `main` on merge; `preview-readme.yaml` posts the rendered diff on the PR for + review. A hand-rendered `README.md` in a PR only creates churn and merge + conflicts with that automation. Leave `README.md` untouched in the PR diff. + +## Helm chart conventions + +- Validate rendering with `helm template` and `helm lint` after editing chart files. +- Keep `values.yaml` `# --` documentation comments in sync with any value changes + (they feed the generated README values table). +- Preserve the existing YAML indentation style (spaces, per the YAML spec). +- The rendered manifests are schema-checked with `kubeconform -strict` and + style-checked with helm-polish in CI (`lint-render.yaml`); run them locally + before pushing when possible. +- Chart changes require a `Chart.yaml` `version` bump (`verify-chart-version.yaml` + enforces this). +- Functional behavior is covered by kind-based tests in `ci/functional/` and the + `functional-test.yaml` workflow; add a leg there when adding a feature that can + be exercised end to end. diff --git a/README.md b/README.md index 25accca..6d94a16 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # s3proxy -![Version: 0.3.0](https://img.shields.io/badge/Version-0.3.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 3.3.0](https://img.shields.io/badge/AppVersion-3.3.0-informational?style=flat-square) +![Version: 0.2.0](https://img.shields.io/badge/Version-0.2.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 3.3.0](https://img.shields.io/badge/AppVersion-3.3.0-informational?style=flat-square) A Helm chart for deploying S3Proxy - Access other storage backends via the S3 API @@ -583,48 +583,6 @@ The following section lists the configurable parameters of the s3proxy chart and bool false - - config.tls.enabled - Enable native in-pod HTTPS (S3Proxy secure-endpoint). When enabled, S3Proxy serves HTTPS only on service.targetPort (the plaintext endpoint is not bound), so TLS is terminated in the pod rather than at the ingress. Requires a PKCS12 (or JKS) keystore and its password. tcpSocket health probes are unaffected (they do not perform a TLS handshake). - bool - false - - - config.tls.keystore.existingSecret - Name of an existing Secret holding the keystore file (binary PKCS12/JKS). Takes precedence over value. Use this for a customer-provided keystore or a cert-manager-issued one (Certificate.spec.keystores.pkcs12). - string - "" - - - config.tls.keystore.secretKey - Key within keystore.existingSecret (or, when value is used, within the chart's own Secret) that holds the keystore file - string - "keystore.p12" - - - config.tls.keystore.value - Inline base64-encoded keystore, stored in the chart's own Secret and mounted as a file. Used only when keystore.existingSecret is empty. Convenient for testing; prefer existingSecret in production. - string - "" - - - config.tls.keystorePassword.existingSecret - Name of an existing Secret holding the keystore password. Takes precedence over value. - string - "" - - - config.tls.keystorePassword.secretKey - Key within keystorePassword.existingSecret that holds the keystore password - string - "keystore-password" - - - config.tls.keystorePassword.value - Inline keystore password, stored in the chart's own Secret and merged into the backend properties by the secret-merge initContainer (kept out of the ConfigMap). Used only when keystorePassword.existingSecret is empty. - string - "" - config.virtualHost Virtual Host configuration @@ -1000,76 +958,6 @@ aws --endpoint-url http://localhost:8080 s3 cp test.txt s3://test-bucket/ aws --endpoint-url http://localhost:8080 s3 ls s3://test-bucket/ ``` -## TLS / HTTPS (native, in-pod) - -By default S3Proxy binds plain HTTP and TLS is expected to be terminated at the -ingress. Set `config.tls.enabled=true` to have **S3Proxy itself terminate HTTPS in -the pod** (S3Proxy's `secure-endpoint`). When enabled: - -- S3Proxy serves **HTTPS only** on `service.targetPort` (the plaintext endpoint is - not bound). The container/Service port is named `https`. -- The health probes are `tcpSocket`, which only check the TCP accept (no TLS - handshake), so they keep working unchanged against the TLS port. -- A **PKCS12** keystore is expected (Jetty's default keystore type; JKS also works - if supplied). S3Proxy exposes only the keystore path and password, so the chart - does not set a keystore type. -- The keystore **password is never written to the ConfigMap**; it is merged into - the backend properties from the Secret by the config-merge initContainer. - -### Option A: existing Secret (recommended; also the cert-manager path) - -Reference a Secret that already holds the keystore file and the password. This is -also how a cert-manager `Certificate` with `spec.keystores.pkcs12` delivers a -keystore (point `keystore.existingSecret` at that Secret, and -`keystorePassword.existingSecret` at the password Secret it references): - -```yaml -config: - tls: - enabled: true - keystore: - existingSecret: my-tls # holds the PKCS12 archive - secretKey: keystore.p12 - keystorePassword: - existingSecret: my-tls # holds the password (may be the same Secret) - secretKey: keystore-password -``` - -### Option B: inline keystore + password - -Provide the base64-encoded keystore and the password inline; both are stored in the -chart's own Secret. Convenient for testing; prefer Option A in production. - -```yaml -config: - tls: - enabled: true - keystore: - value: "" - keystorePassword: - value: "changeit" -``` - -Create a PKCS12 keystore, for example: - -```bash -# From an existing cert + key: -openssl pkcs12 -export -inkey tls.key -in tls.crt \ - -out keystore.p12 -passout pass:changeit -# Inline value: -base64 -w0 keystore.p12 -# Or as an existing Secret (Option A): -kubectl create secret generic my-tls \ - --from-file=keystore.p12=keystore.p12 \ - --from-literal=keystore-password=changeit -``` - -Testing over HTTPS (the CI keystore above is self-signed, so skip verification): - -```bash -aws --endpoint-url https://localhost:9000 --no-verify-ssl s3 ls -``` - ## CORS Configuration To enable CORS support: @@ -1186,3 +1074,5 @@ This will remove all resources created by the chart. If using persistence, the P This Helm chart is provided as-is. S3Proxy itself is licensed under the Apache License 2.0. +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/charts/s3proxy/templates/_helpers.tpl b/charts/s3proxy/templates/_helpers.tpl index 2389513..07a7df4 100644 --- a/charts/s3proxy/templates/_helpers.tpl +++ b/charts/s3proxy/templates/_helpers.tpl @@ -25,5 +25,5 @@ secure-endpoint), otherwise "http". Shared by service.yaml and deployment.yaml (port + tcpSocket probes) so the port name tracks the actual protocol. */}} {{- define "s3proxy.portName" -}} -{{- ternary "https" "http" .Values.config.tls.enabled -}} + {{- ternary "https" "http" .Values.config.tls.enabled -}} {{- end }}