feat(ext-proxy): поддержка username/password для basic auth — хэш генерируется автоматически

Раньше требовалась готовая строка 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
This commit is contained in:
Sergey Antropoff
2026-04-26 07:26:31 +03:00
parent aae7941416
commit 12578dda27
4 changed files with 112 additions and 18 deletions

View File

@@ -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 и т.д.)

View File

@@ -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: []

View File

@@ -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

View File

@@ -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) }}