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