Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 0 additions & 29 deletions .cursorrules

This file was deleted.

28 changes: 26 additions & 2 deletions .github/workflows/functional-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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: |
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/lint-render.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion charts/s3proxy/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
70 changes: 70 additions & 0 deletions charts/s3proxy/README.md.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<base64-encoded PKCS12 keystore>"
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:
Expand Down
16 changes: 16 additions & 0 deletions charts/s3proxy/override-values.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<base64 PKCS12>"
# keystorePassword:
# existingSecret: "my-tls"
# secretKey: "keystore-password"
# # value: "changeit"

# Enable CORS for web applications
cors:
enabled: true
Expand Down
9 changes: 9 additions & 0 deletions charts/s3proxy/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
8 changes: 8 additions & 0 deletions charts/s3proxy/templates/configmap.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
Expand Down
50 changes: 47 additions & 3 deletions charts/s3proxy/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,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

Expand All @@ -94,6 +103,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:
Expand Down Expand Up @@ -143,20 +157,22 @@ spec:
{{- toYaml . | nindent 12 }}
{{- end }}
ports:
- name: http
- 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: http
port: {{ include "s3proxy.portName" . }}
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 3
readinessProbe:
tcpSocket:
port: http
port: {{ include "s3proxy.portName" . }}
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
Expand All @@ -168,6 +184,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
Expand All @@ -192,6 +213,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
Expand Down
Loading
Loading