Как ArgoCD секреты из Vault не расшифровывал или при чём тут App of Apps

от автора

Привет, Хабр!

Столкнулся по работе с интересной, на мой взгляд, ситуацией. Многим она может показаться банальной, но мне пришлось достаточно весомое время посидеть, чтобы разобраться.

Предыстория

Стояла довольно понятная задача — развернуть Helm-чарт через ArgoCD, конкретно речь идёт про Airflow. Часть данных в values чувствительна, например пароль от Postgres, поэтому её неплохо бы вынести во внешнее хранилище секретов. Мы используем для этого HashiCorp Vault.

Есть несколько способов подтянуть секреты из Vault в поды. Наиболее предпочтительным по ряду причин является vault-injector, но основная причина для нас была в том, что ArgoCD image-updater на данный момент не умеет работать, если используются плагины, об особенности работы с которыми и пойдёт речь. В обычной ситуации я бы и воспользовался vault-injector, но в случае с чартом Airflow это показалось довольно сложной задачей, поэтому было принято решение воспользоваться менее предпочтительным, но точно рабочим (как я думал) вариантом с ArgoCD Vault Plugin.

Что за плагин такой

Не вдаваясь в детали, настроив этот плагин согласно инструкции вы получаете возможность инжектить секреты прямо в values, используя следующий синтаксис:

secret: <path:path/to/secret/in/vault#secret_key>

Если не обращать внимание на image-updater и на то, что секреты не будут обновляться внутри пода при обновлении их в Vault, то такой путь вполне приемлем.

Непосредственно проблема

Когда секреты были добавлены в хранилище и ArgoCD Application был написан, я попытался развернуть его для теста. Приведу примерный Application с которым это делалось (весомая часть пропущена для компактности):

apiVersion: argoproj.io/v1alpha1 kind: Application metadata:   name: airflow   labels:     app.kubernetes.io/name: airflow     app.kubernetes.io/component: airflow   namespace: argocd   finalizers:     - resources-finalizer.argocd.argoproj.io spec:   project: default   destination:     namespace: some-namespace     name: cluster   source:     repoURL: "airflow_repo_url"     targetRevision: "revision"     chart: airflow     plugin:       name: argocd-vault-plugin-helm       env:         - name: HELM_VALUES           value: |               ...               metadataConnection:                 user: user                 pass: <path:path/to/airflow/secrets#postgres_password>                 protocol: postgresql                 host: postgres.db.url                 port: 5432                 db: airflow_db                 sslmode: prefer               ...         syncPolicy:     automated:       prune: true       selfHeal: true     syncOptions:       - Validate=true       - CreateNamespace=true

Как видно, ничего необычного, за исключением прокидывания values прямо из Application и того самого секрета.

К моему удивлению компонент webserver отказывался запускаться, ссылаясь на невозможность подключиться к базе данных, хотя в правильности данных я был абсолютно уверен. После долгого дебага я наконец решил посмотреть, что же лежит в Kubernetes Secret, который создаёт чарт и откуда берётся пароль. Вот, что я увидел:

apiVersion: v1 data:   // base64 encoded   postgres_password: %3Cpath%3Apath%2Fto%2Fairflow%2Fsecrets%23postgres_password%3E

При этом рядом был другой секрет, созданный этим же чартом, который расшифровался абсолютно корректно. После долгих попыток понять в чём же дело, я решил развернуть приложение иначе. Кроме прямого разворачивания Application есть и другой вариант — App of Apps. Подробнее можно почитать в официальной документации, но вкратце — это способ создать базовое приложение (ArgoCD Application), которое будет подтягивать из директории в Git-репозитории другие приложения, и о чудо — всё заработало как надо.

В чём же дело

После недолгих размышлений я заметил одну особенность — момент в который секреты непосредственно расшифровываются, а происходит это при создании того Application, в котором прописаны пути до секретов. То есть когда я создавал приложение напрямую, расшифровка происходила при развороте непосредственно Airflow, а в случае App of Apps — при разворачивании родительского приложения.

Этот факт дал мне понять, что первоначальный корень проблемы лежит где-то в самом чарте Airflow и я не прогадал.

Вот что мы видим в манифесте секрета metadata-connection:

connection: {{ urlJoin (dict "scheme" .protocol "userinfo" (printf "%s:%s" (.user | urlquery) (.pass | urlquery) ) "host" (printf "%s:%s" $host $port) "path" (printf "/%s" $database) "query" $query) | b64enc | quote }}

Согласно официальной документации Helm:

urlquery

Returns the escaped version of the value passed in as an argument so that it is suitable for embedding in the query portion of a URL.

Виновник экранированных данных в объекте Secret найден. Осталось только понять, почему же строка сначала была проэкранирована, а не расшифрована.

Тут придётся вспомнить, как именно argocd-vault-plugin устанавливается в кластер. Обратимся к официальной документации и найдём в ней ссылку на пример манифеста ConfigMap, который и объясняет происходящее волшебство:

apiVersion: v1 kind: ConfigMap metadata:   name: argocd-cm data:   configManagementPlugins: |     - name: argocd-vault-plugin       generate:         command: ["argocd-vault-plugin"]         args: ["generate", "./"]      - name: argocd-vault-plugin-helm       generate:         command: ["sh", "-c"]         args: ['helm template "$ARGOCD_APP_NAME" -n "$ARGOCD_APP_NAMESPACE" . | argocd-vault-plugin generate -']      # This lets you pass args to the Helm invocation as described here: https://argocd-vault-plugin.readthedocs.io/en/stable/usage/#with-helm     # IMPORTANT: passing $helm_args effectively allows users to run arbitrary code in the Argo CD repo-server.     # Only use this when the users are completely trusted. If possible, determine which Helm arguments are needed by      # your users and explicitly pass only those arguments.     - name: argocd-vault-plugin-helm-with-args       generate:         command: ["sh", "-c"]         args: ['helm template "$ARGOCD_APP_NAME" -n "$ARGOCD_APP_NAMESPACE" ${helm_args} . | argocd-vault-plugin generate -']      # This lets you pass a values file as a string as described here:     # https://argocd-vault-plugin.readthedocs.io/en/stable/usage/#with-helm     - name: argocd-vault-plugin-helm-with-values       generate:         command: ["bash", "-c"]         args: ['helm template "$ARGOCD_APP_NAME" -n "$ARGOCD_APP_NAMESPACE" -f <(echo "$ARGOCD_ENV_HELM_VALUES") . | argocd-vault-plugin generate -']      - name: argocd-vault-plugin-kustomize       generate:         command: ["sh", "-c"]         args: ["kustomize build . | argocd-vault-plugin generate -"]

Главное, что нужно тут заметить — сперва вызывается helm template, который рендерит шаблон, в том числе выполняя urlquery, и только после этого подменяются секреты. Указание секретов для плагина имеет строгий синтаксис, помните?

secret: <path:path/to/secret/in/vault#secret_key>

Но когда этот строгий синтаксис подвергается экранированию при рендере чарта, то плагин уже не в состоянии заменить значение. Вот и вся загадка.

Заключение

Конечно, подобных проблем можно было избежать, но история не терпит сослагательного наклонения, поэтому надеюсь, что моё небольшое исследование покажется кому-то полезным или, может быть, даже сэкономит несколько часов работы.

Я рад, что пришлось погрузиться в такие дебри, ведь на ошибках учимся. Делитесь своими кейсами подобного рода, будет интересно почитать.


ссылка на оригинал статьи https://habr.com/ru/articles/863788/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *