Files
versitygw/chart/templates/deployment.yaml
T
niksis02 abb3b27149 feat: add standalone IAM support in WebGUI
Gates bucket listing behind an identity policy, lets browsers reach the standalone IAM API, and turns the WebUI into a dashboard for S3, IAM, or both.

**Bucket listing.** `ListBuckets` is now gated by the new `s3:ListAllMyBuckets` action, evaluated against `arn:aws:s3:::*`. The request names no bucket, so only identity policies apply — there is no resource policy to combine with, which is the same shape `CreateBucket` already had, so both now share one identity-only evaluation path. Root and admin bypass it, and backends with no identity-policy layer keep listing as before since their listing is already narrowed to the caller's own buckets. The action is IAM-only and is deliberately absent from the bucket-policy action list.

**Fixed bucket ownership.** The standalone IAM client has no per-user ownership to express — accounts are all plain users, cannot be enumerated, and access is decided by policy rather than ACL — so it now implements `auth.FixedBucketOwner` and every bucket is owned by root. Bucket creation stops resolving an owner, `ListBuckets` returns every bucket to every caller (what they may then do with one stays a per-request policy decision), and the admin `ChangeBucketOwner` reports method-not-supported. Other IAM backends are untouched.

**IAM service CORS.** `--cors-allow-origin` now applies to the `iam` command: it answers preflights and stamps the CORS headers, mirroring back the requested method and headers rather than enumerating the SigV4 header set. Without it no browser can reach the IAM API at all, so setting `--webui` without it falls back to `*` with a warning. The chart gets `iamServer.corsAllowOrigin`.

**WebUI.** New IAM pages for users, roles and OIDC providers, signing IAM/STS query-form requests directly from the browser. Navigation is capability-gated rather than role-gated: on sign-in the session probes the S3, admin and IAM endpoints independently and each page shows only what those credentials actually reach, so one build serves an IAM-only dashboard, an S3-only dashboard, and a combined one. The login page takes an optional IAM endpoint, seeded from the new `--webui-iam-gateways` (chart: `webui.iamGateways`) — never auto-detected, since the IAM service is a separate process. The WebUI can also be hosted by `versitygw iam` itself, for deployments with no S3 gateway behind it.

**The admin API is ignored once an IAM endpoint is in play.** The IAM service is then the user directory and bucket ownership is fixed, which leaves the admin API no job: the session is given no admin endpoint at all, its login field is hidden, `users.html` redirects to its IAM counterpart, and every admin-only surface stays off screen. Dashboard and Buckets remain available to any S3 session in such a deployment, running on the S3 and IAM APIs alone and surfacing each denial per action instead of redirecting.

Also fixes two WebUI bugs: embedded assets went out with a zero modification time and no `Cache-Control`, so browsers treated them as fresh for centuries and an upgraded gateway served new HTML against stale JS — they now revalidate against an ETag; and the login page's advanced-options section clipped its last field, since it animated to a height named in the stylesheet rather than the one it measures now.

**Usage**

IAM-only dashboard, served by the IAM service:

    versitygw iam --port :7076 --webui :8080 --cors-allow-origin http://localhost:8080/

IAM + S3, dashboard served by the IAM service — point it at the gateway with `--webui-gateways`, and let the gateway accept the dashboard's origin:

    versitygw iam --port :7076 --webui :8080 --webui-gateways http://localhost:7070/ --cors-allow-origin http://localhost:8080/
    versitygw --port :7070 --cors-allow-origin http://localhost:8080/ posix /data

IAM + S3, dashboard served by the S3 gateway — point it at the IAM service with `--webui-iam-gateways`, and let the IAM service accept the dashboard's origin:

    versitygw --port :7070 --webui :8080 --webui-iam-gateways http://localhost:7076/ posix /data
    versitygw iam --port :7076 --cors-allow-origin http://localhost:8080/
2026-08-27 20:28:51 +04:00

