Уточни TOTP seed и статус модулей VPN

This commit is contained in:
2026-05-19 12:20:45 +03:00
parent c42d2ba11e
commit 88e9695b03
3 changed files with 183 additions and 18 deletions

View File

@@ -2,11 +2,13 @@
CLI-установка корпоративного VPN `vpn.lemanapro.ru` для macOS.
**Модули по умолчанию:** Core: включён; Bitwarden: включён; Touch ID: включён; DNS cleanup: включён; runtime-патчи: применяются автоматически перед подключением.
Репозиторий собирает в один воспроизводимый пакет то, что раньше было ручной локальной настройкой:
- `openconnect` как VPN-клиент;
- `openconnect-lite` для SAML SSO через Keycloak;
- опциональный Bitwarden CLI для LDAP-пароля и TOTP;
- опциональный Bitwarden CLI для LDAP-пароля и TOTP seed;
- опциональный Touch ID helper для мастер-пароля Bitwarden;
- безопасный DNS cleanup через root-owned wrapper;
- алиасы `vpn`, `vpn-debug`, `vpn-fix-dns`.
@@ -38,7 +40,7 @@ curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh |
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh -s -- --without-touchid
```
Минимальная установка без Bitwarden и Touch ID. Пароль LDAP и TOTP будут один раз записаны в macOS Keychain вручную:
Минимальная установка без Bitwarden и Touch ID. В macOS Keychain вручную будут записаны LDAP-пароль и TOTP secret. Не текущий 30-секундный TOTP-код, а постоянный seed из настройки 2FA.
```sh
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh -s -- --minimal --configure-keychain
@@ -72,6 +74,35 @@ curl -fsSL https://example.org/dokril/lemana-vpn/raw/branch/main/install.sh \
| `/etc/sudoers.d/lemana-vpn-dns` | `NOPASSWD` только для DNS cleanup wrapper |
| `~/.zshrc` | Идемпотентный блок алиасов `vpn`, `vpn-debug`, `vpn-fix-dns` |
## Статус модулей
`vpn` и `vpn --status` первой строкой показывают, какие модули включены в конфиге и реально установлены на машине:
```sh
vpn --status
Modules: core=ok, bitwarden=on, touchid=on, dns=on, patches=active, keychain=password:yes/totp_seed:yes
VPN disconnected
```
Значения:
| Поле | Значение |
| --- | --- |
| `core=ok` | Есть `openconnect`, `openconnect-lite` и config |
| `bitwarden=on` | Модуль включён и `bw` установлен |
| `bitwarden=off` | Модуль отключён через `--without-bitwarden` или `LEMANA_VPN_USE_BITWARDEN=0` |
| `bitwarden=missing` | Модуль включён, но `bw` не найден |
| `touchid=on/off/missing` | Состояние Touch ID helper |
| `dns=on/missing` | Наличие DNS cleanup wrapper |
| `patches=active/pending` | Применены ли runtime-патчи `openconnect-lite` |
| `keychain=password:yes/totp_seed:yes` | Есть ли LDAP-пароль и TOTP seed в Keychain |
JSON-режим тоже отдаёт модульный статус:
```sh
vpn --status --json
```
## Модули
### Core
@@ -87,7 +118,9 @@ curl -fsSL https://example.org/dokril/lemana-vpn/raw/branch/main/install.sh \
### Bitwarden
Включён по умолчанию. CLI при каждом запуске `vpn` пытается получить LDAP-пароль и TOTP из записи Bitwarden `LM LDAP`, затем записывает их в macOS Keychain для `openconnect-lite`.
Включён по умолчанию. CLI при каждом запуске `vpn` пытается получить LDAP-пароль и TOTP seed из записи Bitwarden `LM LDAP`, затем записывает их в macOS Keychain для `openconnect-lite`.
TOTP seed — это постоянный секрет 2FA. Сам одноразовый TOTP-код меняется каждые 30 секунд и генерируется `openconnect-lite` в момент входа.
Отключить:
@@ -101,6 +134,34 @@ sh install.sh --without-bitwarden
vpn-lemanapro.sh --configure-keychain
```
### Если Bitwarden нет
Bitwarden не обязателен. Без него установка работает как обычный `openconnect-lite` profile с секретами в macOS Keychain.
Установка:
```sh
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh \
| sh -s -- --without-bitwarden --without-touchid --configure-keychain
```
Что понадобится:
- LDAP username;
- LDAP password;
- TOTP secret из корпоративной 2FA настройки.
Важно: вводить нужно не текущие 6 цифр из authenticator-приложения, а постоянный secret. Обычно он есть в QR-коде как `secret=BASE32...` или может быть показан при ручной настройке TOTP.
Если secret есть только в QR-коде:
1. Открой QR-код в приложении/на портале, где настраивалась 2FA.
2. Найди режим ручной настройки, где показывается secret.
3. Если доступен только QR, его нужно расшифровать любым локальным QR-сканером и взять параметр `secret`.
4. Вставь secret в prompt `TOTP secret (BASE32...)`.
Если TOTP secret получить нельзя, автоматический headless-вход невозможен: `openconnect-lite` не сможет сам генерировать свежий TOTP-код на каждом входе.
### Touch ID
Включён по умолчанию. Установщик собирает `keychain-fingerprint` из `https://github.com/dss99911/keychain-fingerprint.git` и кладёт бинарник в `~/bin/keychain-fingerprint`.
@@ -128,7 +189,7 @@ vpn-fix-dns # сбросить корпоративные DNS после
1. CLI проверит `bw`.
2. Если vault locked, попросит мастер-пароль.
3. Если установлен Touch ID helper, предложит сохранить мастер-пароль за Touch ID prompt.
4. Достанет `LM LDAP`, запишет LDAP-пароль и TOTP в Keychain.
4. Достанет `LM LDAP`, запишет LDAP-пароль и TOTP seed в Keychain.
5. Запустит `openconnect-lite` и пройдёт Keycloak SSO.
## Настройка
@@ -159,6 +220,8 @@ curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh \
- password: LDAP пароль;
- TOTP: `otpauth://...secret=BASE32...` или raw BASE32 secret.
Это не 6-значный одноразовый код. В Bitwarden должен лежать постоянный TOTP secret, из которого коды генерируются автоматически.
## Почему DNS wrapper, а не wildcard sudoers
Старый вариант давал `NOPASSWD` на `networksetup -setdnsservers *`. Это слишком широкое право: любой локальный процесс пользователя мог поменять DNS на произвольный сервер.

