Уточни TOTP seed и статус модулей VPN
This commit is contained in:
71
README.md
71
README.md
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
CLI-установка корпоративного VPN `vpn.lemanapro.ru` для macOS.
|
CLI-установка корпоративного VPN `vpn.lemanapro.ru` для macOS.
|
||||||
|
|
||||||
|
**Модули по умолчанию:** Core: включён; Bitwarden: включён; Touch ID: включён; DNS cleanup: включён; runtime-патчи: применяются автоматически перед подключением.
|
||||||
|
|
||||||
Репозиторий собирает в один воспроизводимый пакет то, что раньше было ручной локальной настройкой:
|
Репозиторий собирает в один воспроизводимый пакет то, что раньше было ручной локальной настройкой:
|
||||||
|
|
||||||
- `openconnect` как VPN-клиент;
|
- `openconnect` как VPN-клиент;
|
||||||
- `openconnect-lite` для SAML SSO через Keycloak;
|
- `openconnect-lite` для SAML SSO через Keycloak;
|
||||||
- опциональный Bitwarden CLI для LDAP-пароля и TOTP;
|
- опциональный Bitwarden CLI для LDAP-пароля и TOTP seed;
|
||||||
- опциональный Touch ID helper для мастер-пароля Bitwarden;
|
- опциональный Touch ID helper для мастер-пароля Bitwarden;
|
||||||
- безопасный DNS cleanup через root-owned wrapper;
|
- безопасный DNS cleanup через root-owned wrapper;
|
||||||
- алиасы `vpn`, `vpn-debug`, `vpn-fix-dns`.
|
- алиасы `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
|
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
|
```sh
|
||||||
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh -s -- --minimal --configure-keychain
|
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 |
|
| `/etc/sudoers.d/lemana-vpn-dns` | `NOPASSWD` только для DNS cleanup wrapper |
|
||||||
| `~/.zshrc` | Идемпотентный блок алиасов `vpn`, `vpn-debug`, `vpn-fix-dns` |
|
| `~/.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
|
### Core
|
||||||
@@ -87,7 +118,9 @@ curl -fsSL https://example.org/dokril/lemana-vpn/raw/branch/main/install.sh \
|
|||||||
|
|
||||||
### Bitwarden
|
### 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
|
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
|
### Touch ID
|
||||||
|
|
||||||
Включён по умолчанию. Установщик собирает `keychain-fingerprint` из `https://github.com/dss99911/keychain-fingerprint.git` и кладёт бинарник в `~/bin/keychain-fingerprint`.
|
Включён по умолчанию. Установщик собирает `keychain-fingerprint` из `https://github.com/dss99911/keychain-fingerprint.git` и кладёт бинарник в `~/bin/keychain-fingerprint`.
|
||||||
@@ -128,7 +189,7 @@ vpn-fix-dns # сбросить корпоративные DNS после
|
|||||||
1. CLI проверит `bw`.
|
1. CLI проверит `bw`.
|
||||||
2. Если vault locked, попросит мастер-пароль.
|
2. Если vault locked, попросит мастер-пароль.
|
||||||
3. Если установлен Touch ID helper, предложит сохранить мастер-пароль за Touch ID prompt.
|
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.
|
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 пароль;
|
- password: LDAP пароль;
|
||||||
- TOTP: `otpauth://...secret=BASE32...` или raw BASE32 secret.
|
- TOTP: `otpauth://...secret=BASE32...` или raw BASE32 secret.
|
||||||
|
|
||||||
|
Это не 6-значный одноразовый код. В Bitwarden должен лежать постоянный TOTP secret, из которого коды генерируются автоматически.
|
||||||
|
|
||||||
## Почему DNS wrapper, а не wildcard sudoers
|
## Почему DNS wrapper, а не wildcard sudoers
|
||||||
|
|
||||||
Старый вариант давал `NOPASSWD` на `networksetup -setdnsservers *`. Это слишком широкое право: любой локальный процесс пользователя мог поменять DNS на произвольный сервер.
|
Старый вариант давал `NOPASSWD` на `networksetup -setdnsservers *`. Это слишком широкое право: любой локальный процесс пользователя мог поменять DNS на произвольный сервер.
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ Usage: vpn-lemanapro.sh [--debug] [--json] [--status] [--configure-keychain]
|
|||||||
--status --json Show current VPN status as JSON
|
--status --json Show current VPN status as JSON
|
||||||
--debug Run visible browser and passthrough debug logs
|
--debug Run visible browser and passthrough debug logs
|
||||||
--json Emit JSON Lines events for UI wrappers
|
--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
|
HELP
|
||||||
exit 0
|
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
|
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() {
|
_check_status() {
|
||||||
|
local modules_json
|
||||||
|
modules_json="$(_module_status_json)"
|
||||||
|
|
||||||
if [[ ! -f "$STATUS_FILE" ]]; then
|
if [[ ! -f "$STATUS_FILE" ]]; then
|
||||||
if $JSON_MODE; 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
|
else
|
||||||
|
_module_status_human
|
||||||
printf '%s\n' "VPN disconnected (нет status-файла)"
|
printf '%s\n' "VPN disconnected (нет status-файла)"
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
@@ -100,8 +202,9 @@ _check_status() {
|
|||||||
|
|
||||||
if [[ "$state" != "connected" ]] || ! $process_alive; then
|
if [[ "$state" != "connected" ]] || ! $process_alive; then
|
||||||
if $JSON_MODE; 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
|
else
|
||||||
|
_module_status_human
|
||||||
printf '%s\n' "VPN disconnected"
|
printf '%s\n' "VPN disconnected"
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
@@ -125,22 +228,15 @@ _check_status() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if $JSON_MODE; then
|
if $JSON_MODE; then
|
||||||
printf '{"state":"connected","ip":"%s","healthy":%s,"remaining_sec":%s,"expires":"%s","pid":%s,"dns_target":"%s"}\n' \
|
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"
|
"$ip" "$healthy" "$remaining_sec" "$expires" "$pid" "$dns_target" "$modules_json"
|
||||||
else
|
else
|
||||||
|
_module_status_human
|
||||||
printf 'VPN connected - %s (session: %sh %sm, tunnel: %s)\n' \
|
printf 'VPN connected - %s (session: %sh %sm, tunnel: %s)\n' \
|
||||||
"$ip" "$hours" "$mins" "$($healthy && printf healthy || printf unhealthy)"
|
"$ip" "$hours" "$mins" "$($healthy && printf healthy || printf unhealthy)"
|
||||||
fi
|
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() {
|
_patch_oc() {
|
||||||
local wep
|
local wep
|
||||||
wep="$(_find_webengine_process)"
|
wep="$(_find_webengine_process)"
|
||||||
@@ -433,6 +529,12 @@ fi
|
|||||||
|
|
||||||
trap '_dns_cleanup; _clear_status' EXIT
|
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
|
_patch_oc
|
||||||
_sync_bitwarden
|
_sync_bitwarden
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ Options:
|
|||||||
--without-bitwarden Do not install/use Bitwarden CLI; use Keychain credentials
|
--without-bitwarden Do not install/use Bitwarden CLI; use Keychain credentials
|
||||||
--with-touchid Install/use keychain-fingerprint Touch ID helper (default)
|
--with-touchid Install/use keychain-fingerprint Touch ID helper (default)
|
||||||
--without-touchid Do not install/use Touch ID helper
|
--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)
|
--username VALUE Corporate LDAP username (default: 60103293)
|
||||||
--bw-item VALUE Bitwarden item name (default: LM LDAP)
|
--bw-item VALUE Bitwarden item name (default: LM LDAP)
|
||||||
--raw-base-url URL Raw file base URL for curl installs
|
--raw-base-url URL Raw file base URL for curl installs
|
||||||
|
|||||||
Reference in New Issue
Block a user