387 lines
18 KiB
YAML

{{- $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 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) }}
{{- fail "persistence.claimName or persistence.hostPath is required when persistence.create is false" }}
{{- end }}
{{- if and .Values.persistence.claimName .Values.persistence.hostPath }}
{{- fail "only persistence.claimName or persistence.hostPath can be set" }}
{{- end }}
{{- if and .Values.persistence.create .Values.persistence.hostPath }}
{{- fail "persistence.create must be false for persistence.hostPath can be set" }}
{{- end }}
{{- if and .Values.gateway.backend.sidecarDir (ne .Values.gateway.backend.type "posix") }}
{{- fail "gateway.backend.sidecarDir is only supported with the posix backend" }}
{{- end }}
{{- 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:
matchLabels:
{{- include "versitygw.selectorLabels" . | nindent 6 }}
{{- with .Values.strategy }}
strategy:
{{- toYaml . | nindent 4 }}
{{- end }}
template:
metadata:
annotations:
{{- if not .Values.auth.existingSecret }}
checksum/credentials: {{ cat .Release.Name .Values.auth.accessKey .Values.auth.secretKey | sha256sum | trunc 12 }}
{{- end }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "versitygw.selectorLabels" . | nindent 8 }}
{{- with .Values.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: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
# Backend selection — consumed by docker-entrypoint.sh
- name: VGW_BACKEND
value: {{ .Values.gateway.backend.type | quote }}
- name: VGW_BACKEND_ARGS
value: {{ .Values.gateway.backend.args | quote }}
{{- if .Values.gateway.backend.sidecarDir }}
- name: VGW_META_SIDECAR
value: {{ .Values.gateway.backend.sidecarDir | quote }}
{{- end }}
{{- if .Values.gateway.backend.versioningDir }}
- name: VGW_VERSIONING_DIR
value: {{ .Values.gateway.backend.versioningDir | quote }}
{{- end }}
# Root credentials — sourced from a Kubernetes Secret
- name: ROOT_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: {{ include "versitygw.credentialsSecretName" . }}
key: rootAccessKeyId
- name: ROOT_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: {{ include "versitygw.credentialsSecretName" . }}
key: rootSecretAccessKey
# Gateway server settings
- name: VGW_PORT
value: ":{{ .Values.gateway.port }}"
- name: VGW_REGION
value: {{ .Values.gateway.region | quote }}
- name: VGW_HEALTH
value: "/_/health"
{{- if .Values.gateway.virtualDomain }}
- name: VGW_VIRTUAL_DOMAIN
value: {{ .Values.gateway.virtualDomain | quote }}
{{- end }}
{{- if .Values.gateway.readOnly }}
- name: VGW_READ_ONLY
value: "true"
{{- end }}
{{- if .Values.gateway.quiet }}
- name: VGW_QUIET
value: "true"
{{- end }}
{{- if ne $gatewayLogLevel "silent" }}
- name: VGW_LOG_LEVEL
value: {{ $gatewayLogLevel | quote }}
{{- else if .Values.gateway.debug }}
- name: VGW_LOG_LEVEL
value: "debug"
{{- end }}
{{- if .Values.gateway.accessLog }}
- name: VGW_ACCESS_LOG
value: {{ .Values.gateway.accessLog | quote }}
{{- end }}
{{- if .Values.gateway.corsAllowOrigin }}
- name: VGW_CORS_ALLOW_ORIGIN
value: {{ .Values.gateway.corsAllowOrigin | quote }}
{{- end }}
{{- if .Values.gateway.disableStrictBucketNames }}
- name: VGW_DISABLE_STRICT_BUCKET_NAMES
value: "true"
{{- end }}
# Admin API server
{{- if .Values.admin.enabled }}
- name: VGW_ADMIN_PORT
value: ":{{ .Values.admin.port }}"
- name: VGW_ADMIN_MAX_CONNECTIONS
value: {{ .Values.admin.maxConnections | quote }}
- name: VGW_ADMIN_MAX_REQUESTS
value: {{ .Values.admin.maxRequests | quote }}
{{- end }}
# WebUI
{{- if .Values.webui.enabled }}
- name: VGW_WEBUI_PORT
value: ":{{ .Values.webui.port }}"
{{- if .Values.webui.noTls }}
- name: VGW_WEBUI_NO_TLS
value: "true"
{{- end }}
{{- if .Values.webui.apiGateways }}
- name: VGW_WEBUI_GATEWAYS
value: {{ .Values.webui.apiGateways | join "," | quote }}
{{- end }}
{{- if .Values.webui.adminGateways }}
- name: VGW_WEBUI_ADMIN_GATEWAYS
value: {{ .Values.webui.adminGateways | join "," | quote }}
{{- end }}
{{- if .Values.webui.iamGateways }}
- name: VGW_WEBUI_IAM_GATEWAYS
value: {{ .Values.webui.iamGateways | join "," | quote }}
{{- end }}
{{- end }}
# Website Hosting
{{- if .Values.website.enabled }}
- name: VGW_WEBSITE_PORT
value: ":{{ .Values.website.port }}"
- name: VGW_WEBSITE_DOMAIN
value: {{ .Values.website.domain | quote }}
{{- if .Values.website.noTls }}
- name: VGW_WEBSITE_NO_TLS
value: "true"
{{- end }}
{{- end }}
{{- if .Values.iam.enabled }}
# IAM settings
{{- 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
value: "true"
{{- end }}
- name: VGW_IAM_CACHE_TTL
value: {{ .Values.iam.cacheTtl | quote }}
- name: VGW_IAM_CACHE_PRUNE
value: {{ .Values.iam.cachePrune | quote }}
{{- if .Values.iam.debug }}
- name: VGW_IAM_DEBUG
value: "true"
{{- end }}
{{- end }}
# TLS
{{- if .Values.tls.enabled }}
- name: VGW_CERT
value: /certs/tls.crt
- name: VGW_KEY
value: /certs/tls.key
{{- end }}
# Extra user-provided environment variables
{{- with .Values.extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
ports:
- name: s3-api
containerPort: {{ .Values.gateway.port }}
protocol: TCP
{{- if .Values.admin.enabled }}
- name: admin
containerPort: {{ .Values.admin.port }}
protocol: TCP
{{- end }}
{{- if .Values.webui.enabled }}
- name: webui
containerPort: {{ .Values.webui.port }}
protocol: TCP
{{- end }}
{{- if .Values.website.enabled }}
- name: website
containerPort: {{ .Values.website.port }}
protocol: TCP
{{- end }}
readinessProbe:
httpGet:
path: "/_/health"
port: s3-api
initialDelaySeconds: 5
periodSeconds: 15
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
terminationMessagePolicy: FallbackToLogsOnError
volumeMounts:
- name: data
mountPath: /mnt/data
subPath: data
readOnly: false
{{- if .Values.gateway.backend.sidecarDir }}
- name: data
mountPath: {{ .Values.gateway.backend.sidecarDir }}
subPath: metadata
readOnly: false
{{- end }}
{{- if .Values.gateway.backend.versioningDir }}
- name: data
mountPath: {{ .Values.gateway.backend.versioningDir }}
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
readOnly: true
{{- end }}
volumes:
- name: data
{{- if .Values.persistence.enabled }}
{{- if .Values.persistence.hostPath }}
hostPath:
path: {{ .Values.persistence.hostPath }}
type: DirectoryOrCreate
{{- else }}
persistentVolumeClaim:
claimName: {{ include "versitygw.pvcName" . }}
{{- end }}
{{- else }}
emptyDir: {}
{{- end }}
{{- if .Values.tls.enabled }}
- name: certificates
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:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.topologySpreadConstraints }}
topologySpreadConstraints:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}