diff --git a/stable/yopass/README.md.gotmpl b/stable/yopass/README.md.gotmpl index 14b86b20..2c1e85fd 100644 --- a/stable/yopass/README.md.gotmpl +++ b/stable/yopass/README.md.gotmpl @@ -82,3 +82,90 @@ memcached: {{ template "chart.requirementsSection" . }} {{ template "chart.valuesSection" . }} + +## Read-Only Mode & Split Deployments + +Read-only mode disables all secret creation endpoints while keeping retrieval fully functional. This chart can deploy an optional second instance in read-only mode within the same release. + +### Enable read-only instance + +```yaml +readOnly: + enabled: true + ingress: + enabled: true + hosts: + - host: yopass.example.com + paths: + - path: / + pathType: Prefix +``` + +Resources created for the read-only instance are suffixed with `-readonly` (Deployment, Service, Ingress). Both instances share the same database configured via `.Values.database.*`. + +In read-only mode the server is started with `--read-only`. Endpoint behavior: + +| Endpoint | Behavior | +|----------|----------| +| `POST /create/secret` | 404 Not Found | +| `POST /create/file` | 404 Not Found | +| `GET /secret/{key}` | Active | +| `GET /file/{key}` | Active | +| `DELETE /secret/{key}` | Active | + +The frontend detects `READ_ONLY: true` from the `/config` endpoint and shows a read-only landing page instead of the create form. + +### Split deployment pattern (one release) + +Deploy one Helm release with both instances sharing one database: + +```yaml +database: + type: memcached + dsn: memcached:11211 + +memcached: + enabled: true + +ingress: + enabled: true + hosts: + - host: internal.example.com + paths: + - path: / + pathType: Prefix + +readOnly: + enabled: true + ingress: + enabled: true + hosts: + - host: yopass.example.com + paths: + - path: / + pathType: Prefix +``` + +Both instances connect to the same Memcached or Redis database. Secrets created on the internal instance can be retrieved via the public read-only instance. + +### OIDC on internal, public read-only + +Require authentication only on the internal instance while the public instance remains open for retrieval: + +```yaml +extraEnvVariables: + REQUIRE_AUTH: "true" + OIDC_ISSUER: https://accounts.google.com + OIDC_CLIENT_ID: "123456789-abc.apps.googleusercontent.com" + OIDC_CLIENT_SECRET: "GOCSPX-..." + OIDC_REDIRECT_URL: https://internal.example.com/auth/callback + OIDC_ALLOWED_DOMAIN: example.com + +readOnly: + enabled: true + # no auth variables here +``` + +Notes: +- Deletion of one-time secrets happens automatically via `DELETE /secret/{key}` when a recipient opens a secret; this endpoint remains active in read-only mode intentionally. +- If you use a CDN or caching layer in front of the public instance, ensure retrieval endpoints (`GET /secret/*`, `GET /file/*`) are not cached. diff --git a/stable/yopass/ci/readonly-values.yaml b/stable/yopass/ci/readonly-values.yaml new file mode 100644 index 00000000..045ce03b --- /dev/null +++ b/stable/yopass/ci/readonly-values.yaml @@ -0,0 +1,25 @@ +--- +readOnly: + enabled: true + ingress: + enabled: true + hosts: + - host: yopass-public.example.com + paths: + - path: / + pathType: Prefix + +memcached: + enabled: true + +ingress: + enabled: true + hosts: + - host: yopass-internal.example.com + paths: + - path: / + pathType: Prefix + +resources: {} + +... diff --git a/stable/yopass/templates/deployment-readonly.yaml b/stable/yopass/templates/deployment-readonly.yaml new file mode 100644 index 00000000..9f7d34be --- /dev/null +++ b/stable/yopass/templates/deployment-readonly.yaml @@ -0,0 +1,142 @@ +{{- if .Values.readOnly.enabled }} +apiVersion: apps/v1 +kind: Deployment + +metadata: + name: {{ include "yopass.fullname" . }}-readonly + namespace: {{ .Release.Namespace }} + labels: + {{- include "yopass.labels" . | nindent 4 }} + +spec: + replicas: {{ default .Values.replicaCount .Values.readOnly.replicaCount }} + + selector: + matchLabels: + {{- include "yopass.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: readonly + + template: + metadata: + labels: + {{- include "yopass.labels" . | nindent 8 }} + app.kubernetes.io/component: readonly +{{- if .Values.annotations }} + annotations: + {{- toYaml .Values.annotations | nindent 8 }} +{{- end }} + + spec: + automountServiceAccountToken: {{ .Values.serviceAccount.automountToken }} + serviceAccountName: {{ include "yopass.serviceAccountName" . }} + {{- if .Values.image.pullSecrets }} + + imagePullSecrets: + {{- range .Values.image.pullSecrets }} + - name: {{ . }} + {{- end }} + {{- end }} + {{- with .Values.securityContext }} + + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.extraInitContainers }} + + initContainers: + {{- toYaml .Values.extraInitContainers | nindent 8 }} + {{- end }} + + containers: + - name: {{ .Chart.Name }} + image: {{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + + args: + - --address=0.0.0.0 + - --port={{ default .Values.service.internalPort .Values.readOnly.service.internalPort }} + {{- if .Values.metrics.enabled }} + - --metrics-port={{ .Values.metrics.internalPort }} + {{- end }} + - --database={{ .Values.database.type }} + {{- if eq .Values.database.type "redis" }} + - --redis={{ .Values.database.dsn }} + {{- end }} + {{- if eq .Values.database.type "memcached" }} + - --memcached={{ .Values.database.dsn }} + {{- end }} + - --max-length={{ .Values.config.maxLength }} + - --read-only + {{- with .Values.podSecurityContext }} + + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if or .Values.readOnly.extraEnvSecrets .Values.readOnly.extraEnvVariables }} + + env: + {{- range $key, $value := .Values.readOnly.extraEnvSecrets }} + - name: {{ $key }} + valueFrom: + secretKeyRef: + name: {{ required "Must specify secret!" $value.secret }} + key: {{ required "Must specify key!" $value.key }} + {{- end }} + {{- range $key, $value := .Values.readOnly.extraEnvVariables }} + - name: {{ $key }} + value: "{{ $value }}" + {{- end }} + {{- end }} + {{- if or .Values.readOnly.envFromSecrets .Values.readOnly.envFromConfigMaps }} + + envFrom: + {{- range $name := .Values.readOnly.envFromSecrets }} + - secretRef: + name: {{ $name }} + {{- end }} + {{- range $name := .Values.readOnly.envFromConfigMaps }} + - configMapRef: + name: {{ $name }} + {{- end }} + {{- end }} + + ports: + - name: http + containerPort: {{ default .Values.service.internalPort .Values.readOnly.service.internalPort }} + protocol: TCP + {{- if .Values.metrics.enabled }} + - name: metrics + containerPort: {{ .Values.metrics.internalPort }} + protocol: TCP + {{- end }} + + livenessProbe: + httpGet: + path: / + port: http + + readinessProbe: + httpGet: + path: / + port: http + {{- if (default .Values.resources .Values.readOnly.resources) }} + + resources: + {{- toYaml (default .Values.resources .Values.readOnly.resources) | nindent 12 }} + {{- end }} + {{- if (default .Values.nodeSelector .Values.readOnly.nodeSelector) }} + + nodeSelector: + {{- toYaml (default .Values.nodeSelector .Values.readOnly.nodeSelector) | nindent 8 }} + {{- end }} + {{- with (default .Values.affinity .Values.readOnly.affinity) }} + + affinity: + {{ toYaml . | nindent 8 }} + {{- end }} + {{- with (default .Values.tolerations .Values.readOnly.tolerations) }} + + tolerations: + {{ toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/stable/yopass/templates/deployment.yaml b/stable/yopass/templates/deployment.yaml index 91f96a05..6b14e3af 100644 --- a/stable/yopass/templates/deployment.yaml +++ b/stable/yopass/templates/deployment.yaml @@ -22,11 +22,13 @@ spec: selector: matchLabels: {{- include "yopass.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: internal template: metadata: labels: {{- include "yopass.labels" . | nindent 8 }} + app.kubernetes.io/component: internal {{- if .Values.annotations }} annotations: {{- toYaml .Values.annotations | nindent 8 }} diff --git a/stable/yopass/templates/ingress-readonly.yaml b/stable/yopass/templates/ingress-readonly.yaml new file mode 100644 index 00000000..41caa91b --- /dev/null +++ b/stable/yopass/templates/ingress-readonly.yaml @@ -0,0 +1,50 @@ +{{- if and .Values.readOnly.enabled .Values.readOnly.ingress.enabled -}} +{{- $serviceName := print (include "yopass.fullname" .) "-readonly" }} +apiVersion: networking.k8s.io/v1 +kind: Ingress + +metadata: + name: {{ include "yopass.fullname" . }}-readonly + namespace: {{ .Release.Namespace }} + labels: + {{- include "yopass.labels" . | nindent 4 }} +{{- if .Values.readOnly.ingress.labels }} + {{- toYaml .Values.readOnly.ingress.labels | nindent 4 }} +{{- end }} +{{- if .Values.readOnly.ingress.annotations }} + annotations: + {{- toYaml .Values.readOnly.ingress.annotations | nindent 4 }} +{{- end }} + +spec: + {{- if .Values.readOnly.ingress.className }} + ingressClassName: {{ .Values.readOnly.ingress.className }} + {{- end }} + {{- if .Values.readOnly.ingress.tls }} + tls: + {{- range .Values.readOnly.ingress.tls }} + - secretName: {{ .secretName }} + {{- if .hosts }} + hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + {{- end }} + {{- end }} + {{- end }} + rules: + {{- range .Values.readOnly.ingress.hosts }} + - host: {{ .host }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ $serviceName }} + port: + name: http + {{- end }} + {{- end }} +{{- end }} diff --git a/stable/yopass/templates/service-readonly.yaml b/stable/yopass/templates/service-readonly.yaml new file mode 100644 index 00000000..ea04cd52 --- /dev/null +++ b/stable/yopass/templates/service-readonly.yaml @@ -0,0 +1,36 @@ +{{- if and .Values.readOnly.enabled .Values.readOnly.service.enabled }} +apiVersion: v1 +kind: Service + +metadata: + name: {{ template "yopass.fullname" . }}-readonly + namespace: {{ .Release.Namespace }} + labels: + {{- include "yopass.labels" . | nindent 4 }} +{{- if .Values.readOnly.service.labels }} + {{- toYaml .Values.readOnly.service.labels | nindent 4 }} +{{- end }} +{{- if .Values.readOnly.service.annotations }} + annotations: + {{- toYaml .Values.readOnly.service.annotations | nindent 4 }} +{{- end }} + +spec: + type: {{ default .Values.service.type .Values.readOnly.service.type }} + + ports: + - name: http + port: {{ default .Values.service.port .Values.readOnly.service.port }} + targetPort: http + protocol: TCP + {{- if .Values.metrics.enabled }} + - name: metrics + port: {{ .Values.metrics.port }} + targetPort: metrics + protocol: TCP + {{- end }} + + selector: + {{- include "yopass.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: readonly +{{- end }} diff --git a/stable/yopass/templates/service.yaml b/stable/yopass/templates/service.yaml index a26bcb70..3c3814fa 100644 --- a/stable/yopass/templates/service.yaml +++ b/stable/yopass/templates/service.yaml @@ -31,3 +31,4 @@ spec: selector: {{- include "yopass.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: internal diff --git a/stable/yopass/values.yaml b/stable/yopass/values.yaml index ed12c97a..1f708a99 100644 --- a/stable/yopass/values.yaml +++ b/stable/yopass/values.yaml @@ -206,6 +206,80 @@ config: # -- (int) Max length of encrypted secret maxLength: 10000 +# -- Optional second deployment in read-only mode +readOnly: + # -- Enable rendering a second, read-only instance (resources suffixed with -readonly) + enabled: false + + # -- (int) Replicas for the read-only deployment (defaults to .Values.replicaCount) + replicaCount: + + # -- List of environment variables from existing secrets for read-only instance + envFromSecrets: [] + + # -- List of environment variables from existing configmaps for read-only instance + envFromConfigMaps: [] + + # -- Extra environment variables for read-only instance + extraEnvVariables: {} + + # -- Extra environment variables from secrets for read-only instance + extraEnvSecrets: {} + + # -- Resources for the read-only deployment (defaults to .Values.resources) + resources: {} + + # -- Node selector for the read-only deployment (defaults to .Values.nodeSelector) + nodeSelector: {} + + # -- Affinity for the read-only deployment (defaults to .Values.affinity) + affinity: {} + + # -- Tolerations for the read-only deployment (defaults to .Values.tolerations) + tolerations: [] + + service: + # -- Enable Service for read-only instance + enabled: true + + # -- Type of the read-only service (defaults to .Values.service.type) + type: + + # -- (int) Port of the read-only service (defaults to .Values.service.port) + port: + + # -- (int) Internal port of the read-only service (defaults to .Values.service.internalPort) + internalPort: + + # -- Additional annotations for the read-only service + annotations: {} + + # -- Additional labels for the read-only service + labels: {} + + ingress: + # -- Enable ingress for read-only instance + enabled: false + + # -- (string) Class name for the ingress resource + className: + + # -- Host definition for read-only ingress + hosts: + - host: example.local + paths: + - path: / + pathType: Prefix + + # -- Optional TLS configuration for read-only ingress + tls: [] + + # -- Additional annotations for the read-only ingress + annotations: {} + + # -- Additional labels for the read-only ingress + labels: {} + # -- Resources for the deployment resources: limits: {}