View File

@@ -45,7 +45,7 @@ Usage: vpn-lemanapro.sh [--debug] [--json] [--status] [--configure-keychain]
--status --json Show current VPN status as JSON
--debug Run visible browser and passthrough debug logs
--json Emit JSON Lines events for UI wrappers
--configure-keychain Prompt for LDAP password/TOTP and save them to Keychain
--configure-keychain Prompt for LDAP password and TOTP secret, then save them to Keychain
HELP
exit 0
;;
@@ -75,11 +75,113 @@ _json_get() {
python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$key',''))" 2>/dev/null || true
}
_find_webengine_process() {
if [[ -n "${LEMANA_VPN_WEBENGINE_PROCESS:-}" ]]; then
printf '%s\n' "$LEMANA_VPN_WEBENGINE_PROCESS"
return 0
fi
find "$OC_VENV/lib" -path '*/site-packages/openconnect_lite/browser/webengine_process.py' -print -quit 2>/dev/null
}
_module_bool() {
if "$@" >/dev/null 2>&1; then
printf true
else
printf false
fi
}
_patches_active() {
local wep
wep="$(_find_webengine_process)"
[[ -n "$wep" && -f "$wep" ]] || return 1
grep -q '"offscreen"' "$wep" \
&& grep -q 'new Event("input", {{bubbles: true}})' "$wep" \
&& grep -q 'new RegExp' "$wep"
}
_keychain_has() {
security find-generic-password -s "$1" -a "$2" >/dev/null 2>&1
}
_module_status_json() {
local openconnect_installed openconnect_lite_installed bitwarden_installed touchid_installed dns_cleanup_installed
local config_present oc_config_present patch_backup_present patches_active keychain_password keychain_totp_seed
openconnect_installed="$(_module_bool command -v openconnect)"
openconnect_lite_installed="$(_module_bool test -x "$OC_BIN")"
bitwarden_installed="$(_module_bool command -v bw)"
touchid_installed="$(_module_bool test -x "$KC_FP")"
dns_cleanup_installed="$(_module_bool test -x "$DNS_CLEANUP")"
config_present="$(_module_bool test -f "$CONFIG_FILE")"
oc_config_present="$(_module_bool test -f "$HOME/.config/openconnect-lite/config.toml")"
patch_backup_present="$(_module_bool test -f "$PATCH_BACKUP_DIR/webengine_process.py.before-lemana-vpn")"
patches_active="$(_module_bool _patches_active)"
keychain_password="$(_module_bool _keychain_has openconnect-lite "$KC_USERNAME")"
keychain_totp_seed="$(_module_bool _keychain_has openconnect-lite "totp/$KC_USERNAME")"
printf '{"core":{"openconnect":%s,"openconnect_lite":%s,"config":%s,"openconnect_lite_config":%s},"bitwarden":{"enabled":%s,"installed":%s,"item":"%s"},"touchid":{"enabled":%s,"installed":%s},"keychain":{"password":%s,"totp_seed":%s},"dns_cleanup":{"installed":%s},"patches":{"active":%s,"backup":%s}}' \
"$openconnect_installed" \
"$openconnect_lite_installed" \
"$config_present" \
"$oc_config_present" \
"$([[ "$USE_BITWARDEN" == "1" ]] && printf true || printf false)" \
"$bitwarden_installed" \
"$BW_ITEM_NAME" \
"$([[ "$USE_TOUCHID" == "1" ]] && printf true || printf false)" \
"$touchid_installed" \
"$keychain_password" \
"$keychain_totp_seed" \
"$dns_cleanup_installed" \
"$patches_active" \
"$patch_backup_present"
}
_module_human_part() {
local name="$1" enabled="$2" installed="$3"
if [[ "$enabled" == "0" ]]; then
printf '%s=off' "$name"
elif [[ "$installed" == "true" ]]; then
printf '%s=on' "$name"
else
printf '%s=missing' "$name"
fi
}
_module_status_human() {
local core bitwarden_installed touchid_installed dns_cleanup_installed patches_active keychain_password keychain_totp_seed
if command -v openconnect >/dev/null 2>&1 && [[ -x "$OC_BIN" && -f "$HOME/.config/openconnect-lite/config.toml" ]]; then
core="core=ok"
else
core="core=missing"
fi
bitwarden_installed="$(_module_bool command -v bw)"
touchid_installed="$(_module_bool test -x "$KC_FP")"
dns_cleanup_installed="$(_module_bool test -x "$DNS_CLEANUP")"
patches_active="$(_module_bool _patches_active)"
keychain_password="$(_module_bool _keychain_has openconnect-lite "$KC_USERNAME")"
keychain_totp_seed="$(_module_bool _keychain_has openconnect-lite "totp/$KC_USERNAME")"
printf 'Modules: %s, ' "$core"
_module_human_part "bitwarden" "$USE_BITWARDEN" "$bitwarden_installed"
printf ', '
_module_human_part "touchid" "$USE_TOUCHID" "$touchid_installed"
printf ', dns=%s, patches=%s, keychain=password:%s/totp_seed:%s\n' \
"$([[ "$dns_cleanup_installed" == "true" ]] && printf on || printf missing)" \
"$([[ "$patches_active" == "true" ]] && printf active || printf pending)" \
"$([[ "$keychain_password" == "true" ]] && printf yes || printf no)" \
"$([[ "$keychain_totp_seed" == "true" ]] && printf yes || printf no)"
}
_check_status() {
local modules_json
modules_json="$(_module_status_json)"
if [[ ! -f "$STATUS_FILE" ]]; then
if $JSON_MODE; then
printf '%s\n' '{"state":"disconnected","reason":"no status file"}'
printf '{"state":"disconnected","reason":"no status file","modules":%s}\n' "$modules_json"
else
_module_status_human
printf '%s\n' "VPN disconnected (нет status-файла)"
fi
return 0
@@ -100,8 +202,9 @@ _check_status() {
if [[ "$state" != "connected" ]] || ! $process_alive; then
if $JSON_MODE; then
printf '{"state":"disconnected","reason":"%s"}\n' "$(! $process_alive && printf 'process dead (pid=%s)' "$pid" || printf '%s' "$state")"
printf '{"state":"disconnected","reason":"%s","modules":%s}\n' "$(! $process_alive && printf 'process dead (pid=%s)' "$pid" || printf '%s' "$state")" "$modules_json"
else
_module_status_human
printf '%s\n' "VPN disconnected"
fi
return 0
@@ -125,22 +228,15 @@ _check_status() {
fi
if $JSON_MODE; then
printf '{"state":"connected","ip":"%s","healthy":%s,"remaining_sec":%s,"expires":"%s","pid":%s,"dns_target":"%s"}\n' \
"$ip" "$healthy" "$remaining_sec" "$expires" "$pid" "$dns_target"
printf '{"state":"connected","ip":"%s","healthy":%s,"remaining_sec":%s,"expires":"%s","pid":%s,"dns_target":"%s","modules":%s}\n' \
"$ip" "$healthy" "$remaining_sec" "$expires" "$pid" "$dns_target" "$modules_json"
else
_module_status_human
printf 'VPN connected - %s (session: %sh %sm, tunnel: %s)\n' \
"$ip" "$hours" "$mins" "$($healthy && printf healthy || printf unhealthy)"
fi
}
_find_webengine_process() {
if [[ -n "${LEMANA_VPN_WEBENGINE_PROCESS:-}" ]]; then
printf '%s\n' "$LEMANA_VPN_WEBENGINE_PROCESS"
return 0
fi
find "$OC_VENV/lib" -path '*/site-packages/openconnect_lite/browser/webengine_process.py' -print -quit 2>/dev/null
}
_patch_oc() {
local wep
wep="$(_find_webengine_process)"
@@ -433,6 +529,12 @@ fi
trap '_dns_cleanup; _clear_status' EXIT
if ! $JSON_MODE; then
_module_status_human
else
printf '{"event":"modules","modules":%s}\n' "$(_module_status_json)"
fi
_patch_oc
_sync_bitwarden

View File

@@ -29,7 +29,7 @@ Options:
--without-bitwarden Do not install/use Bitwarden CLI; use Keychain credentials
--with-touchid Install/use keychain-fingerprint Touch ID helper (default)
--without-touchid Do not install/use Touch ID helper
--configure-keychain Prompt for LDAP password/TOTP after install
--configure-keychain Prompt for LDAP password and TOTP secret after install
--username VALUE Corporate LDAP username (default: 60103293)
--bw-item VALUE Bitwarden item name (default: LM LDAP)
--raw-base-url URL Raw file base URL for curl installs