From 12578dda279c5bb8e24626ea4273d84e87d1b02a Mon Sep 17 00:00:00 2001 From: Sergey Antropoff Date: Sun, 26 Apr 2026 07:26:31 +0300 Subject: [PATCH] =?UTF-8?q?feat(ext-proxy):=20=D0=BF=D0=BE=D0=B4=D0=B4?= =?UTF-8?q?=D0=B5=D1=80=D0=B6=D0=BA=D0=B0=20username/password=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20basic=20auth=20=E2=80=94=20=D1=85=D1=8D=D1=88=20?= =?UTF-8?q?=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B8=D1=80=D1=83=D0=B5=D1=82?= =?UTF-8?q?=D1=81=D1=8F=20=D0=B0=D0=B2=D1=82=D0=BE=D0=BC=D0=B0=D1=82=D0=B8?= =?UTF-8?q?=D1=87=D0=B5=D1=81=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Раньше требовалась готовая строка htpasswd -nb admin 'пароль'. Теперь достаточно указать username и password — Ansible автоматически вызывает openssl passwd -apr1 и записывает хэш в K8s Secret. Изменения: - defaults/main.yml: добавлены поля auth.username и auth.password - tasks/main.yml: Python-скрипт обрабатывает все прокси перед деплоем: вызывает openssl passwd -apr1, убирает открытый пароль из values, поддерживает глобальные defaults и per-proxy overrides - templates/values.yaml.j2: использует _ext_proxy_proxies_final и _ext_proxy_def_auth_final (с уже подставленными хэшами) - README.md: обновлена документация по basic auth — username/password основной вариант Приоритет: secretName > credentials > username+password --- addons/ext-proxy/README.md | 49 ++++++++++---- addons/ext-proxy/role/defaults/main.yml | 11 +++- addons/ext-proxy/role/tasks/main.yml | 65 +++++++++++++++++++ .../ext-proxy/role/templates/values.yaml.j2 | 5 +- 4 files changed, 112 insertions(+), 18 deletions(-) diff --git a/addons/ext-proxy/README.md b/addons/ext-proxy/README.md index 5fa812a..84a2303 100644 --- a/addons/ext-proxy/README.md +++ b/addons/ext-proxy/README.md @@ -108,7 +108,9 @@ ext_proxy_defaults: auth: enabled: false # включить nginx basic authentication - credentials: "" # строка htpasswd: htpasswd -nb admin 'пароль' + username: "" # логин (пароль хэшируется автоматически) + password: "" # пароль в открытом виде — задавать через vault! + credentials: "" # готовая htpasswd-строка (приоритет над username/password) secretName: "" # использовать существующий Secret вместо генерации нового websocket: true # включить WebSocket (заголовки HTTP/1.1 upgrade) @@ -148,8 +150,10 @@ ext_proxy_proxies: auth: enabled: true - credentials: "admin:$apr1$abc..." # строка htpasswd - # ИЛИ: secretName: my-existing-auth-secret + username: admin # логин — хэш генерируется автоматически + password: "{{ vault_mypass }}" # пароль из vault (рекомендуется) + # ИЛИ готовая строка: credentials: "admin:$apr1$..." + # ИЛИ существующий Secret: secretName: my-existing-auth-secret annotations: # аннотации уровня прокси (переопределяют всё) nginx.ingress.kubernetes.io/proxy-body-size: "0" @@ -207,21 +211,26 @@ cert-manager автоматически выпустит сертификат д ### Basic Authentication -Сгенерируй учётные данные htpasswd (установи `apache2-utils` или `httpd-tools`): - -```bash -htpasswd -nb admin 'мойсекретныйпароль' -# выводит: admin:$apr1$Rh0Ycxl9$rPTH7gRHfMBkS.7.Q1BxM/ -``` +Просто укажи логин и пароль — хэш генерируется автоматически: ```yaml +# group_vars/all/addons.yml + ext_proxy_defaults: auth: enabled: true - credentials: "admin:$apr1$Rh0Ycxl9$rPTH7gRHfMBkS.7.Q1BxM/" + username: admin + password: "{{ vault_ext_proxy_password }}" # пароль из vault ``` -Или выборочно по конкретным прокси: +```yaml +# group_vars/all/vault.yml +vault_ext_proxy_password: "мойсекретныйпароль" +``` + +Ansible автоматически вызовет `openssl passwd -apr1` и запишет хэш в Kubernetes Secret. Пароль в открытом виде **не попадает** в Helm values, логи или конфиги. + +Выборочно для конкретного прокси (остальные без auth): ```yaml ext_proxy_proxies: @@ -231,7 +240,8 @@ ext_proxy_proxies: port: 8080 auth: enabled: true - credentials: "admin:$apr1$..." + username: admin + password: "{{ vault_router_password }}" - name: plex # без auth hosts: [plex.home.ru] @@ -239,7 +249,18 @@ ext_proxy_proxies: port: 32400 ``` -Использование существующего Secret (ключ должен называться `auth`, тип `Opaque`): +Разные пароли для разных прокси — каждый прокси создаёт свой отдельный Secret. + +**Если уже есть готовая htpasswd-строка** (поле `credentials` имеет приоритет над `username`/`password`): + +```yaml +auth: + enabled: true + credentials: "admin:$apr1$Rh0Ycxl9$rPTH7gRHfMBkS.7.Q1BxM/" + # Сгенерировать вручную: htpasswd -nb admin 'пароль' +``` + +**Использование существующего Secret** (ключ должен называться `auth`, тип `Opaque`): ```yaml auth: @@ -247,6 +268,8 @@ auth: secretName: my-shared-htpasswd # создать вручную через kubectl ``` +**Приоритет:** `secretName` > `credentials` > `username` + `password` + > **Примечание:** `configuration-snippet` должен быть разрешён в ingress-nginx (`allow-snippet-annotations: true`), если добавляешь кастомные сниппеты. Стандартные аннотации работают без этого. ### WebSocket (Plex, Grafana, Home Assistant и т.д.) diff --git a/addons/ext-proxy/role/defaults/main.yml b/addons/ext-proxy/role/defaults/main.yml index 8107bf8..63b2301 100644 --- a/addons/ext-proxy/role/defaults/main.yml +++ b/addons/ext-proxy/role/defaults/main.yml @@ -15,8 +15,10 @@ ext_proxy_defaults: issuerKind: ClusterIssuer auth: enabled: false - credentials: "" # htpasswd -nb user pass - secretName: "" + username: "" # логин — пароль хэшируется автоматически через openssl passwd -apr1 + password: "" # пароль в открытом виде (задай в vault.yml!) + credentials: "" # готовая htpasswd-строка (если задана — username/password игнорируются) + secretName: "" # использовать существующий Secret вместо генерации нового websocket: true path: / pathType: Prefix @@ -59,7 +61,10 @@ ext_proxy_defaults: # issuerKind: ClusterIssuer # auth: # enabled: true -# credentials: "admin:$apr1$..." # htpasswd -nb admin password +# username: admin # простой логин и пароль — хэш генерируется автоматически +# password: "{{ vault_myapp_password }}" +# # ИЛИ готовая htpasswd-строка: +# # credentials: "admin:$apr1$..." # annotations: # nginx.ingress.kubernetes.io/proxy-body-size: "0" ext_proxy_proxies: [] diff --git a/addons/ext-proxy/role/tasks/main.yml b/addons/ext-proxy/role/tasks/main.yml index 456c37c..fe4a069 100644 --- a/addons/ext-proxy/role/tasks/main.yml +++ b/addons/ext-proxy/role/tasks/main.yml @@ -42,6 +42,71 @@ mode: preserve become: true +# ── Generate htpasswd hashes from plain username/password ──────────────────── +# Если auth.username + auth.password заданы — автоматически хэшируем через +# openssl passwd -apr1, записываем в auth.credentials и убираем открытые поля. +# Если auth.credentials уже задан — используем его без изменений. +# Пароли в открытом виде нигде не попадают в Helm values или логи. + +- name: Generate htpasswd credentials (username/password → apr1 hash) + ansible.builtin.command: + argv: + - python3 + - -c + - | + import json, subprocess, sys + + proxies = json.loads(sys.argv[1]) + def_auth = json.loads(sys.argv[2]) + + def gen_credentials(auth, fallback): + """Return htpasswd string or '' if nothing to generate.""" + username = auth.get('username') or fallback.get('username', '') + password = auth.get('password') or fallback.get('password', '') + credentials = auth.get('credentials') or fallback.get('credentials', '') + secret_name = auth.get('secretName') or fallback.get('secretName', '') + if credentials or secret_name: + return credentials # уже готово или внешний Secret + if username and password: + r = subprocess.run( + ['openssl', 'passwd', '-apr1', password], + capture_output=True, text=True, check=True + ) + return username + ':' + r.stdout.strip() + return '' + + # Обработать глобальный default auth + cleaned_def_auth = {k: v for k, v in def_auth.items() + if k not in ('username', 'password')} + creds = gen_credentials(def_auth, {}) + if creds: + cleaned_def_auth['credentials'] = creds + + # Обработать каждый прокси + for proxy in proxies: + auth = dict(proxy.get('auth') or {}) + enabled = auth.get('enabled', def_auth.get('enabled', False)) + if enabled: + creds = gen_credentials(auth, def_auth) + if creds: + auth['credentials'] = creds + # Убрать открытый текст из итоговых values + auth.pop('username', None) + auth.pop('password', None) + proxy['auth'] = auth + + print(json.dumps({'proxies': proxies, 'def_auth': cleaned_def_auth})) + - "{{ ext_proxy_proxies | to_json }}" + - "{{ ext_proxy_defaults.auth | to_json }}" + register: _auth_processed + changed_when: false + no_log: true + +- name: Set final proxies and defaults with generated credentials + ansible.builtin.set_fact: + _ext_proxy_proxies_final: "{{ (_auth_processed.stdout | from_json).proxies }}" + _ext_proxy_def_auth_final: "{{ (_auth_processed.stdout | from_json).def_auth }}" + # ── Template Helm values ────────────────────────────────────────────────────── - name: Template Helm values diff --git a/addons/ext-proxy/role/templates/values.yaml.j2 b/addons/ext-proxy/role/templates/values.yaml.j2 index 88ca903..fa84245 100644 --- a/addons/ext-proxy/role/templates/values.yaml.j2 +++ b/addons/ext-proxy/role/templates/values.yaml.j2 @@ -1,8 +1,9 @@ # Generated by Ansible — do not edit manually. # Configure via: group_vars/all/addons.yml → ext_proxy_* variables. +# Note: auth.username/password are resolved to htpasswd hashes before this file is written. defaults: -{{ ext_proxy_defaults | to_yaml | indent(2, True) }} +{{ (ext_proxy_defaults | combine({'auth': _ext_proxy_def_auth_final})) | to_yaml | indent(2, True) }} proxies: -{{ ext_proxy_proxies | to_yaml | indent(2, True) }} +{{ _ext_proxy_proxies_final | to_yaml | indent(2, True) }}