From e936e0f7ede58d46e47ced7b39cd79f2130fb73e Mon Sep 17 00:00:00 2001 From: Ney Walens De Mesquita Date: Fri, 17 Dec 2021 19:43:55 +0100 Subject: [PATCH] Chart: Add support for securityContext (#18249) This adds the ability to set both a global `securityContext` and `securityContext` by deployment, allowing for greater flexibility in configuring how Airflow is run. --- .../pod-template-file.kubernetes-helm-yaml | 5 +- chart/templates/_helpers.yaml | 84 +++++++- chart/templates/cleanup/cleanup-cronjob.yaml | 2 + chart/templates/flower/flower-deployment.yaml | 4 +- chart/templates/jobs/create-user-job.yaml | 4 +- .../templates/jobs/migrate-database-job.yaml | 4 +- .../scheduler/scheduler-deployment.yaml | 5 +- chart/templates/statsd/statsd-deployment.yaml | 4 +- .../triggerer/triggerer-deployment.yaml | 5 +- .../webserver/webserver-deployment.yaml | 5 +- .../templates/workers/worker-deployment.yaml | 7 +- chart/tests/test_security_context.py | 200 ++++++++++++++++++ chart/values.schema.json | 143 +++++++++++++ chart/values.yaml | 65 ++++++ docs/helm-chart/production-guide.rst | 127 +++++++++++ 15 files changed, 638 insertions(+), 26 deletions(-) create mode 100644 chart/tests/test_security_context.py diff --git a/chart/files/pod-template-file.kubernetes-helm-yaml b/chart/files/pod-template-file.kubernetes-helm-yaml index cc04d5739e10..57386c62bfed 100644 --- a/chart/files/pod-template-file.kubernetes-helm-yaml +++ b/chart/files/pod-template-file.kubernetes-helm-yaml @@ -18,6 +18,7 @@ {{- $nodeSelector := or .Values.nodeSelector .Values.workers.nodeSelector }} {{- $affinity := or .Values.affinity .Values.workers.affinity }} {{- $tolerations := or .Values.tolerations .Values.workers.tolerations }} +{{- $securityContext := include "airflowSecurityContext" (list . .Values.workers) }} apiVersion: v1 kind: Pod metadata: @@ -83,9 +84,7 @@ spec: - name: {{ template "registry_secret" . }} {{- end }} restartPolicy: Never - securityContext: - runAsUser: {{ .Values.uid }} - fsGroup: {{ .Values.gid }} + securityContext: {{ $securityContext | nindent 4 }} nodeSelector: {{ toYaml $nodeSelector | nindent 4 }} affinity: {{ toYaml $affinity | nindent 4 }} tolerations: {{ toYaml $tolerations | nindent 4 }} diff --git a/chart/templates/_helpers.yaml b/chart/templates/_helpers.yaml index 52eca789c218..424c6ab55cda 100644 --- a/chart/templates/_helpers.yaml +++ b/chart/templates/_helpers.yaml @@ -143,8 +143,7 @@ If release name contains chart name it will be used as a full name. - name: {{ .Values.dags.gitSync.containerName }}{{ if .is_init }}-init{{ end }} image: {{ template "git_sync_image" . }} imagePullPolicy: {{ .Values.images.gitSync.pullPolicy }} - securityContext: - runAsUser: {{ .Values.dags.gitSync.uid }} + securityContext: {{ include "localSecurityContext" .Values.dags.gitSync | nindent 4 }} env: {{- if .Values.dags.gitSync.sshKeySecret }} - name: GIT_SSH_KEY_FILE @@ -616,3 +615,84 @@ Create the name of the cleanup service account to use {{- end -}} {{- $kubeVersion -}} {{- end -}} + +{{/* +Set the default value for securityContext +If no value is passed for securityContext or .securityContext, defaults to global uid and gid. + + +------------------------+ +-----------------+ +-------------------------+ + | .securityContext | -> | securityContext | -> | Values.uid + Values.gid | + +------------------------+ +-----------------+ +-------------------------+ + +Values are not accumulated meaning that if runAsUser is set to 10 in .securityContext, +any extra values set to securityContext or uid+gid will be ignored. + +The template can be called like so: + include "airflowSecurityContext" (list . .Values.webserver) + +Where `.` is the global variables scope and `.Values.webserver` the local variables scope for the webserver template. +*/}} +{{- define "airflowSecurityContext" -}} + {{- $ := index . 0 -}} + {{- with index . 1 }} + {{- if .securityContext -}} +{{ toYaml .securityContext | print }} + {{- else if $.Values.securityContext -}} +{{ toYaml $.Values.securityContext | print }} + {{- else -}} +runAsUser: {{ $.Values.uid }} +fsGroup: {{ $.Values.gid }} + {{- end -}} + {{- end -}} +{{- end -}} + +{{/* +Set the default value for securityContext +If no value is passed for securityContext or .securityContext, defaults to UID in the local node. + + +------------------------+ +-------------+ + | .securityContext | > | .uid | + +------------------------+ +-------------+ + +The template can be called like so: + include "localSecurityContext" .Values.statsd + +It is important to pass the local variables scope to this template as it is used to determine the local node value for uid. +*/}} +{{- define "localSecurityContext" -}} + {{- if .securityContext -}} +{{ toYaml .securityContext | print }} + {{- else -}} +runAsUser: {{ .uid }} + {{- end -}} +{{- end -}} + +{{/* +Set the default value for workers chown for persistent storage +If no value is passed for securityContext or .securityContext, defaults to global uid and gid. +The template looks for `runAsUser` and `fsGroup` specifically, any other parameter will be ignored. + + +------------------------+ +-----------------+ +-------------------------+ + | .securityContext | -> | securityContext | -> | Values.uid + Values.gid | + +------------------------+ +-----------------+ +-------------------------+ + +Values are not accumulated meaning that if runAsUser is set to 10 in .securityContext, +any extra values set to securityContext or uid+gid will be ignored. + +The template can be called like so: + include "airflowSecurityContextIds" (list . .Values.workers) + +Where `.` is the global variables scope and `.Values.workers` the local variables scope for the workers template. +*/}} +{{- define "airflowSecurityContextIds" -}} + {{- $ := index . 0 -}} + {{- with index . 1 }} + {{- if .securityContext -}} +{{ pluck "runAsUser" .securityContext | first | default $.Values.uid }}:{{ pluck "fsGroup" .securityContext | first | default $.Values.gid }} + {{- else if $.Values.securityContext -}} +{{ pluck "runAsUser" $.Values.securityContext | first | default $.Values.uid }}:{{ pluck "fsGroup" $.Values.securityContext | first | default $.Values.gid }} + {{- else -}} +{{ $.Values.uid }}:{{ $.Values.gid }} + {{- end -}} + {{- end -}} +{{- end -}} diff --git a/chart/templates/cleanup/cleanup-cronjob.yaml b/chart/templates/cleanup/cleanup-cronjob.yaml index c9bbbfdde88d..402932dfec54 100644 --- a/chart/templates/cleanup/cleanup-cronjob.yaml +++ b/chart/templates/cleanup/cleanup-cronjob.yaml @@ -22,6 +22,7 @@ {{- $nodeSelector := or .Values.cleanup.nodeSelector .Values.nodeSelector }} {{- $affinity := or .Values.cleanup.affinity .Values.affinity }} {{- $tolerations := or .Values.cleanup.tolerations .Values.tolerations }} +{{- $securityContext := include "airflowSecurityContext" (list . .Values.cleanup) }} {{- if semverCompare ">= 1.21.x" (include "kubeVersion" .) }} apiVersion: batch/v1 {{- else }} @@ -70,6 +71,7 @@ spec: imagePullSecrets: - name: {{ template "registry_secret" . }} {{- end }} + securityContext: {{ $securityContext | nindent 12 }} containers: - name: airflow-cleanup-pods image: {{ template "airflow_image" . }} diff --git a/chart/templates/flower/flower-deployment.yaml b/chart/templates/flower/flower-deployment.yaml index 2c40ee1d4f5f..9e9244d64c67 100644 --- a/chart/templates/flower/flower-deployment.yaml +++ b/chart/templates/flower/flower-deployment.yaml @@ -23,6 +23,7 @@ {{- $nodeSelector := or .Values.flower.nodeSelector .Values.nodeSelector }} {{- $affinity := or .Values.flower.affinity .Values.affinity }} {{- $tolerations := or .Values.flower.tolerations .Values.tolerations }} +{{- $securityContext := include "airflowSecurityContext" (list . .Values.flower) }} kind: Deployment apiVersion: apps/v1 metadata: @@ -67,8 +68,7 @@ spec: {{ toYaml $tolerations | indent 8 }} serviceAccountName: {{ include "flower.serviceAccountName" . }} restartPolicy: Always - securityContext: - runAsUser: {{ .Values.uid }} + securityContext: {{ $securityContext | nindent 8 }} {{- if or .Values.registry.secretName .Values.registry.connection }} imagePullSecrets: - name: {{ template "registry_secret" . }} diff --git a/chart/templates/jobs/create-user-job.yaml b/chart/templates/jobs/create-user-job.yaml index a5e5df08379f..9a2582abb923 100644 --- a/chart/templates/jobs/create-user-job.yaml +++ b/chart/templates/jobs/create-user-job.yaml @@ -22,6 +22,7 @@ {{- $nodeSelector := or .Values.createUserJob.nodeSelector .Values.nodeSelector }} {{- $affinity := or .Values.createUserJob.affinity .Values.affinity }} {{- $tolerations := or .Values.createUserJob.tolerations .Values.tolerations }} +{{- $securityContext := include "airflowSecurityContext" (list . .Values.createUserJob) }} apiVersion: batch/v1 kind: Job metadata: @@ -65,8 +66,7 @@ spec: {{- end }} {{- end }} spec: - securityContext: - runAsUser: {{ .Values.uid }} + securityContext: {{ $securityContext | nindent 8 }} restartPolicy: OnFailure nodeSelector: {{ toYaml $nodeSelector | indent 8 }} diff --git a/chart/templates/jobs/migrate-database-job.yaml b/chart/templates/jobs/migrate-database-job.yaml index 859b045bbf00..0362f200bac5 100644 --- a/chart/templates/jobs/migrate-database-job.yaml +++ b/chart/templates/jobs/migrate-database-job.yaml @@ -21,6 +21,7 @@ {{- $nodeSelector := or .Values.migrateDatabaseJob.nodeSelector .Values.nodeSelector }} {{- $affinity := or .Values.migrateDatabaseJob.affinity .Values.affinity }} {{- $tolerations := or .Values.migrateDatabaseJob.tolerations .Values.tolerations }} +{{- $securityContext := include "airflowSecurityContext" (list . .Values.migrateDatabaseJob) }} apiVersion: batch/v1 kind: Job metadata: @@ -64,8 +65,7 @@ spec: {{- end }} {{- end }} spec: - securityContext: - runAsUser: {{ .Values.uid }} + securityContext: {{ $securityContext | nindent 8 }} restartPolicy: OnFailure nodeSelector: {{ toYaml $nodeSelector | indent 8 }} diff --git a/chart/templates/scheduler/scheduler-deployment.yaml b/chart/templates/scheduler/scheduler-deployment.yaml index a73c6b90e4ce..d7a5e1a0ba50 100644 --- a/chart/templates/scheduler/scheduler-deployment.yaml +++ b/chart/templates/scheduler/scheduler-deployment.yaml @@ -31,6 +31,7 @@ {{- $nodeSelector := or .Values.scheduler.nodeSelector .Values.nodeSelector }} {{- $affinity := or .Values.scheduler.affinity .Values.affinity }} {{- $tolerations := or .Values.scheduler.tolerations .Values.tolerations }} +{{- $securityContext := include "airflowSecurityContext" (list . .Values.scheduler) }} kind: {{ if $stateful }}StatefulSet{{ else }}Deployment{{ end }} apiVersion: apps/v1 @@ -98,9 +99,7 @@ spec: restartPolicy: Always terminationGracePeriodSeconds: 10 serviceAccountName: {{ include "scheduler.serviceAccountName" . }} - securityContext: - runAsUser: {{ .Values.uid }} - fsGroup: {{ .Values.gid }} + securityContext: {{ $securityContext | nindent 8 }} {{- if or .Values.registry.secretName .Values.registry.connection }} imagePullSecrets: - name: {{ template "registry_secret" . }} diff --git a/chart/templates/statsd/statsd-deployment.yaml b/chart/templates/statsd/statsd-deployment.yaml index e14e224e66ae..670dc53c456a 100644 --- a/chart/templates/statsd/statsd-deployment.yaml +++ b/chart/templates/statsd/statsd-deployment.yaml @@ -22,6 +22,7 @@ {{- $nodeSelector := or .Values.statsd.nodeSelector .Values.nodeSelector }} {{- $affinity := or .Values.statsd.affinity .Values.affinity }} {{- $tolerations := or .Values.statsd.tolerations .Values.tolerations }} +{{- $securityContext := include "localSecurityContext" .Values.statsd }} kind: Deployment apiVersion: apps/v1 metadata: @@ -63,8 +64,7 @@ spec: tolerations: {{ toYaml $tolerations | indent 8 }} serviceAccountName: {{ include "statsd.serviceAccountName" . }} - securityContext: - runAsUser: {{ .Values.statsd.uid }} + securityContext: {{ $securityContext | nindent 8 }} restartPolicy: Always {{- if or .Values.registry.secretName .Values.registry.connection }} imagePullSecrets: diff --git a/chart/templates/triggerer/triggerer-deployment.yaml b/chart/templates/triggerer/triggerer-deployment.yaml index 2591640a91eb..11ccdc6f8d65 100644 --- a/chart/templates/triggerer/triggerer-deployment.yaml +++ b/chart/templates/triggerer/triggerer-deployment.yaml @@ -23,6 +23,7 @@ {{- $nodeSelector := or .Values.nodeSelector .Values.triggerer.nodeSelector }} {{- $affinity := or .Values.affinity .Values.triggerer.affinity }} {{- $tolerations := or .Values.tolerations .Values.triggerer.tolerations }} +{{- $securityContext := include "airflowSecurityContext" (list . .Values.triggerer) }} kind: Deployment apiVersion: apps/v1 metadata: @@ -81,9 +82,7 @@ spec: terminationGracePeriodSeconds: {{ .Values.triggerer.terminationGracePeriodSeconds }} restartPolicy: Always serviceAccountName: {{ include "triggerer.serviceAccountName" . }} - securityContext: - runAsUser: {{ .Values.uid }} - fsGroup: {{ .Values.gid }} + securityContext: {{ $securityContext | nindent 8 }} {{- if or .Values.registry.secretName .Values.registry.connection }} imagePullSecrets: - name: {{ template "registry_secret" . }} diff --git a/chart/templates/webserver/webserver-deployment.yaml b/chart/templates/webserver/webserver-deployment.yaml index 4d76cd5ccce0..8aeb91bb04c5 100644 --- a/chart/templates/webserver/webserver-deployment.yaml +++ b/chart/templates/webserver/webserver-deployment.yaml @@ -21,6 +21,7 @@ {{- $nodeSelector := or .Values.webserver.nodeSelector .Values.nodeSelector }} {{- $affinity := or .Values.webserver.affinity .Values.affinity }} {{- $tolerations := or .Values.webserver.tolerations .Values.tolerations }} +{{- $securityContext := include "airflowSecurityContext" (list . .Values.webserver) }} kind: Deployment apiVersion: apps/v1 metadata: @@ -93,9 +94,7 @@ spec: tolerations: {{ toYaml $tolerations | indent 8 }} restartPolicy: Always - securityContext: - runAsUser: {{ .Values.uid }} - fsGroup: {{ .Values.gid }} + securityContext: {{ $securityContext | nindent 8 }} {{- if or .Values.registry.secretName .Values.registry.connection }} imagePullSecrets: - name: {{ template "registry_secret" . }} diff --git a/chart/templates/workers/worker-deployment.yaml b/chart/templates/workers/worker-deployment.yaml index e00ff7c9f9dc..38c50ceede95 100644 --- a/chart/templates/workers/worker-deployment.yaml +++ b/chart/templates/workers/worker-deployment.yaml @@ -23,6 +23,7 @@ {{- $nodeSelector := or .Values.nodeSelector .Values.workers.nodeSelector }} {{- $affinity := or .Values.affinity .Values.workers.affinity }} {{- $tolerations := or .Values.tolerations .Values.workers.tolerations }} +{{- $securityContext := include "airflowSecurityContext" (list . .Values.workers) }} kind: {{ if $persistence }}StatefulSet{{ else }}Deployment{{ end }} apiVersion: apps/v1 metadata: @@ -95,9 +96,7 @@ spec: terminationGracePeriodSeconds: {{ .Values.workers.terminationGracePeriodSeconds }} restartPolicy: Always serviceAccountName: {{ include "worker.serviceAccountName" . }} - securityContext: - runAsUser: {{ .Values.uid }} - fsGroup: {{ .Values.gid }} + securityContext: {{ $securityContext | nindent 8 }} {{- if or .Values.registry.secretName .Values.registry.connection }} imagePullSecrets: - name: {{ template "registry_secret" . }} @@ -112,7 +111,7 @@ spec: command: - chown - -R - - "{{ .Values.uid }}:{{ .Values.gid }}" + - "{{ include "airflowSecurityContextIds" (list . .Values.workers) }}" - {{ template "airflow_logs" . }} securityContext: runAsUser: 0 diff --git a/chart/tests/test_security_context.py b/chart/tests/test_security_context.py new file mode 100644 index 000000000000..d40e85647ad2 --- /dev/null +++ b/chart/tests/test_security_context.py @@ -0,0 +1,200 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import jmespath + +from tests.helm_template_generator import render_chart + + +class TestSCBackwardsCompatibility: + def test_check_deployments_and_jobs(self): + docs = render_chart( + values={ + "uid": 3000, + "gid": 30, + "webserver": {"defaultUser": {"enabled": True}}, + "flower": {"enabled": True}, + "airflowVersion": "2.2.0", + "executor": "CeleryKubernetesExecutor", + }, + show_only=[ + "templates/flower/flower-deployment.yaml", + "templates/scheduler/scheduler-deployment.yaml", + "templates/triggerer/triggerer-deployment.yaml", + "templates/webserver/webserver-deployment.yaml", + "templates/workers/worker-deployment.yaml", + "templates/jobs/create-user-job.yaml", + "templates/jobs/migrate-database-job.yaml", + ], + ) + + for index in range(len(docs)): + assert 3000 == jmespath.search("spec.template.spec.securityContext.runAsUser", docs[index]) + assert 30 == jmespath.search("spec.template.spec.securityContext.fsGroup", docs[index]) + + def test_check_statsd_uid(self): + docs = render_chart( + values={"statsd": {"enabled": True, "uid": 3000}}, + show_only=["templates/statsd/statsd-deployment.yaml"], + ) + + assert 3000 == jmespath.search("spec.template.spec.securityContext.runAsUser", docs[0]) + + def test_check_cleanup_job(self): + docs = render_chart( + values={"uid": 3000, "gid": 30, "cleanup": {"enabled": True}}, + show_only=["templates/cleanup/cleanup-cronjob.yaml"], + ) + + assert 3000 == jmespath.search( + "spec.jobTemplate.spec.template.spec.securityContext.runAsUser", docs[0] + ) + assert 30 == jmespath.search("spec.jobTemplate.spec.template.spec.securityContext.fsGroup", docs[0]) + + def test_gitsync_sidecar_and_init_container(self): + docs = render_chart( + values={ + "dags": {"gitSync": {"enabled": True, "uid": 3000}}, + "airflowVersion": "1.10.15", + }, + show_only=[ + "templates/workers/worker-deployment.yaml", + "templates/webserver/webserver-deployment.yaml", + "templates/scheduler/scheduler-deployment.yaml", + ], + ) + + for index in range(len(docs)): + assert "git-sync" in [ + c["name"] for c in jmespath.search("spec.template.spec.containers", docs[index]) + ] + assert "git-sync-init" in [ + c["name"] for c in jmespath.search("spec.template.spec.initContainers", docs[index]) + ] + assert 3000 == jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'].securityContext.runAsUser | [0]", + docs[index], + ) + assert 3000 == jmespath.search( + "spec.template.spec.containers[?name=='git-sync'].securityContext.runAsUser | [0]", + docs[index], + ) + + +class TestSecurityContext: + # Test securityContext setting for Pods and Containers + def test_check_default_setting(self): + docs = render_chart( + values={ + "securityContext": {"runAsUser": 6000, "fsGroup": 60}, + "webserver": {"defaultUser": {"enabled": True}}, + "flower": {"enabled": True}, + "statsd": {"enabled": False}, + "airflowVersion": "2.2.0", + "executor": "CeleryKubernetesExecutor", + }, + show_only=[ + "templates/flower/flower-deployment.yaml", + "templates/scheduler/scheduler-deployment.yaml", + "templates/triggerer/triggerer-deployment.yaml", + "templates/webserver/webserver-deployment.yaml", + "templates/workers/worker-deployment.yaml", + "templates/jobs/create-user-job.yaml", + "templates/jobs/migrate-database-job.yaml", + ], + ) + + for index in range(len(docs)): + print(docs[index]) + assert 6000 == jmespath.search("spec.template.spec.securityContext.runAsUser", docs[index]) + assert 60 == jmespath.search("spec.template.spec.securityContext.fsGroup", docs[index]) + + # Test priority: + # .securityContext > securityContext > uid + gid + def test_check_local_setting(self): + component_contexts = {"securityContext": {"runAsUser": 9000, "fsGroup": 90}} + docs = render_chart( + values={ + "uid": 3000, + "gid": 30, + "securityContext": {"runAsUser": 6000, "fsGroup": 60}, + "webserver": {"defaultUser": {"enabled": True}, **component_contexts}, + "workers": {**component_contexts}, + "flower": {"enabled": True, **component_contexts}, + "scheduler": {**component_contexts}, + "createUserJob": {**component_contexts}, + "migrateDatabaseJob": {**component_contexts}, + "triggerer": {**component_contexts}, + "statsd": {"enabled": True, **component_contexts}, + "airflowVersion": "2.2.0", + "executor": "CeleryKubernetesExecutor", + }, + show_only=[ + "templates/flower/flower-deployment.yaml", + "templates/scheduler/scheduler-deployment.yaml", + "templates/triggerer/triggerer-deployment.yaml", + "templates/webserver/webserver-deployment.yaml", + "templates/workers/worker-deployment.yaml", + "templates/jobs/create-user-job.yaml", + "templates/jobs/migrate-database-job.yaml", + "templates/statsd/statsd-deployment.yaml", + ], + ) + + for index in range(len(docs)): + print(docs[index]) + assert 9000 == jmespath.search("spec.template.spec.securityContext.runAsUser", docs[index]) + assert 90 == jmespath.search("spec.template.spec.securityContext.fsGroup", docs[index]) + + # Test containerSecurity priority over uid under statsd + def test_check_statsd_uid(self): + docs = render_chart( + values={"statsd": {"enabled": True, "uid": 3000, "securityContext": {"runAsUser": 7000}}}, + show_only=["templates/statsd/statsd-deployment.yaml"], + ) + + assert 7000 == jmespath.search("spec.template.spec.securityContext.runAsUser", docs[0]) + + # Test containerSecurity priority over uid under dags.gitSync + def test_gitsync_sidecar_and_init_container(self): + docs = render_chart( + values={ + "dags": {"gitSync": {"enabled": True, "uid": 9000, "securityContext": {"runAsUser": 8000}}}, + "airflowVersion": "1.10.15", + }, + show_only=[ + "templates/workers/worker-deployment.yaml", + "templates/webserver/webserver-deployment.yaml", + "templates/scheduler/scheduler-deployment.yaml", + ], + ) + + for index in range(len(docs)): + assert "git-sync" in [ + c["name"] for c in jmespath.search("spec.template.spec.containers", docs[index]) + ] + assert "git-sync-init" in [ + c["name"] for c in jmespath.search("spec.template.spec.initContainers", docs[index]) + ] + assert 8000 == jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'].securityContext.runAsUser | [0]", + docs[index], + ) + assert 8000 == jmespath.search( + "spec.template.spec.containers[?name=='git-sync'].securityContext.runAsUser | [0]", + docs[index], + ) diff --git a/chart/values.schema.json b/chart/values.schema.json index aefa3634d2d9..72f37f203cdc 100644 --- a/chart/values.schema.json +++ b/chart/values.schema.json @@ -76,6 +76,20 @@ "default": "2.2.2", "x-docsSection": "Common" }, + "securityContext": { + "description": "Pod security context definition. The values in this parameter will be used when `securityContext` is not defined for specific Pods", + "type": "object", + "$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.22.0/_definitions.json#/definitions/io.k8s.api.core.v1.PodSecurityContext", + "default": {}, + "x-docsSection": "Kubernetes", + "examples": [ + { + "runAsUser": 50000, + "runAsGroup": 0, + "fsGroup": 0 + } + ] + }, "nodeSelector": { "description": "Select certain nodes for all pods.", "type": "object", @@ -1379,6 +1393,19 @@ "$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.22.0-standalone-strict/resourcerequirements-v1.json" } } + }, + "securityContext": { + "description": "Security context for the worker pod. If not set, the values from `securityContext` will be used.", + "type": "object", + "$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.22.0/_definitions.json#/definitions/io.k8s.api.core.v1.PodSecurityContext", + "default": {}, + "examples": [ + { + "runAsUser": 50000, + "runAsGroup": 0, + "fsGroup": 0 + } + ] } } }, @@ -1657,6 +1684,19 @@ "$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.22.0-standalone-strict/resourcerequirements-v1.json" } } + }, + "securityContext": { + "description": "Security context for the scheduler pod. If not set, the values from `securityContext` will be used.", + "type": "object", + "$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.22.0/_definitions.json#/definitions/io.k8s.api.core.v1.PodSecurityContext", + "default": {}, + "examples": [ + { + "runAsUser": 50000, + "runAsGroup": 0, + "fsGroup": 0 + } + ] } } }, @@ -1860,6 +1900,19 @@ "additionalProperties": { "type": "string" } + }, + "securityContext": { + "description": "Security context for the triggerer pod. If not set, the values from `securityContext` will be used.", + "type": "object", + "$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.22.0/_definitions.json#/definitions/io.k8s.api.core.v1.PodSecurityContext", + "default": {}, + "examples": [ + { + "runAsUser": 50000, + "runAsGroup": 0, + "fsGroup": 0 + } + ] } } }, @@ -1960,6 +2013,19 @@ "$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.22.0-standalone-strict/toleration-v1.json" } }, + "securityContext": { + "description": "Security context for the create user job pod. If not set, the values from `securityContext` will be used.", + "type": "object", + "$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.22.0/_definitions.json#/definitions/io.k8s.api.core.v1.PodSecurityContext", + "default": {}, + "examples": [ + { + "runAsUser": 50000, + "runAsGroup": 0, + "fsGroup": 0 + } + ] + }, "resources": { "description": "Resources for the create user job pod", "type": "object", @@ -2099,6 +2165,19 @@ "$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.22.0-standalone-strict/toleration-v1.json" } }, + "securityContext": { + "description": "Security context for the migrate database job pod. If not set, the values from `securityContext` will be used.", + "type": "object", + "$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.22.0/_definitions.json#/definitions/io.k8s.api.core.v1.PodSecurityContext", + "default": {}, + "examples": [ + { + "runAsUser": 50000, + "runAsGroup": 0, + "fsGroup": 0 + } + ] + }, "useHelmHooks": { "description": "Specify if you want to use the default Helm Hook annotations", "type": "boolean", @@ -2285,6 +2364,19 @@ } } }, + "securityContext": { + "description": "Security context for the webserver job pod. If not set, the values from `securityContext` will be used.", + "type": "object", + "$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.22.0/_definitions.json#/definitions/io.k8s.api.core.v1.PodSecurityContext", + "default": {}, + "examples": [ + { + "runAsUser": 50000, + "runAsGroup": 0, + "fsGroup": 0 + } + ] + }, "resources": { "description": "Resources for webserver pods.", "type": "object", @@ -2788,6 +2880,19 @@ "additionalProperties": { "type": "string" } + }, + "securityContext": { + "description": "Security context for the flower pod. If not set, the values from `securityContext` will be used.", + "type": "object", + "$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.22.0/_definitions.json#/definitions/io.k8s.api.core.v1.PodSecurityContext", + "default": {}, + "examples": [ + { + "runAsUser": 50000, + "runAsGroup": 0, + "fsGroup": 0 + } + ] } } }, @@ -2903,6 +3008,19 @@ "description": "Additional mappings for StatsD exporter.", "type": "array", "default": [] + }, + "securityContext": { + "description": "Security context for the statsd pod. If not set, the values from `securityContext` will be used.", + "type": "object", + "$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.22.0/_definitions.json#/definitions/io.k8s.api.core.v1.PodSecurityContext", + "default": {}, + "examples": [ + { + "runAsUser": 50000, + "runAsGroup": 0, + "fsGroup": 0 + } + ] } } }, @@ -3654,6 +3772,19 @@ } } } + }, + "securityContext": { + "description": "Security context for the cleanup job pod. If not set, the values from `securityContext` will be used.", + "type": "object", + "$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.22.0/_definitions.json#/definitions/io.k8s.api.core.v1.PodSecurityContext", + "default": {}, + "examples": [ + { + "runAsUser": 50000, + "runAsGroup": 0, + "fsGroup": 0 + } + ] } } }, @@ -3813,6 +3944,18 @@ "type": "string", "default": "git-sync" }, + "securityContext": { + "description": "Security context for the gitSync container. If not set, the values from `securityContext` will be used.", + "type": "object", + "$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.22.0/_definitions.json#/definitions/io.k8s.api.core.v1.SecurityContext", + "default": {}, + "examples": [ + { + "runAsUser": 50000, + "runAsGroup": 0 + } + ] + }, "uid": { "description": "Git sync container run as user parameter.", "type": "integer", diff --git a/chart/values.yaml b/chart/values.yaml index 6681b56c344f..e4880ea5b4e8 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -32,6 +32,12 @@ kubeVersionOverride: "" uid: 50000 gid: 0 +# Default security context for airflow +securityContext: {} +# runAsUser: 50000 +# fsGroup: 0 +# runAsGroup: 0 + # Airflow home directory # Used for mount paths airflowHome: /opt/airflow @@ -405,6 +411,12 @@ workers: maxSurge: "100%" maxUnavailable: "50%" + # When not set, the values defined in the global securityContext will be used + securityContext: {} + # runAsUser: 50000 + # fsGroup: 0 + # runAsGroup: 0 + # Create ServiceAccount serviceAccount: # Specifies whether a ServiceAccount should be created @@ -549,6 +561,12 @@ scheduler: # (when not using LocalExecutor and workers.persistence) strategy: ~ + # When not set, the values defined in the global securityContext will be used + securityContext: {} + # runAsUser: 50000 + # fsGroup: 0 + # runAsGroup: 0 + # Create ServiceAccount serviceAccount: # Specifies whether a ServiceAccount should be created @@ -628,6 +646,12 @@ createUserJob: # jobAnnotations are annotations on the create user job jobAnnotations: {} + # When not set, the values defined in the global securityContext will be used + securityContext: {} + # runAsUser: 50000 + # fsGroup: 0 + # runAsGroup: 0 + # Create ServiceAccount serviceAccount: # Specifies whether a ServiceAccount should be created @@ -668,6 +692,12 @@ migrateDatabaseJob: # jobAnnotations are annotations on the database migration job jobAnnotations: {} + # When not set, the values defined in the global securityContext will be used + securityContext: {} + # runAsUser: 50000 + # fsGroup: 0 + # runAsGroup: 0 + # Create ServiceAccount serviceAccount: # Specifies whether a ServiceAccount should be created @@ -738,6 +768,12 @@ webserver: # Allow overriding Update Strategy for Webserver strategy: ~ + # When not set, the values defined in the global securityContext will be used + securityContext: {} + # runAsUser: 50000 + # fsGroup: 0 + # runAsGroup: 0 + # Additional network policies as needed (Deprecated - renamed to `webserver.networkPolicy.ingress.from`) extraNetworkPolicies: [] networkPolicy: @@ -861,6 +897,12 @@ triggerer: # Annotations to add to triggerer kubernetes service account. annotations: {} + # When not set, the values defined in the global securityContext will be used + securityContext: {} + # runAsUser: 50000 + # fsGroup: 0 + # runAsGroup: 0 + resources: {} # limits: # cpu: 100m @@ -935,6 +977,12 @@ flower: # cpu: 100m # memory: 128Mi + # When not set, the values defined in the global securityContext will be used + securityContext: {} + # runAsUser: 50000 + # fsGroup: 0 + # runAsGroup: 0 + # Create ServiceAccount serviceAccount: # Specifies whether a ServiceAccount should be created @@ -998,6 +1046,12 @@ statsd: # Annotations to add to worker kubernetes service account. annotations: {} + # When not set, the values defined in the global securityContext will be used + securityContext: {} + # runAsUser: 65534 + # fsGroup: 0 + # runAsGroup: 0 + # Additional network policies as needed extraNetworkPolicies: [] resources: {} @@ -1263,6 +1317,11 @@ cleanup: # Annotations to add to cleanup cronjob kubernetes service account. annotations: {} + # When not set, the values defined in the global securityContext will be used + securityContext: {} + # runAsUser: 50000 + # runAsGroup: 0 + # Configuration for postgresql subchart # Not recommended for production postgresql: @@ -1434,6 +1493,12 @@ dags: wait: 60 containerName: git-sync uid: 65533 + + # When not set, the values defined in the global securityContext will be used + securityContext: {} + # runAsUser: 65533 + # runAsGroup: 0 + extraVolumeMounts: [] env: [] resources: {} diff --git a/docs/helm-chart/production-guide.rst b/docs/helm-chart/production-guide.rst index fbb70d3442ec..04149e71dd08 100644 --- a/docs/helm-chart/production-guide.rst +++ b/docs/helm-chart/production-guide.rst @@ -283,3 +283,130 @@ In order to enable the usage of SCCs, one must set the parameter :ref:`rbac.crea In this chart, SCCs are bound to the Pods via RoleBindings meaning that the option ``rbac.create`` must also be set to ``true`` in order to fully enable the SCC usage. For more information about SCCs and what can be achieved with this construct, please refer to `Managing security context constraints `_. + +Security Context +---------------- + +In Kubernetes a ``securityContext`` can be used to define user ids, group ids and capabilities such as running a container in privileged mode. + +When deploying an application to Kubernetes, it is recommended to give the least privilege to containers so as +to reduce access and protect the host where the container is running. + +In the Airflow Helm chart, the ``securityContext`` can be configured in several ways: + + * :ref:`uid ` (configures the global uid or RunAsUser) + * :ref:`gid ` (configures the global gid or fsGroup) + * :ref:`securityContext ` (same as ``uid`` but allows for setting all `Pod securityContext options `_) + +The same way one can configure the global :ref:`securityContext `, it is also possible to configure different values for specific workloads by setting their local ``securityContext`` as follows: + +.. code-block:: yaml + + workers: + securityContext: + runAsUser: 5000 + fsGroup: 0 + +In the example above, the workers Pod ``securityContext`` will be set to ``runAsUser: 5000`` and ``runAsGroup: 0``. + +As one can see, the local setting will take precedence over the global setting when defined. The following explains the precedence rule for ``securityContext`` options in this chart: + +.. code-block:: yaml + + uid: 40000 + gid: 0 + + securityContext: + runAsUser: 50000 + fsGroup: 0 + + workers: + securityContext: + runAsUser: 1001 + fsGroup: 0 + +This will generate the following worker deployment: + +.. code-block:: yaml + + kind: StatefulSet + apiVersion: apps/v1 + metadata: + name: airflow-worker + spec: + serviceName: airflow-worker + template: + spec: + securityContext: # As the securityContext was defined in ``workers``, its value will take priority + runAsUser: 1001 + fsGroup: 0 + +If we remove both the ``securityContext`` and ``workers.securityContext`` from the example above, the output will be the following: + +.. code-block:: yaml + + uid: 40000 + gid: 0 + + securityContext: {} + + workers: + securityContext: {} + +This will generate the following worker deployment: + +.. code-block:: yaml + + kind: StatefulSet + apiVersion: apps/v1 + metadata: + name: airflow-worker + spec: + serviceName: airflow-worker + template: + spec: + securityContext: + runAsUser: 40000 # As the securityContext was not defined in ``workers`` or ``podSecurity``, the value from uid will be used + fsGroup: 0 # As the securityContext was not defined in ``workers`` or ``podSecurity``, the value from gid will be used + initContainers: + - name: wait-for-airflow-migrations + ... + containers: + - name: worker + ... + +And finally if we set ``securityContext`` but not ``workers.securityContext``: + +.. code-block:: yaml + + uid: 40000 + gid: 0 + + securityContext: + runAsUser: 50000 + fsGroup: 0 + + workers: + securityContext: {} + +This will generate the following worker deployment: + +.. code-block:: yaml + + kind: StatefulSet + apiVersion: apps/v1 + metadata: + name: airflow-worker + spec: + serviceName: airflow-worker + template: + spec: + securityContext: # As the securityContext was not defined in ``workers``, the values from securityContext will take priority + runAsUser: 50000 + fsGroup: 0 + initContainers: + - name: wait-for-airflow-migrations + ... + containers: + - name: worker + ...