From 7b6b816df9397b544002b45f19632acd55922022 Mon Sep 17 00:00:00 2001 From: niksis02 Date: Mon, 17 Aug 2026 23:22:27 +0400 Subject: [PATCH] feat: add Helm chart support for the standalone IAM service This adds a new iamServer Deployment, split public and private Services, a PersistentVolumeClaim, and cert-manager Certificate resources so the standalone versitygw iam API server can be deployed directly from this chart, independently scalable from the S3 gateway and backed by either internal file storage or HashiCorp Vault, with Vault auth credentials and Vault TLS material kept in separate Kubernetes secrets. The gateway side gains iam.type=standalone client wiring that reaches the IAM service over its private mTLS endpoint, with certificates supplied either through an existing secret or auto-provisioned via cert-manager using a shared CA-type issuer so both peers can verify each other from their own certificate's ca.crt, and the chart auto-targets the in-chart service when no external endpoint is configured. gateway.logLevel and iamServer.logLevel replace the old boolean debug flag with the silent, debug, and unsafe levels the binary now supports, docker-entrypoint.sh gained iam as a recognized VGW_BACKEND value so the new deployment can start through the existing entrypoint, NetworkPolicy resources were corrected to use proper peer lists and to default to deny instead of allow when no ingress rules are configured, pod and Deployment selector labels were separated between the gateway and the IAM server to prevent them from matching each other's Services, and a battery of template time validation guards was added to fail fast on invalid combinations such as multiple replicas against the internal file store or a missing certificate for a TCP private endpoint, together with expanded helm lint coverage in CI for these new configurations. --- .github/workflows/helm-lint.yml | 20 ++ chart/Chart.yaml | 2 +- chart/README.md | 46 +++- chart/templates/_helpers.tpl | 119 +++++++++++ chart/templates/deployment.yaml | 106 +++++++++- chart/templates/iam-certificate.yaml | 45 ++++ chart/templates/iam-deployment.yaml | 300 +++++++++++++++++++++++++++ chart/templates/iam-pvc.yaml | 18 ++ chart/templates/iam-service.yaml | 36 ++++ chart/templates/networkpolicy.yaml | 86 +++++++- chart/values.yaml | 209 ++++++++++++++++++- docker-entrypoint.sh | 4 +- 12 files changed, 967 insertions(+), 24 deletions(-) create mode 100644 chart/templates/iam-certificate.yaml create mode 100644 chart/templates/iam-deployment.yaml create mode 100644 chart/templates/iam-pvc.yaml create mode 100644 chart/templates/iam-service.yaml diff --git a/.github/workflows/helm-lint.yml b/.github/workflows/helm-lint.yml index 3a05d15a..5615c294 100644 --- a/.github/workflows/helm-lint.yml +++ b/.github/workflows/helm-lint.yml @@ -15,3 +15,23 @@ jobs: - name: Lint chart run: helm lint chart/ --strict --set auth.accessKey=dummy --set auth.secretKey=dummy + + - name: Lint standalone IAM configurations + run: | + scenarios=( + # existingSecret-based mTLS for both sides, plus NetworkPolicy + "--set iam.enabled=true --set iam.type=standalone --set iam.standalone.certificate.existingSecret=iam-client-tls --set iamServer.enabled=true --set iamServer.private.certificate.existingSecret=iam-server-tls --set networkPolicy.enabled=true" + # cert-manager-issued mTLS, plus TLS on the public control-plane API + "--set iam.enabled=true --set iam.type=standalone --set iam.standalone.certificate.create=true --set iam.standalone.certificate.issuerRef.name=internal-ca --set iamServer.enabled=true --set iamServer.private.certificate.create=true --set iamServer.private.certificate.issuerRef.name=internal-ca --set iamServer.tls.enabled=true --set iamServer.tls.secretName=iam-public-tls" + # Vault storage with multiple iamServer replicas + "--set iamServer.enabled=true --set iamServer.replicaCount=2 --set iamServer.storage.type=vault --set iamServer.storage.vault.endpointUrl=https://vault.example.test --set iamServer.storage.vault.existingSecret=vault-auth --set iamServer.storage.vault.tlsExistingSecret=vault-tls --set iamServer.private.certificate.existingSecret=iam-server-tls" + # externally managed standalone IAM service (no in-chart iamServer) + "--set iam.enabled=true --set iam.type=standalone --set iam.standalone.endpoint=iam.example.test:7443 --set iam.standalone.certificate.existingSecret=iam-client-tls" + ) + + for scenario in "${scenarios[@]}"; do + echo "::group::helm lint --set ${scenario}" + # shellcheck disable=SC2086 + helm lint chart/ --strict --set auth.existingSecret=root-credentials ${scenario} + echo "::endgroup::" + done diff --git a/chart/Chart.yaml b/chart/Chart.yaml index 56f594b9..3f840a35 100644 --- a/chart/Chart.yaml +++ b/chart/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: versitygw description: A Helm chart for deploying the Versity S3 Gateway on Kubernetes type: application -version: 0.3.5 +version: 0.4.0 sources: - https://github.com/versity/versitygw icon: https://raw.githubusercontent.com/versity/versitygw/main/webui/web/assets/images/Versity-logo-blue-horizontal.png diff --git a/chart/README.md b/chart/README.md index c04907c4..2ef15225 100644 --- a/chart/README.md +++ b/chart/README.md @@ -12,7 +12,7 @@ Versity is an S3-compatible storage gateway that proxies S3 API requests to a va - Kubernetes **1.19+** - Helm **3.8+** (OCI registry support) -- optional: [cert-manager](https://cert-manager.io/) (only required if `certificate.create=true`) +- optional: [cert-manager](https://cert-manager.io/) (required when any of `certificate.create`, `iam.standalone.certificate.create`, or `iamServer.private.certificate.create` is enabled) ## Installation @@ -104,11 +104,50 @@ gateway: | **Admin API** | `admin.enabled=true` — exposes a separate management API on `admin.port` (default `7071`) | | **WebUI** | `webui.enabled=true` — browser-based management UI on `webui.port` (default `8080`); set `webui.apiGateways` and `webui.adminGateways` to your externally reachable endpoints | | **Website Hosting** | `website.enabled=true` — static website hosting endpoint on `website.port` (default `8090`); optionally set `website.domain` for virtual-host routing (e.g. `example.com`), or omit it for catch-all mode where the full hostname is the bucket name | -| **IAM** | `iam.enabled=true` — flat-file identity and access management stored alongside backend data | +| **IAM** | `iam.enabled=true` — identity and access management. `iam.type=internal` (default) stores accounts in a flat file alongside backend data; `iam.type=standalone` delegates to a separate standalone IAM API service — see [Standalone IAM Service](#standalone-iam-service) below | | **Persistence** | `persistence.enabled=true` — provisions a PVC for backend data and IAM storage; defaults to `10Gi`, or uses a hostPath volume specified by `persistence.hostPath` | | **NetworkPolicy** | `networkPolicy.enabled=true` — restricts ingress to selected pods/namespaces; allows all egress | +| **Debug logging** | `gateway.logLevel` — `silent` (default), `debug` (request/response logging, secrets masked), or `unsafe` (unmasked, local troubleshooting only) | | **Scheduling** | `nodeSelector`, `affinity`, `tolerations`, and `topologySpreadConstraints` — control pod placement and spread replicas across nodes/zones for high availability | +## Standalone IAM Service + +In addition to `iam.type=internal` (flat-file IAM stored inside the gateway pod), the chart can deploy the standalone IAM API server — an AWS-compatible IAM Query API — as its own Deployment with separate public and private Services, and configure one or more gateways to use it via `iam.type=standalone`. + +```yaml +iam: + enabled: true + type: standalone + standalone: + # Left empty here: auto-targets the in-chart private IAM Service below. + certificate: + create: true + issuerRef: + kind: ClusterIssuer + name: internal-ca + +iamServer: + enabled: true + storage: + type: internal # or vault + private: + certificate: + create: true + issuerRef: + kind: ClusterIssuer + name: internal-ca +``` + +Key points: + +- **Independent scaling**: `iamServer` is a separate Deployment (`iamServer.replicaCount`), so it can be centralized and scaled independently of the gateway. Manage users/roles/policies against its public control-plane API (`iamServer.port`, default `7070`) using the AWS CLI/SDK. It reuses the gateway root Secret by default; set `iamServer.auth.existingSecret` to separate the control-plane identity, and point `iam.standalone.credentials.existingSecret` at the corresponding client identity. +- **Storage**: `iamServer.storage.type` is `internal` (file-backed, needs `iamServer.persistence`, is limited to one replica, and always uses a `Recreate` rollout) or `vault` (`iamServer.storage.vault.*`, centralized and required if `iamServer.replicaCount > 1`). +- **Separate Services**: `iamServer.service.type` applies only to the public control-plane Service. The private listener is exposed by a separate, always-`ClusterIP` Service, so selecting `NodePort` or `LoadBalancer` does not publish the private port. Enable `iamServer.tls` before exposing the public API outside a trusted network. +- **Private mTLS endpoint**: gateways reach the standalone IAM service over a private endpoint (`iamServer.private.port`, default `7443`) that always requires mutual TLS on TCP. Provide certificates either via `existingSecret` (bring your own `tls.crt`/`tls.key`/`ca.crt`) or `certificate.create=true` to auto-provision via cert-manager. +- **Shared CA requirement**: when using cert-manager auto-provisioning, `iamServer.private.certificate.issuerRef` and `iam.standalone.certificate.issuerRef` **must reference the same CA-type issuer** (an `Issuer`/`ClusterIssuer` of kind `CA`, or a Vault issuer) — one that populates `ca.crt` in the resulting Secret. Both sides verify their peer using their own certificate's `ca.crt`, which only works when both certificates share the same issuing CA. +- **External IAM service**: to point a gateway at a standalone IAM service deployed outside this chart (or by a separate chart release), set `iam.standalone.endpoint` to its `host:port` and provide the mTLS material via `iam.standalone.certificate.existingSecret`. +- **Secret rotation**: the processes load mTLS material and environment-based credentials at startup. After a referenced Secret rotates, restart both Deployments or configure a Secret-reloader controller through `deploymentAnnotations` and `iamServer.deploymentAnnotations`. + ## Scaling and Persistence By default, this chart enables persistence via a `PersistentVolumeClaim` (PVC) to ensure data consistency and prevent data loss. @@ -121,9 +160,10 @@ Special care must be taken particularly when using multiple replicas with such a When scaling `versitygw` horizontally by setting `replicaCount` greater than 1, special care must be taken regarding the storage backend: -- **POSIX or Internal IAM**: These backends store state locally on the filesystem. +- **POSIX**: This backend stores state on the filesystem. - Using **ReadWriteOnce (RWO)**: All replicas must be scheduled on the **same Kubernetes node** to share the same volume. This is useful for process-level concurrency (e.g., when using high-performance local block storage) but limits high availability across nodes. - Using **ReadWriteMany (RWX)**: Replicas can be distributed across **multiple nodes** in the cluster. This is the recommended approach for true horizontal scaling and high availability. When using RWX, it is also recommended to use pod anti-affinity (via `affinity` in `values.yaml`) or topology spread constraints (via `topologySpreadConstraints` in `values.yaml`) to ensure pods are distributed across nodes/zones. +- **IAM**: `iam.type=internal` is limited to a single gateway replica because its file store does not coordinate concurrent writers. Use standalone IAM with Vault storage, LDAP, Vault-direct, or another external IAM backend before scaling the gateway above one replica. - **Stateless Backends (S3, Azure)**: If you are using a stateless storage backend (e.g. proxying to another S3 store) **and** you are either not using IAM or using an external IAM provider (e.g. LDAP, Vault), persistence can be safely disabled by setting `persistence.enabled=false`. ### Deployment Strategy diff --git a/chart/templates/_helpers.tpl b/chart/templates/_helpers.tpl index 51ec393c..5b12140d 100644 --- a/chart/templates/_helpers.tpl +++ b/chart/templates/_helpers.tpl @@ -73,6 +73,20 @@ Uses auth.existingSecret if set, otherwise derives a name from the release fulln {{- end }} {{- end }} +{{/* +The root credential Secret used by the standalone IAM API server. It defaults +to the gateway root Secret for backward compatibility, but can be separated so +the public IAM control plane and S3 gateway do not share administrative keys. +*/}} +{{- define "versitygw.iamServerCredentialsSecretName" -}} +{{- $auth := .Values.iamServer.auth | default dict -}} +{{- if $auth.existingSecret }} +{{- $auth.existingSecret }} +{{- else }} +{{- include "versitygw.credentialsSecretName" . }} +{{- end }} +{{- end }} + {{/* The name of the PVC to use for persistence. Returns empty string if persistence is disabled. @@ -98,3 +112,108 @@ Uses certificate.secretName if set, otherwise derives a name from the release fu {{- printf "%s-cert" (include "versitygw.fullname" .) }} {{- end }} {{- end }} + +{{/* +The name label for the standalone IAM API server. It must differ from the +gateway's name label because the gateway Deployment's immutable selector only +contains app.kubernetes.io/name and app.kubernetes.io/instance. Reusing that +pair would make the gateway Deployment, Service, and NetworkPolicy also select +IAM server pods. +*/}} +{{- define "versitygw.iamServerName" -}} +{{- $base := include "versitygw.name" . | trunc 59 | trimSuffix "-" -}} +{{- printf "%s-iam" $base }} +{{- end }} + +{{/* +The fullname of the standalone IAM API server's Deployment/Service. +*/}} +{{- define "versitygw.iamServerFullname" -}} +{{- $base := include "versitygw.fullname" . | trunc 59 | trimSuffix "-" -}} +{{- printf "%s-iam" $base }} +{{- end }} + +{{/* +The standalone IAM private Service is always cluster-internal, independently +of how the public control-plane Service is exposed. +*/}} +{{- define "versitygw.iamServerPrivateServiceFullname" -}} +{{- $base := include "versitygw.fullname" . | trunc 51 | trimSuffix "-" -}} +{{- printf "%s-iam-private" $base }} +{{- end }} + +{{/* +Selector labels for the standalone IAM API server. Deliberately separate from +versitygw.selectorLabels (used by the main gateway Deployment's immutable +spec.selector) so the two Deployments never collide. +*/}} +{{- define "versitygw.iamServerSelectorLabels" -}} +app.kubernetes.io/name: {{ include "versitygw.iamServerName" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/component: iam-server +{{- end }} + +{{/* +Common labels for the standalone IAM API server. +*/}} +{{- define "versitygw.iamServerLabels" -}} +helm.sh/chart: {{ include "versitygw.chart" . }} +{{ include "versitygw.iamServerSelectorLabels" . }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +The name of the PVC used for the standalone IAM API server's file-backed storage. +Returns empty string if persistence is disabled. +*/}} +{{- define "versitygw.iamServerPvcName" -}} +{{- if .Values.iamServer.persistence.enabled }} +{{- if .Values.iamServer.persistence.claimName }} +{{- .Values.iamServer.persistence.claimName }} +{{- else }} +{{- $base := include "versitygw.fullname" . | trunc 54 | trimSuffix "-" -}} +{{- printf "%s-iam-data" $base }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +The name of the Secret holding the standalone IAM API server's private-listener +server certificate (tls.crt/tls.key) and the CA (ca.crt) used to verify gateway +client certificates. Uses iamServer.private.certificate.existingSecret if set, +otherwise derives a name for the cert-manager-managed Certificate. +*/}} +{{- define "versitygw.iamServerPrivateCertSecretName" -}} +{{- if .Values.iamServer.private.certificate.existingSecret }} +{{- .Values.iamServer.private.certificate.existingSecret }} +{{- else }} +{{- printf "%s-private-cert" (include "versitygw.iamServerFullname" .) }} +{{- end }} +{{- end }} + +{{/* +The name of the Secret holding the gateway's mTLS client certificate +(tls.crt/tls.key) and the CA (ca.crt) used to verify the standalone IAM +service's server certificate. Uses iam.standalone.certificate.existingSecret +if set, otherwise derives a name for the cert-manager-managed Certificate. +*/}} +{{- define "versitygw.iamClientCertSecretName" -}} +{{- if .Values.iam.standalone.certificate.existingSecret }} +{{- .Values.iam.standalone.certificate.existingSecret }} +{{- else }} +{{- printf "%s-iam-client-cert" (include "versitygw.fullname" .) }} +{{- end }} +{{- end }} + +{{/* +The gateway's standalone-IAM private endpoint address. Uses +iam.standalone.endpoint if set, otherwise auto-targets the in-chart iamServer +Service's private port. +*/}} +{{- define "versitygw.standaloneIAMEndpoint" -}} +{{- if .Values.iam.standalone.endpoint }} +{{- .Values.iam.standalone.endpoint }} +{{- else if .Values.iamServer.enabled }} +{{- printf "%s:%d" (include "versitygw.iamServerPrivateServiceFullname" .) (.Values.iamServer.private.port | int) }} +{{- end }} +{{- end }} diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml index 393835a5..47dd6d3a 100644 --- a/chart/templates/deployment.yaml +++ b/chart/templates/deployment.yaml @@ -1,7 +1,19 @@ +{{- $gatewayLogLevel := default "silent" .Values.gateway.logLevel -}} +{{- $iamServer := .Values.iamServer | default dict -}} +{{- $iamServerEnabled := $iamServer.enabled | default false -}} +{{- $iamServerPrivate := $iamServer.private | default dict -}} +{{- $iamServerPrivateEnabled := $iamServerPrivate.enabled | default false -}} +{{- $iamServerAuth := $iamServer.auth | default dict -}} +{{- $iamStandalone := .Values.iam.standalone | default dict -}} +{{- $iamStandaloneEndpoint := $iamStandalone.endpoint | default "" -}} +{{- $iamStandaloneCredentials := $iamStandalone.credentials | default dict -}} {{- /* Safety check: multiple replicas with local state must have persistence enabled */}} +{{- if and (gt (int .Values.replicaCount) 1) .Values.iam.enabled (eq .Values.iam.type "internal") }} + {{- fail "replicaCount > 1 cannot use iam.type=internal because the file store does not coordinate concurrent writers; use standalone or another external IAM backend" }} +{{- end }} {{- if and (gt (int .Values.replicaCount) 1) (not .Values.persistence.enabled) }} - {{- if or (eq .Values.gateway.backend.type "posix") (and .Values.iam.enabled (eq .Values.iam.type "internal")) }} - {{- fail "Multiple replicas with POSIX backend or Internal IAM require persistence.enabled=true to prevent data loss and inconsistency across pods. If using a stateless backend (e.g. S3, Azure) and external IAM, set persistence.enabled=false." }} + {{- if eq .Values.gateway.backend.type "posix" }} + {{- fail "Multiple replicas with a POSIX backend require persistence.enabled=true. If using a stateless backend (e.g. S3 or Azure) and external IAM, persistence can be disabled." }} {{- end }} {{- end }} {{- if and .Values.persistence.enabled (not .Values.persistence.create) (not .Values.persistence.claimName) (not .Values.persistence.hostPath) }} @@ -19,12 +31,46 @@ {{- if and .Values.gateway.backend.versioningDir (not (or (eq .Values.gateway.backend.type "posix") (eq .Values.gateway.backend.type "scoutfs"))) }} {{- fail "gateway.backend.versioningDir is only supported with the posix and scoutfs backends" }} {{- end }} +{{- if not (or (eq $gatewayLogLevel "silent") (eq $gatewayLogLevel "debug") (eq $gatewayLogLevel "unsafe")) }} + {{- fail "gateway.logLevel must be one of silent, debug, or unsafe" }} +{{- end }} +{{- if and .Values.iam.enabled (eq .Values.iam.type "standalone") (not .Values.iam.standalone.endpoint) (not $iamServerEnabled) }} + {{- fail "iam.type=standalone requires either iam.standalone.endpoint or iamServer.enabled=true" }} +{{- end }} +{{- if and .Values.iam.enabled (eq .Values.iam.type "standalone") (not .Values.iam.standalone.endpoint) $iamServerEnabled (not $iamServerPrivateEnabled) }} + {{- fail "in-chart standalone IAM requires iamServer.private.enabled=true; otherwise set iam.standalone.endpoint explicitly" }} +{{- end }} +{{- if and .Values.iam.enabled (eq .Values.iam.type "standalone") $iamStandaloneEndpoint (not (regexMatch "^(\\[[0-9A-Fa-f:]+\\]|[^/:[:space:]]+):[0-9]+$" $iamStandaloneEndpoint)) }} + {{- fail "iam.standalone.endpoint must be a host:port TCP address; the chart does not provide a shared volume for Unix sockets" }} +{{- end }} +{{- if and .Values.iam.enabled (eq .Values.iam.type "standalone") (not (or .Values.iam.standalone.certificate.create .Values.iam.standalone.certificate.existingSecret)) }} + {{- fail "a standalone IAM TCP endpoint requires iam.standalone.certificate.create=true or iam.standalone.certificate.existingSecret" }} +{{- end }} +{{- if and .Values.iam.enabled (eq .Values.iam.type "standalone") .Values.iam.standalone.certificate.create (not .Values.iam.standalone.certificate.existingSecret) (not .Values.iam.standalone.certificate.issuerRef.name) }} + {{- fail "iam.standalone.certificate.issuerRef.name is required when creating the IAM client certificate" }} +{{- end }} +{{- if and .Values.iam.enabled (eq .Values.iam.type "standalone") (or (and .Values.iam.standalone.access (not .Values.iam.standalone.secret)) (and .Values.iam.standalone.secret (not .Values.iam.standalone.access))) }} + {{- fail "iam.standalone.access and iam.standalone.secret must be set together, or both left empty to use the root credentials" }} +{{- end }} +{{- if and .Values.iam.enabled (eq .Values.iam.type "standalone") $iamStandaloneCredentials.existingSecret (or .Values.iam.standalone.access .Values.iam.standalone.secret) }} + {{- fail "iam.standalone.credentials.existingSecret cannot be combined with literal iam.standalone.access/secret values" }} +{{- end }} +{{- if and .Values.iam.enabled (eq .Values.iam.type "standalone") $iamStandaloneCredentials.existingSecret (or (not $iamStandaloneCredentials.accessKeyKey) (not $iamStandaloneCredentials.secretKeyKey)) }} + {{- fail "iam.standalone.credentials.accessKeyKey and secretKeyKey are required when credentials.existingSecret is set" }} +{{- end }} +{{- if and .Values.iam.enabled (eq .Values.iam.type "standalone") (not .Values.iam.standalone.endpoint) $iamServerAuth.existingSecret (not $iamStandaloneCredentials.existingSecret) (not .Values.iam.standalone.access) }} + {{- fail "in-chart IAM with iamServer.auth.existingSecret requires iam.standalone.credentials.existingSecret (normally the same Secret)" }} +{{- end }} apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "versitygw.fullname" . }} labels: {{- include "versitygw.labels" . | nindent 4 }} + {{- with .Values.deploymentAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} spec: replicas: {{ .Values.replicaCount }} selector: @@ -105,7 +151,10 @@ spec: - name: VGW_QUIET value: "true" {{- end }} - {{- if .Values.gateway.debug }} + {{- if ne $gatewayLogLevel "silent" }} + - name: VGW_LOG_LEVEL + value: {{ $gatewayLogLevel | quote }} + {{- else if .Values.gateway.debug }} - name: VGW_LOG_LEVEL value: "debug" {{- end }} @@ -163,6 +212,44 @@ spec: {{- if eq .Values.iam.type "internal" }} - name: VGW_IAM_DIR value: "/mnt/iam" + {{- else if eq .Values.iam.type "standalone" }} + - name: VGW_IAM_STANDALONE_ENDPOINT + value: {{ include "versitygw.standaloneIAMEndpoint" . | quote }} + {{- if $iamStandaloneCredentials.existingSecret }} + - name: VGW_IAM_STANDALONE_ACCESS + valueFrom: + secretKeyRef: + name: {{ $iamStandaloneCredentials.existingSecret }} + key: {{ $iamStandaloneCredentials.accessKeyKey }} + - name: VGW_IAM_STANDALONE_SECRET + valueFrom: + secretKeyRef: + name: {{ $iamStandaloneCredentials.existingSecret }} + key: {{ $iamStandaloneCredentials.secretKeyKey }} + {{- else }} + {{- if .Values.iam.standalone.access }} + - name: VGW_IAM_STANDALONE_ACCESS + value: {{ .Values.iam.standalone.access | quote }} + {{- end }} + {{- if .Values.iam.standalone.secret }} + - name: VGW_IAM_STANDALONE_SECRET + value: {{ .Values.iam.standalone.secret | quote }} + {{- end }} + {{- end }} + {{- if or .Values.iam.standalone.certificate.create .Values.iam.standalone.certificate.existingSecret }} + - name: VGW_IAM_STANDALONE_CLIENT_CERT + value: /iam-client-certs/tls.crt + - name: VGW_IAM_STANDALONE_CLIENT_CERT_KEY + value: /iam-client-certs/tls.key + - name: VGW_IAM_STANDALONE_SERVER_CA + value: /iam-client-certs/ca.crt + {{- end }} + - name: VGW_IAM_STANDALONE_DEFAULT_UID + value: {{ .Values.iam.standalone.defaultUid | quote }} + - name: VGW_IAM_STANDALONE_DEFAULT_GID + value: {{ .Values.iam.standalone.defaultGid | quote }} + - name: VGW_IAM_STANDALONE_DEFAULT_PROJECT_ID + value: {{ .Values.iam.standalone.defaultProjectId | quote }} {{- end }} {{- if .Values.iam.cacheDisable }} - name: VGW_IAM_CACHE_DISABLE @@ -235,10 +322,17 @@ spec: subPath: versioning readOnly: false {{- end }} + {{- if or (not .Values.iam.enabled) (eq .Values.iam.type "internal") }} - name: data mountPath: /mnt/iam subPath: iam readOnly: false + {{- end }} + {{- if and .Values.iam.enabled (eq .Values.iam.type "standalone") (or .Values.iam.standalone.certificate.create .Values.iam.standalone.certificate.existingSecret) }} + - name: iam-client-certs + mountPath: /iam-client-certs + readOnly: true + {{- end }} {{- if .Values.tls.enabled }} - name: certificates mountPath: /certs @@ -263,6 +357,12 @@ spec: secret: secretName: {{ include "versitygw.certificateSecretName" . }} {{- end }} + {{- if and .Values.iam.enabled (eq .Values.iam.type "standalone") (or .Values.iam.standalone.certificate.create .Values.iam.standalone.certificate.existingSecret) }} + - name: iam-client-certs + secret: + secretName: {{ include "versitygw.iamClientCertSecretName" . }} + defaultMode: 0440 + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: diff --git a/chart/templates/iam-certificate.yaml b/chart/templates/iam-certificate.yaml new file mode 100644 index 00000000..273de8b3 --- /dev/null +++ b/chart/templates/iam-certificate.yaml @@ -0,0 +1,45 @@ +{{- $iamServer := .Values.iamServer | default dict -}} +{{- if and ($iamServer.enabled | default false) .Values.iamServer.private.enabled .Values.iamServer.private.certificate.create (not .Values.iamServer.private.certificate.existingSecret) }} +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ include "versitygw.iamServerPrivateCertSecretName" . }} + labels: + {{- include "versitygw.iamServerLabels" . | nindent 4 }} +spec: + dnsNames: + {{- if .Values.iamServer.private.certificate.dnsNames }} + {{- toYaml .Values.iamServer.private.certificate.dnsNames | nindent 4 }} + {{- else }} + - {{ include "versitygw.iamServerPrivateServiceFullname" . }} + - {{ printf "%s.%s.svc" (include "versitygw.iamServerPrivateServiceFullname" .) .Release.Namespace }} + - {{ printf "%s.%s.svc.cluster.local" (include "versitygw.iamServerPrivateServiceFullname" .) .Release.Namespace }} + {{- end }} + issuerRef: + {{- toYaml .Values.iamServer.private.certificate.issuerRef | nindent 4 }} + privateKey: + algorithm: ECDSA + size: 256 + secretName: {{ include "versitygw.iamServerPrivateCertSecretName" . }} + usages: + - server auth +{{- end }} +{{- if and .Values.iam.enabled (eq .Values.iam.type "standalone") .Values.iam.standalone.certificate.create (not .Values.iam.standalone.certificate.existingSecret) }} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ include "versitygw.iamClientCertSecretName" . }} + labels: + {{- include "versitygw.labels" . | nindent 4 }} +spec: + commonName: {{ include "versitygw.fullname" . }} + issuerRef: + {{- toYaml .Values.iam.standalone.certificate.issuerRef | nindent 4 }} + privateKey: + algorithm: ECDSA + size: 256 + secretName: {{ include "versitygw.iamClientCertSecretName" . }} + usages: + - client auth +{{- end }} diff --git a/chart/templates/iam-deployment.yaml b/chart/templates/iam-deployment.yaml new file mode 100644 index 00000000..7ff671cc --- /dev/null +++ b/chart/templates/iam-deployment.yaml @@ -0,0 +1,300 @@ +{{- $iamServer := .Values.iamServer | default dict -}} +{{- if ($iamServer.enabled | default false) }} +{{- $iamServerAuth := .Values.iamServer.auth | default dict -}} +{{- if not (or (eq .Values.iamServer.storage.type "internal") (eq .Values.iamServer.storage.type "vault")) }} + {{- fail "iamServer.storage.type must be either internal or vault" }} +{{- end }} +{{- if and (gt (int .Values.iamServer.replicaCount) 1) (eq .Values.iamServer.storage.type "internal") }} + {{- fail "iamServer.replicaCount > 1 requires iamServer.storage.type=vault; the internal file store does not coordinate concurrent writers" }} +{{- end }} +{{- if and (eq .Values.iamServer.storage.type "internal") .Values.iamServer.persistence.enabled (not .Values.iamServer.persistence.create) (not .Values.iamServer.persistence.claimName) }} + {{- fail "iamServer.persistence.claimName is required when iamServer.persistence.create is false" }} +{{- end }} +{{- if and (eq .Values.iamServer.storage.type "vault") (not .Values.iamServer.storage.vault.endpointUrl) }} + {{- fail "iamServer.storage.vault.endpointUrl is required when iamServer.storage.type=vault" }} +{{- end }} +{{- if and .Values.iamServer.private.enabled (not .Values.iamServer.private.certificate.create) (not .Values.iamServer.private.certificate.existingSecret) }} + {{- fail "iamServer.private.enabled requires either iamServer.private.certificate.create=true or iamServer.private.certificate.existingSecret" }} +{{- end }} +{{- if and .Values.iamServer.private.enabled .Values.iamServer.private.certificate.create (not .Values.iamServer.private.certificate.existingSecret) (not .Values.iamServer.private.certificate.issuerRef.name) }} + {{- fail "iamServer.private.certificate.issuerRef.name is required when creating the IAM server certificate" }} +{{- end }} +{{- if and .Values.iamServer.private.enabled (eq (int .Values.iamServer.private.port) (int .Values.iamServer.port)) }} + {{- fail "iamServer.private.port must differ from iamServer.port" }} +{{- end }} +{{- if and .Values.iamServer.tls.enabled (not .Values.iamServer.tls.secretName) }} + {{- fail "iamServer.tls.secretName is required when iamServer.tls.enabled=true" }} +{{- end }} +{{- if not (or (eq .Values.iamServer.logLevel "silent") (eq .Values.iamServer.logLevel "debug") (eq .Values.iamServer.logLevel "unsafe")) }} + {{- fail "iamServer.logLevel must be one of silent, debug, or unsafe" }} +{{- end }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "versitygw.iamServerFullname" . }} + labels: + {{- include "versitygw.iamServerLabels" . | nindent 4 }} + {{- with .Values.iamServer.deploymentAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + replicas: {{ .Values.iamServer.replicaCount }} + selector: + matchLabels: + {{- include "versitygw.iamServerSelectorLabels" . | nindent 6 }} + {{- if eq .Values.iamServer.storage.type "internal" }} + # Avoid overlapping file-store writers and ReadWriteOnce multi-attach errors + # while a new ReplicaSet replaces the old one. + strategy: + type: Recreate + {{- else }} + {{- with .Values.iamServer.strategy }} + strategy: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} + template: + metadata: + annotations: + {{- if and (not $iamServerAuth.existingSecret) (not .Values.auth.existingSecret) }} + checksum/credentials: {{ cat .Release.Name .Values.auth.accessKey .Values.auth.secretKey | sha256sum | trunc 12 }} + {{- end }} + {{- with .Values.iamServer.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "versitygw.iamServerSelectorLabels" . | nindent 8 }} + {{- with .Values.iamServer.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.image.pullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "versitygw.serviceAccountName" . }} + automountServiceAccountToken: false + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: iam-server + image: "{{ .Values.iamServer.image.repository | default .Values.image.repository }}:{{ .Values.iamServer.image.tag | default .Values.image.tag }}" + imagePullPolicy: {{ .Values.iamServer.image.pullPolicy | default .Values.image.pullPolicy }} + env: + - name: VGW_BACKEND + value: "iam" + # Root credentials -- sourced from the same Kubernetes Secret as the gateway + - name: ROOT_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: {{ include "versitygw.iamServerCredentialsSecretName" . }} + key: rootAccessKeyId + - name: ROOT_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ include "versitygw.iamServerCredentialsSecretName" . }} + key: rootSecretAccessKey + - name: VGW_PORT + value: ":{{ .Values.iamServer.port }}" + - name: VGW_HEALTH + value: "/_/health" + - name: VGW_MAX_CONNECTIONS + value: {{ .Values.iamServer.maxConnections | quote }} + - name: VGW_MAX_REQUESTS + value: {{ .Values.iamServer.maxRequests | quote }} + {{- if .Values.iamServer.quiet }} + - name: VGW_QUIET + value: "true" + {{- end }} + {{- if ne .Values.iamServer.logLevel "silent" }} + - name: VGW_LOG_LEVEL + value: {{ .Values.iamServer.logLevel | quote }} + {{- end }} + {{- if .Values.iamServer.disableOidcThumbprintAutofetch }} + - name: VGW_IAM_DISABLE_OIDC_THUMBPRINT_AUTOFETCH + value: "true" + {{- end }} + {{- if .Values.iamServer.tls.enabled }} + - name: VGW_CERT + value: /certs/tls.crt + - name: VGW_KEY + value: /certs/tls.key + {{- end }} + # Storage backend + {{- if eq .Values.iamServer.storage.type "internal" }} + - name: VGW_IAM_DIR + value: "/mnt/iam" + {{- else if eq .Values.iamServer.storage.type "vault" }} + - name: VGW_IAM_VAULT_ENDPOINT_URL + value: {{ .Values.iamServer.storage.vault.endpointUrl | quote }} + {{- if .Values.iamServer.storage.vault.namespace }} + - name: VGW_IAM_VAULT_NAMESPACE + value: {{ .Values.iamServer.storage.vault.namespace | quote }} + {{- end }} + {{- if .Values.iamServer.storage.vault.secretStoragePath }} + - name: VGW_IAM_VAULT_SECRET_STORAGE_PATH + value: {{ .Values.iamServer.storage.vault.secretStoragePath | quote }} + {{- end }} + {{- if .Values.iamServer.storage.vault.secretStorageNamespace }} + - name: VGW_IAM_VAULT_SECRET_STORAGE_NAMESPACE + value: {{ .Values.iamServer.storage.vault.secretStorageNamespace | quote }} + {{- end }} + {{- if .Values.iamServer.storage.vault.authMethod }} + - name: VGW_IAM_VAULT_AUTH_METHOD + value: {{ .Values.iamServer.storage.vault.authMethod | quote }} + {{- end }} + {{- if .Values.iamServer.storage.vault.authNamespace }} + - name: VGW_IAM_VAULT_AUTH_NAMESPACE + value: {{ .Values.iamServer.storage.vault.authNamespace | quote }} + {{- end }} + {{- if .Values.iamServer.storage.vault.mountPath }} + - name: VGW_IAM_VAULT_MOUNT_PATH + value: {{ .Values.iamServer.storage.vault.mountPath | quote }} + {{- end }} + {{- if .Values.iamServer.storage.vault.roleId }} + - name: VGW_IAM_VAULT_ROLE_ID + value: {{ .Values.iamServer.storage.vault.roleId | quote }} + {{- end }} + {{- if .Values.iamServer.storage.vault.existingSecret }} + - name: VGW_IAM_VAULT_ROLE_SECRET + valueFrom: + secretKeyRef: + name: {{ .Values.iamServer.storage.vault.existingSecret }} + key: roleSecret + optional: true + - name: VGW_IAM_VAULT_ROOT_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.iamServer.storage.vault.existingSecret }} + key: rootToken + optional: true + {{- end }} + {{- if .Values.iamServer.storage.vault.tlsExistingSecret }} + {{- if not .Values.iamServer.storage.vault.serverCert }} + - name: VGW_IAM_VAULT_SERVER_CERT + valueFrom: + secretKeyRef: + name: {{ .Values.iamServer.storage.vault.tlsExistingSecret }} + key: serverCert + optional: true + {{- end }} + {{- if not .Values.iamServer.storage.vault.clientCert }} + - name: VGW_IAM_VAULT_CLIENT_CERT + valueFrom: + secretKeyRef: + name: {{ .Values.iamServer.storage.vault.tlsExistingSecret }} + key: clientCert + optional: true + {{- end }} + {{- if not .Values.iamServer.storage.vault.clientCertKey }} + - name: VGW_IAM_VAULT_CLIENT_CERT_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.iamServer.storage.vault.tlsExistingSecret }} + key: clientCertKey + optional: true + {{- end }} + {{- end }} + {{- if .Values.iamServer.storage.vault.serverCert }} + - name: VGW_IAM_VAULT_SERVER_CERT + value: | + {{- .Values.iamServer.storage.vault.serverCert | nindent 16 }} + {{- end }} + {{- if .Values.iamServer.storage.vault.clientCert }} + - name: VGW_IAM_VAULT_CLIENT_CERT + value: | + {{- .Values.iamServer.storage.vault.clientCert | nindent 16 }} + {{- end }} + {{- if .Values.iamServer.storage.vault.clientCertKey }} + - name: VGW_IAM_VAULT_CLIENT_CERT_KEY + value: | + {{- .Values.iamServer.storage.vault.clientCertKey | nindent 16 }} + {{- end }} + {{- end }} + # Private mTLS endpoint + {{- if .Values.iamServer.private.enabled }} + - name: VGW_IAM_PRIVATE_PORTS + value: ":{{ .Values.iamServer.private.port }}" + - name: VGW_IAM_PRIVATE_CERT + value: /iam-private-certs/tls.crt + - name: VGW_IAM_PRIVATE_CERT_KEY + value: /iam-private-certs/tls.key + - name: VGW_IAM_PRIVATE_CLIENT_CA + value: /iam-private-certs/ca.crt + {{- end }} + ports: + - name: iam-api + containerPort: {{ .Values.iamServer.port }} + protocol: TCP + {{- if .Values.iamServer.private.enabled }} + - name: iam-private + containerPort: {{ .Values.iamServer.private.port }} + protocol: TCP + {{- end }} + readinessProbe: + httpGet: + path: "/_/health" + port: iam-api + {{- if .Values.iamServer.tls.enabled }} + scheme: HTTPS + {{- end }} + initialDelaySeconds: 5 + periodSeconds: 15 + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + resources: + {{- toYaml .Values.iamServer.resources | nindent 12 }} + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + {{- if eq .Values.iamServer.storage.type "internal" }} + - name: data + mountPath: /mnt/iam + readOnly: false + {{- end }} + {{- if .Values.iamServer.private.enabled }} + - name: private-certs + mountPath: /iam-private-certs + readOnly: true + {{- end }} + {{- if .Values.iamServer.tls.enabled }} + - name: certificates + mountPath: /certs + readOnly: true + {{- end }} + volumes: + {{- if eq .Values.iamServer.storage.type "internal" }} + - name: data + {{- if .Values.iamServer.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ include "versitygw.iamServerPvcName" . }} + {{- else }} + emptyDir: {} + {{- end }} + {{- end }} + {{- if .Values.iamServer.private.enabled }} + - name: private-certs + secret: + secretName: {{ include "versitygw.iamServerPrivateCertSecretName" . }} + defaultMode: 0440 + {{- end }} + {{- if .Values.iamServer.tls.enabled }} + - name: certificates + secret: + secretName: {{ .Values.iamServer.tls.secretName }} + defaultMode: 0440 + {{- end }} + + {{- with .Values.iamServer.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.iamServer.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.iamServer.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/chart/templates/iam-pvc.yaml b/chart/templates/iam-pvc.yaml new file mode 100644 index 00000000..55900e06 --- /dev/null +++ b/chart/templates/iam-pvc.yaml @@ -0,0 +1,18 @@ +{{- $iamServer := .Values.iamServer | default dict -}} +{{- if and ($iamServer.enabled | default false) (eq .Values.iamServer.storage.type "internal") .Values.iamServer.persistence.enabled .Values.iamServer.persistence.create -}} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "versitygw.iamServerPvcName" . }} + labels: + {{- include "versitygw.iamServerLabels" . | nindent 4 }} +spec: + accessModes: + - {{ .Values.iamServer.persistence.accessMode | quote }} + resources: + requests: + storage: {{ .Values.iamServer.persistence.size }} + {{- if .Values.iamServer.persistence.storageClassName }} + storageClassName: {{ .Values.iamServer.persistence.storageClassName | quote }} + {{- end }} +{{- end }} diff --git a/chart/templates/iam-service.yaml b/chart/templates/iam-service.yaml new file mode 100644 index 00000000..279a899a --- /dev/null +++ b/chart/templates/iam-service.yaml @@ -0,0 +1,36 @@ +{{- $iamServer := .Values.iamServer | default dict -}} +{{- if ($iamServer.enabled | default false) }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "versitygw.iamServerFullname" . }} + labels: + {{- include "versitygw.iamServerLabels" . | nindent 4 }} +spec: + type: {{ .Values.iamServer.service.type }} + ports: + - port: {{ .Values.iamServer.port }} + targetPort: iam-api + protocol: TCP + name: iam-api + selector: + {{- include "versitygw.iamServerSelectorLabels" . | nindent 4 }} +{{- if .Values.iamServer.private.enabled }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "versitygw.iamServerPrivateServiceFullname" . }} + labels: + {{- include "versitygw.iamServerLabels" . | nindent 4 }} +spec: + type: ClusterIP + ports: + - port: {{ .Values.iamServer.private.port }} + targetPort: iam-private + protocol: TCP + name: iam-private + selector: + {{- include "versitygw.iamServerSelectorLabels" . | nindent 4 }} +{{- end }} +{{- end }} diff --git a/chart/templates/networkpolicy.yaml b/chart/templates/networkpolicy.yaml index 8cdd073e..e9cdaf35 100644 --- a/chart/templates/networkpolicy.yaml +++ b/chart/templates/networkpolicy.yaml @@ -1,20 +1,98 @@ +{{- $allowedNamespaces := .Values.networkPolicy.allowIngressFromNamespaces | default dict -}} +{{- $allowedPods := .Values.networkPolicy.allowIngressFromPods | default dict -}} +{{- $iamServer := .Values.iamServer | default dict -}} {{- if .Values.networkPolicy.enabled }} apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: {{ include "versitygw.fullname" . }} + labels: + {{- include "versitygw.labels" . | nindent 4 }} spec: podSelector: matchLabels: {{- include "versitygw.selectorLabels" . | nindent 6 }} # Permit ingress traffic only from the specified namespaces and pods ingress: + {{- if and $allowedNamespaces $allowedPods }} - from: - namespaceSelector: - matchLabels: {{- toYaml .Values.networkPolicy.allowIngressFromNamespaces | nindent 10 }} - podSelector: {{- toYaml .Values.networkPolicy.allowIngressFromPods | nindent 10 }} - # By omitting the ports specification we apply this NetworkPolicy to all ports + - namespaceSelector: + matchLabels: + {{- toYaml $allowedNamespaces | nindent 14 }} + podSelector: + matchLabels: + {{- toYaml $allowedPods | nindent 14 }} ports: [] + {{- else if $allowedNamespaces }} + - from: + - namespaceSelector: + matchLabels: + {{- toYaml $allowedNamespaces | nindent 14 }} + ports: [] + {{- else if $allowedPods }} + - from: + - podSelector: + matchLabels: + {{- toYaml $allowedPods | nindent 14 }} + ports: [] + {{- else }} + [] + {{- end }} + # All egress traffic is allowed + egress: + - {} + policyTypes: + - Ingress + - Egress +{{- end }} +{{- if and .Values.networkPolicy.enabled ($iamServer.enabled | default false) }} +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "versitygw.iamServerFullname" . }} + labels: + {{- include "versitygw.iamServerLabels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "versitygw.iamServerSelectorLabels" . | nindent 6 }} + # The in-chart gateway must always be able to reach the private IAM port. + # The configured peer selectors additionally control access to all IAM ports. + ingress: + {{- if .Values.iamServer.private.enabled }} + - from: + - podSelector: + matchLabels: + {{- include "versitygw.selectorLabels" . | nindent 14 }} + ports: + - protocol: TCP + port: iam-private + {{- end }} + {{- if and $allowedNamespaces $allowedPods }} + - from: + - namespaceSelector: + matchLabels: + {{- toYaml $allowedNamespaces | nindent 14 }} + podSelector: + matchLabels: + {{- toYaml $allowedPods | nindent 14 }} + ports: [] + {{- else if $allowedNamespaces }} + - from: + - namespaceSelector: + matchLabels: + {{- toYaml $allowedNamespaces | nindent 14 }} + ports: [] + {{- else if $allowedPods }} + - from: + - podSelector: + matchLabels: + {{- toYaml $allowedPods | nindent 14 }} + ports: [] + {{- else if not .Values.iamServer.private.enabled }} + [] + {{- end }} # All egress traffic is allowed egress: - {} diff --git a/chart/values.yaml b/chart/values.yaml index 94247795..74d258e6 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -34,6 +34,9 @@ serviceAccount: # Additional annotations for the pod podAnnotations: {} +# Additional annotations for the Deployment object (for example, a Secret +# reloader controller that restarts pods after credential/certificate rotation). +deploymentAnnotations: {} # Additional labels for the pod podLabels: {} @@ -42,8 +45,11 @@ podLabels: {} # For more details refer to https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ podSecurityContext: fsGroup: 1000 + seccompProfile: + type: RuntimeDefault securityContext: + allowPrivilegeEscalation: false capabilities: drop: - ALL @@ -159,7 +165,14 @@ gateway: readOnly: false # Silence stdout request logging (useful when using a dedicated access log). quiet: false - # Enable verbose debug output. + # Debug logger verbosity: "silent" (default, no debug output), "debug" (full + # request/response logging with secrets and tokens masked), or "unsafe" + # (full logging with NO masking -- prints access keys, secrets, session + # tokens, and signatures in the clear; only use for local troubleshooting, + # never in production). + logLevel: silent + # Deprecated: use logLevel: debug instead. Kept for backwards compatibility; + # only takes effect when logLevel is left at "silent". debug: false # Access log file path for S3 requests (AWS log format). Empty = disabled. accessLog: "" @@ -287,11 +300,17 @@ website: # Disable TLS for the website endpoint even when gateway TLS is enabled. noTls: false -# --- IAM (Identity and Access Management) --- +# --- IAM (Identity and Access Management) client, consumed by the S3 gateway --- iam: enabled: false - # TODO: implement other IAM backend - # Internal flat-file IAM is the default. For production, consider LDAP or Vault. + # TODO: implement other IAM backend (LDAP, Vault-direct, FreeIPA) + # - internal: flat-file IAM stored alongside backend data, inside the gateway pod. + # - standalone: delegate to a separate standalone IAM API service (an + # AWS-compatible IAM Query API server), reached over its private mTLS + # endpoint. See `iamServer` below to optionally deploy that service from + # this same chart, or point `iam.standalone.endpoint` at an externally + # managed one. + # Other values remain available for IAM backends configured through extraEnv. type: internal # Disable IAM result caching. cacheDisable: false @@ -299,9 +318,175 @@ iam: cacheTtl: 120 # IAM cache cleanup interval in seconds. cachePrune: 3600 - # Enable debug output for IAM operations. + # Enable IAM-specific debug output (independent of gateway.logLevel). debug: false + # --- standalone (iam.type: standalone) --- + standalone: + # Private endpoint of the standalone IAM service: a "host:port" TCP + # address (mTLS required -- see `certificate` below). Leave empty to + # auto-target the in-chart private `iamServer` Service + # (requires iamServer.enabled=true and iamServer.private.enabled=true). + endpoint: "" + # Identity this gateway signs its private-endpoint calls with. Both + # default to auth.accessKey/secretKey (root) when left empty. Literal + # values are retained for compatibility but are stored in Helm release + # history; prefer credentials.existingSecret for production. + access: "" + secret: "" + credentials: + # Optional Secret containing a distinct IAM client identity. Point this + # at iamServer.auth.existingSecret to use that server's root identity. + existingSecret: "" + accessKeyKey: rootAccessKeyId + secretKeyKey: rootSecretAccessKey + # POSIX uid/gid/project-id assigned to every account resolved through + # this backend (the standalone IAM service has no per-user POSIX + # identity concept of its own). + defaultUid: 0 + defaultGid: 0 + defaultProjectId: 0 + # mTLS client identity presented to the standalone IAM service's private + # endpoint. Required together with the peer trusting this certificate. + certificate: + # Auto-provision this client certificate via cert-manager. issuerRef + # must be a CA-type issuer (Issuer/ClusterIssuer of kind CA, or Vault) -- + # one that populates ca.crt in the resulting Secret -- and MUST be the + # same issuer used for iamServer.private.certificate, so each side's + # ca.crt can verify the other's certificate. + create: false + # Use an existing Secret instead (or as well as -- existingSecret wins + # when both are set). Must contain tls.crt, tls.key, and ca.crt (the CA + # that signed iamServer's private-listener server certificate). + existingSecret: "" + issuerRef: {} + # group: cert-manager.io + # kind: ClusterIssuer + # name: internal-ca + +# --- Standalone IAM API Server --- +# Deploys a separate `versitygw iam` server as its own Deployment and public/ +# private Services -- +# an AWS-compatible IAM Query API, independently scalable and centralized +# (unlike iam.type: internal, which runs file-backed IAM inside each gateway +# pod). Manage users/roles/policies against its public control-plane API +# (reusing the gateway root Secret by default, or `iamServer.auth`); point +# gateways at it via iam.type: standalone. +iamServer: + enabled: false + replicaCount: 1 + # Deployment strategy override for Vault storage. Internal file storage + # always uses Recreate to prevent concurrent writers and ReadWriteOnce + # multi-attach failures. Empty uses Kubernetes' default RollingUpdate for Vault. + strategy: {} + auth: + # Optional Secret containing rootAccessKeyId and rootSecretAccessKey. + # Empty reuses the top-level auth Secret for backward compatibility. + existingSecret: "" + # Optional image override; defaults to the top-level `image` values when unset. + image: + repository: "" + tag: "" + pullPolicy: "" + # Public control-plane API port (aws iam create-user, list-roles, etc.). + port: 7070 + maxConnections: 250000 + maxRequests: 100000 + # Silence stdout request logging. + quiet: false + # Debug logger verbosity: "silent" (default), "debug", or "unsafe" -- see + # gateway.logLevel for details. + logLevel: silent + # Reject CreateOpenIDConnectProvider requests that omit ThumbprintList + # instead of auto-fetching it over an outbound TLS connection to the + # caller-supplied URL. Recommended for restricted/air-gapped clusters. + disableOidcThumbprintAutofetch: false + # Optional TLS for the public control-plane API. No cert-manager automation + # here -- bring your own Secret (must contain tls.crt / tls.key). + tls: + enabled: false + secretName: "" + + # -- Storage backend -- + storage: + # internal (file-backed, requires persistence below) | vault + type: internal + vault: + endpointUrl: "" + namespace: "" + secretStoragePath: "" + secretStorageNamespace: "" + authMethod: "" + authNamespace: "" + mountPath: "" + roleId: "" + # Secret containing sensitive Vault auth material. Recognized keys + # (provide whichever your vault-auth-method needs): + # roleSecret -- AppRole secret ID (VGW_IAM_VAULT_ROLE_SECRET) + # rootToken -- Vault root token (VGW_IAM_VAULT_ROOT_TOKEN) + existingSecret: "" + # Separate Secret for Vault's TLS material -- kept distinct from + # `existingSecret` above (auth credentials) since these are a different + # security concern (transport trust vs. identity). Recognized keys + # (provide whichever your vault-endpoint-url/mTLS setup needs): + # serverCert -- CA cert verifying Vault's server certificate (VGW_IAM_VAULT_SERVER_CERT) + # clientCert -- client cert for mTLS to Vault (VGW_IAM_VAULT_CLIENT_CERT) + # clientCertKey -- private key for clientCert (VGW_IAM_VAULT_CLIENT_CERT_KEY) + # Ignored for any of the three that has a literal value below. + tlsExistingSecret: "" + # PEM-encoded certificate content (literal text, not a file path). + # Prefer tlsExistingSecret above for production; these are stored in + # Helm release history in plaintext. + serverCert: "" + clientCert: "" + clientCertKey: "" + + # -- Private mTLS endpoint -- + # Serves the derive-signing-key/evaluate-policy/resolve-identity calls that + # gateways configured with iam.type: standalone use internally. Required + # for any such gateway to reach this service over TCP. + private: + enabled: true + port: 7443 + certificate: + # Auto-provision the server certificate for this listener via + # cert-manager. issuerRef must be a CA-type issuer -- see + # iam.standalone.certificate for the matching client-side requirement. + create: false + # Use an existing Secret instead (or as well as -- existingSecret wins + # when both are set). Must contain tls.crt, tls.key, and ca.crt (the CA + # that signs gateway client certificates). + existingSecret: "" + issuerRef: {} + # group: cert-manager.io + # kind: ClusterIssuer + # name: internal-ca + # DNS names for the certificate. Defaults to the in-chart Service's + # names when left empty. + dnsNames: [] + + service: + # Type of the public control-plane Service. The private Service is always + # ClusterIP and is not exposed when this is NodePort or LoadBalancer. + type: ClusterIP + + # Storage for the standalone IAM service's own file-backed data (storage.type: internal). + persistence: + enabled: true + create: true + claimName: "" + size: 1Gi + storageClassName: "" + accessMode: ReadWriteOnce + + resources: {} + nodeSelector: {} + tolerations: [] + affinity: {} + deploymentAnnotations: {} + podAnnotations: {} + podLabels: {} + # Extra environment variables to pass to the container. # Use this to configure advanced options not covered above (e.g. event notifications, # LDAP IAM, S3 proxy backend credentials, StatsD metrics, etc.). @@ -389,9 +574,11 @@ topologySpreadConstraints: [] networkPolicy: enabled: false - # allowIngressFromNamespaces: - # matchLabels: - # kubernetes.io/metadata.name: kube-system - # allowIngressFromPods: - # matchLabels: - # svccontroller.k3s.cattle.io/svcname: versitygw + # Label maps used as matchLabels selectors. When both are set, a source pod + # must match both selectors. A pod-only selector applies to this release's + # namespace; an empty pair denies ingress to the gateway. The IAM policy + # always permits this release's gateway pods to reach its private mTLS port. + allowIngressFromNamespaces: {} + # kubernetes.io/metadata.name: kube-system + allowIngressFromPods: {} + # svccontroller.k3s.cattle.io/svcname: versitygw diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index b4523674..56976a0d 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -17,14 +17,14 @@ backend="${VGW_BACKEND:-}" if [ -z "$backend" ]; then cat >&2 <<'EOF' No command arguments were provided and VGW_BACKEND is unset. -Set VGW_BACKEND to one of: posix, scoutfs, s3, azure, plugin +Set VGW_BACKEND to one of: posix, scoutfs, s3, azure, plugin, iam or pass explicit arguments to the container to run the versitygw command directly. EOF exit 1 fi case "$backend" in - posix|scoutfs|s3|azure|plugin) + posix|scoutfs|s3|azure|plugin|iam) ;; *) echo "VGW_BACKEND invalid backend (was '$backend')." >&2