Files
han-app/VM2_services/codebase/services/deployment/scripts/setup-tunnel-user.sh
T

589 lines
22 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Создание OS-пользователя tunnel на VM2 для SSH local port forwarding к PostgreSQL/PgBouncer.
#
# Контракт: arch-06 — только local forwarding, PermitOpen allow-list, без sudo/shell.
# На VM2 по умолчанию AllowTcpForwarding=no; исключение только для tunnel.
#
# Использование (на VM2 под root):
# TUNNEL_AUTHORIZED_KEY_FILE=/root/bootstrap/tunnel.pub \
# PG_HOST=192.168.0.210 \
# PG_PORT=6433 \
# bash setup-tunnel-user.sh
#
# Опционально:
# TUNNEL_USER=tunnel
# TUNNEL_SOURCE_CIDRS="203.0.113.10/32,198.51.100.0/24" # ограничить источники SSH
# TUNNEL_ALLOW_ANY_SOURCE=true # осознанно разрешить ключу вход с любого адреса
# EXTRA_PERMIT_OPEN="192.168.0.210:5433" # доп. endpoint (direct PG)
# SSHD_MAIN_CONF=/etc/ssh/sshd_config.d/00-han-chat-vm2.conf
#
# По умолчанию TUNNEL_SOURCE_CIDRS обязателен. Разрешение любого источника
# требует явного TUNNEL_ALLOW_ANY_SOURCE=true и должно компенсироваться SG/firewall.
set -Eeuo pipefail
IFS=$'\n\t'
TUNNEL_USER="${TUNNEL_USER:-tunnel}"
TUNNEL_AUTHORIZED_KEY_FILE="${TUNNEL_AUTHORIZED_KEY_FILE:-}"
TUNNEL_SOURCE_CIDRS="${TUNNEL_SOURCE_CIDRS:-}"
TUNNEL_ALLOW_ANY_SOURCE="${TUNNEL_ALLOW_ANY_SOURCE:-false}"
PG_HOST="${PG_HOST:-192.168.0.210}"
PG_PORT="${PG_PORT:-6433}"
EXTRA_PERMIT_OPEN="${EXTRA_PERMIT_OPEN:-}"
SSHD_MAIN_CONF="${SSHD_MAIN_CONF:-/etc/ssh/sshd_config.d/00-han-chat-vm2.conf}"
SSHD_TUNNEL_CONF="/etc/ssh/sshd_config.d/50-han-tunnel-user.conf"
AUTHORIZED_KEYS_DIR="/etc/ssh/authorized_keys"
AUTHORIZED_KEYS_FILE="${AUTHORIZED_KEYS_DIR}/${TUNNEL_USER}"
declare -a PERMIT_OPENS=()
log() { printf '[setup-tunnel] %s\n' "$*"; }
step() { printf '\n=== %s ===\n' "$*"; }
die() { printf '[setup-tunnel] ERROR: %s\n' "$*" >&2; exit 1; }
require_root() {
[[ "$(id -u)" -eq 0 ]] || die "Скрипт нужно запускать от root"
}
require_commands() {
local command
for command in awk chmod cp getent grep id install mktemp mv passwd python3 rm \
ssh-keygen sshd stat systemctl useradd usermod; do
command -v "$command" >/dev/null 2>&1 || die "Не найдена обязательная команда: ${command}"
done
}
validate_tunnel_user() {
[[ "$TUNNEL_USER" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] \
|| die "Некорректное имя пользователя: ${TUNNEL_USER}"
}
validate_source_policy() {
[[ "$TUNNEL_ALLOW_ANY_SOURCE" == "true" || "$TUNNEL_ALLOW_ANY_SOURCE" == "false" ]] \
|| die "TUNNEL_ALLOW_ANY_SOURCE должен быть true или false"
if [[ -n "$TUNNEL_SOURCE_CIDRS" && "$TUNNEL_ALLOW_ANY_SOURCE" == "true" ]]; then
die "Задайте либо TUNNEL_SOURCE_CIDRS, либо TUNNEL_ALLOW_ANY_SOURCE=true, но не оба"
fi
if [[ -z "$TUNNEL_SOURCE_CIDRS" && "$TUNNEL_ALLOW_ANY_SOURCE" != "true" ]]; then
die "TUNNEL_SOURCE_CIDRS обязателен; для осознанного отказа задайте TUNNEL_ALLOW_ANY_SOURCE=true"
fi
if [[ "$TUNNEL_ALLOW_ANY_SOURCE" == "true" ]]; then
log "ВНИМАНИЕ: источник SSH-ключа не ограничен; доступ должен ограничиваться SG/firewall"
fi
}
validate_cidrs() {
local cidrs=$1
local err normalized
[[ -n "$cidrs" ]] || return 0
[[ "$cidrs" =~ ^[0-9A-Fa-f:./,[:space:]]+$ ]] \
|| die "TUNNEL_SOURCE_CIDRS содержит недопустимые символы"
if ! err=$(python3 - "$cidrs" 2>&1 <<'PY'
import ipaddress
import sys
parts = [raw.strip() for raw in sys.argv[1].split(",")]
if not parts or any(not part for part in parts):
print("TUNNEL_SOURCE_CIDRS содержит пустой элемент", file=sys.stderr)
raise SystemExit(1)
for cidr in parts:
try:
ipaddress.ip_network(cidr, strict=False)
except ValueError as exc:
print(f"Некорректный CIDR: {cidr} ({exc})", file=sys.stderr)
raise SystemExit(1) from exc
PY
); then
die "${err:-TUNNEL_SOURCE_CIDRS содержит некорректный CIDR}"
fi
normalized=$(python3 - "$cidrs" <<'PY'
import ipaddress
import sys
print(",".join(
str(ipaddress.ip_network(part.strip(), strict=False))
for part in sys.argv[1].split(",")
))
PY
)
TUNNEL_SOURCE_CIDRS=$normalized
}
validate_host_port() {
local host=$1 port=$2 label=$3
[[ -n "$host" ]] || die "${label}: host пустой"
[[ "$host" =~ ^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$ ]] \
|| die "${label}: host содержит недопустимые символы"
[[ "$port" =~ ^[0-9]+$ ]] && (( port >= 1 && port <= 65535 )) \
|| die "${label}: некорректный port ${port}"
}
parse_host_port_item() {
local item=$1 label=$2
local host port
item="${item//[[:space:]]/}"
[[ -n "$item" ]] || die "${label}: пустой host:port"
[[ "$item" == *:* ]] || die "${label}: ожидается host:port, получено ${item}"
host="${item%%:*}"
port="${item##*:}"
[[ "$host" != "$port" ]] || die "${label}: отсутствует port в ${item}"
validate_host_port "$host" "$port" "$label"
printf '%s:%s\n' "$host" "$port"
}
collect_permit_opens() {
local item host_port existing duplicate
local -a extra=()
validate_host_port "$PG_HOST" "$PG_PORT" "PG"
PERMIT_OPENS=("${PG_HOST}:${PG_PORT}")
if [[ -n "$EXTRA_PERMIT_OPEN" ]]; then
IFS=',' read -r -a extra <<< "$EXTRA_PERMIT_OPEN"
((${#extra[@]} > 0)) || die "EXTRA_PERMIT_OPEN не содержит endpoint"
for item in "${extra[@]}"; do
host_port=$(parse_host_port_item "$item" "EXTRA_PERMIT_OPEN")
duplicate=false
for existing in "${PERMIT_OPENS[@]}"; do
[[ "$existing" == "$host_port" ]] && duplicate=true
done
if [[ "$duplicate" == "false" ]]; then
PERMIT_OPENS+=("$host_port")
fi
done
fi
}
validate_key_file() {
local file=$1
local lines
[[ -n "$file" && -f "$file" && -s "$file" ]] \
|| die "Не найден TUNNEL_AUTHORIZED_KEY_FILE: ${file:-<empty>}"
ssh-keygen -l -f "$file" >/dev/null 2>&1 || die "Некорректный SSH public key: ${file}"
mapfile -t lines < <(grep -Ev '^[[:space:]]*(#|$)' "$file")
((${#lines[@]} == 1)) || die "Файл ключа должен содержать ровно одну строку без комментариев: ${file}"
[[ "${lines[0]}" =~ ^ssh-ed25519[[:space:]]+[A-Za-z0-9+/=]+([[:space:]].*)?$ ]] \
|| die "Разрешён только чистый ssh-ed25519 без SSH-опций: ${file}"
}
read_ed25519_public_key() {
local file=$1
local line key_type key_data ignored_comment
line=$(grep -Ev '^[[:space:]]*(#|$)' "$file")
IFS=$' \t' read -r key_type key_data ignored_comment <<< "$line"
[[ "$key_type" == "ssh-ed25519" ]] || die "Разрешён только ssh-ed25519"
[[ "$key_data" =~ ^[A-Za-z0-9+/=]+$ ]] || die "Некорректное тело публичного ключа"
ED25519_KEY_TYPE=$key_type
ED25519_KEY_DATA=$key_data
# Входной комментарий не влияет на авторизацию и намеренно не переносится.
ED25519_KEY_COMMENT=han-vm2-db-tunnel
}
build_authorized_keys_line() {
local key_file=$1
local from_prefix="" options host_port
read_ed25519_public_key "$key_file"
options='restrict,port-forwarding'
if [[ -n "$TUNNEL_SOURCE_CIDRS" ]]; then
from_prefix="from=\"${TUNNEL_SOURCE_CIDRS}\","
fi
for host_port in "${PERMIT_OPENS[@]}"; do
options+=",permitopen=\"${host_port}\""
done
if [[ -n "$from_prefix" ]]; then
printf '%s%s %s %s %s\n' \
"$from_prefix" "$options" "$ED25519_KEY_TYPE" "$ED25519_KEY_DATA" "$ED25519_KEY_COMMENT"
else
printf '%s %s %s %s\n' \
"$options" "$ED25519_KEY_TYPE" "$ED25519_KEY_DATA" "$ED25519_KEY_COMMENT"
fi
}
create_or_update_user() {
step "Создание пользователя ${TUNNEL_USER}"
local actual_home actual_shell primary_group home_owner
if id "$TUNNEL_USER" >/dev/null 2>&1; then
IFS=: read -r _ _ _ _ _ actual_home actual_shell < <(getent passwd "$TUNNEL_USER")
primary_group=$(id -gn "$TUNNEL_USER")
[[ "$actual_home" == "/home/${TUNNEL_USER}" ]] \
|| die "Существующий ${TUNNEL_USER} имеет неожиданный home: ${actual_home}"
[[ "$primary_group" == "$TUNNEL_USER" ]] \
|| die "Существующий ${TUNNEL_USER} имеет неожиданную primary group: ${primary_group}"
[[ "$actual_shell" == "/usr/sbin/nologin" ]] \
|| die "Отказываюсь переоборудовать существующего ${TUNNEL_USER} с shell ${actual_shell}"
[[ -d "$actual_home" && ! -L "$actual_home" ]] \
|| die "Home ${actual_home} отсутствует, не является каталогом или является симлинком"
home_owner=$(stat -c '%U' "$actual_home")
[[ "$home_owner" == "$TUNNEL_USER" ]] \
|| die "Home ${actual_home} принадлежит ${home_owner}, ожидался ${TUNNEL_USER}"
log "Найден ранее созданный выделенный пользователь ${TUNNEL_USER}"
else
getent group "$TUNNEL_USER" >/dev/null 2>&1 \
&& die "Группа ${TUNNEL_USER} уже существует без одноимённого пользователя"
useradd --create-home --user-group --home-dir "/home/${TUNNEL_USER}" \
--shell /usr/sbin/nologin "$TUNNEL_USER"
fi
passwd -l "$TUNNEL_USER" >/dev/null
usermod --lock --groups "" "$TUNNEL_USER"
[[ "$(id -nG "$TUNNEL_USER")" == "$TUNNEL_USER" ]] \
|| die "Не удалось удалить дополнительные группы пользователя ${TUNNEL_USER}"
}
build_authorized_keys_candidate() {
local target=$1
build_authorized_keys_line "$TUNNEL_AUTHORIZED_KEY_FILE" > "$target"
# sshd читает AuthorizedKeysFile с правами целевого пользователя.
# Файл остаётся root-owned и поэтому 0644 не позволяет tunnel изменить ключ.
chmod 0644 "$target"
}
build_sshd_tunnel_candidate() {
local target=$1 host_port
{
printf 'Match User %s\n' "$TUNNEL_USER"
printf ' AuthorizedKeysFile %s/%%u\n' "$AUTHORIZED_KEYS_DIR"
echo ' AllowTcpForwarding local'
echo ' AllowStreamLocalForwarding no'
echo ' GatewayPorts no'
echo ' AllowAgentForwarding no'
echo ' X11Forwarding no'
echo ' PermitTTY no'
echo ' PermitTunnel no'
echo ' PermitUserRC no'
echo ' MaxSessions 0'
printf ' PermitOpen'
for host_port in "${PERMIT_OPENS[@]}"; do
printf ' %s' "$host_port"
done
printf '\nMatch all\n'
} > "$target"
chmod 0644 "$target"
}
build_main_config_candidate() {
local target=$1 allow_line user
local -a allow_users=()
[[ -f "$SSHD_MAIN_CONF" ]] || die "Не найден ${SSHD_MAIN_CONF}"
[[ ! -L "$SSHD_MAIN_CONF" ]] || die "${SSHD_MAIN_CONF} не должен быть симлинком"
! grep -Eq '^[[:space:]]*Match([[:space:]]|$)' "$SSHD_MAIN_CONF" \
|| die "${SSHD_MAIN_CONF} должен содержать только глобальные настройки, без Match"
python3 - "$SSHD_MAIN_CONF" "$target" "$TUNNEL_USER" <<'PY'
from pathlib import Path
import re
import sys
source = Path(sys.argv[1])
target = Path(sys.argv[2])
user = sys.argv[3]
lines = source.read_text(encoding="utf-8").splitlines(keepends=True)
matches = [
index for index, line in enumerate(lines)
if re.match(r"^\s*AllowUsers\s+", line)
]
if len(matches) != 1:
print(
f"{source}: ожидалась ровно одна глобальная директива AllowUsers, найдено {len(matches)}",
file=sys.stderr,
)
raise SystemExit(1)
index = matches[0]
tokens = lines[index].split()
if user not in tokens[1:]:
newline = "\n" if lines[index].endswith("\n") else ""
lines[index] = f"AllowUsers {' '.join(tokens[1:] + [user])}{newline}"
Path(target).write_text("".join(lines), encoding="utf-8")
PY
allow_line=$(awk '/^[[:space:]]*AllowUsers[[:space:]]+/ {print; exit}' "$target")
IFS=' ' read -r -a allow_users <<< "$allow_line"
((${#allow_users[@]} >= 2)) || die "AllowUsers не содержит пользователей"
for user in "${allow_users[@]:1}"; do
[[ "$user" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] \
|| die "Неподдерживаемый шаблон AllowUsers: ${user}; перечислите реальные локальные учётные записи"
getent passwd "$user" >/dev/null \
|| die "AllowUsers содержит несуществующего пользователя: ${user}"
done
chmod 0644 "$target"
}
verify_endpoint_from_host() {
local endpoint=$1
local host="${endpoint%%:*}"
local port="${endpoint##*:}"
if command -v nc >/dev/null 2>&1; then
nc -z -w 5 "$host" "$port" \
&& log "TCP ${endpoint} доступен" \
|| die "С VM2 нет TCP-доступа к ${endpoint}; сначала почините private network/firewall"
elif command -v timeout >/dev/null 2>&1; then
timeout 5 bash -c 'echo > "/dev/tcp/$1/$2"' _ "$host" "$port" 2>/dev/null \
&& log "TCP ${endpoint} доступен" \
|| die "С VM2 нет TCP-доступа к ${endpoint}; сначала почините private network/firewall"
else
log "nc и timeout не установлены; пропускаю TCP-проверку"
fi
}
verify_network_from_host() {
local endpoint
step "Проверка сетевой доступности разрешённых endpoint с VM2"
for endpoint in "${PERMIT_OPENS[@]}"; do
verify_endpoint_from_host "$endpoint"
done
}
atomic_install() {
local source=$1 target=$2 mode=$3
local temporary
temporary=$(mktemp "${target}.tmp.XXXXXX") || return 1
if ! install -o root -g root -m "$mode" "$source" "$temporary"; then
rm -f -- "$temporary"
return 1
fi
if ! mv -Tf -- "$temporary" "$target"; then
rm -f -- "$temporary"
return 1
fi
}
backup_file() {
local target=$1 name=$2 backup_dir=$3
if [[ -e "$target" || -L "$target" ]]; then
cp -a -- "$target" "${backup_dir}/${name}"
: > "${backup_dir}/${name}.existed"
fi
}
restore_file() {
local target=$1 name=$2 backup_dir=$3
rm -f -- "$target"
if [[ -f "${backup_dir}/${name}.existed" ]]; then
cp -a -- "${backup_dir}/${name}" "$target"
fi
}
rollback_transaction() {
local backup_dir=$1
log "Откат SSH-конфигурации и authorized_keys"
restore_file "$SSHD_MAIN_CONF" main.conf "$backup_dir"
restore_file "$SSHD_TUNNEL_CONF" tunnel.conf "$backup_dir"
restore_file "$AUTHORIZED_KEYS_FILE" authorized_keys "$backup_dir"
}
assert_effective_value() {
local config=$1 key=$2 expected=$3 actual
actual=$(awk -v key="$key" '$1 == key {$1=""; sub(/^ /, ""); print; exit}' <<< "$config")
if [[ "$actual" != "$expected" ]]; then
log "Эффективный sshd ${key}=${actual:-<empty>}, ожидалось ${expected}"
return 1
fi
}
verify_effective_sshd_config() {
local effective endpoint permit_open authorized_keys_value allow_user_found=false
local -a effective_permit_opens=()
effective=$(sshd -T -C "user=${TUNNEL_USER},host=localhost,addr=127.0.0.1") \
|| return 1
assert_effective_value "$effective" allowtcpforwarding local || return 1
assert_effective_value "$effective" allowstreamlocalforwarding no || return 1
assert_effective_value "$effective" allowagentforwarding no || return 1
assert_effective_value "$effective" x11forwarding no || return 1
assert_effective_value "$effective" gatewayports no || return 1
assert_effective_value "$effective" permittty no || return 1
assert_effective_value "$effective" permittunnel no || return 1
assert_effective_value "$effective" permituserrc no || return 1
assert_effective_value "$effective" maxsessions 0 || return 1
assert_effective_value "$effective" passwordauthentication no || return 1
assert_effective_value "$effective" kbdinteractiveauthentication no || return 1
assert_effective_value "$effective" pubkeyauthentication yes || return 1
authorized_keys_value=$(awk '
$1 == "authorizedkeysfile" {$1=""; sub(/^ /, ""); print; exit}
' <<< "$effective")
if [[ "$authorized_keys_value" != "${AUTHORIZED_KEYS_DIR}/%u" \
&& "$authorized_keys_value" != "$AUTHORIZED_KEYS_FILE" ]]; then
log "Эффективный AuthorizedKeysFile=${authorized_keys_value:-<empty>}, ожидался root-owned путь"
return 1
fi
permit_open=$(awk '
$1 == "permitopen" {
for (i = 2; i <= NF; i++) values = values " " $i
}
END {sub(/^ /, "", values); print values}
' <<< "$effective")
IFS=' ' read -r -a effective_permit_opens <<< "$permit_open"
if ((${#effective_permit_opens[@]} != ${#PERMIT_OPENS[@]})); then
log "Эффективный PermitOpen содержит неожиданный набор: ${permit_open:-<empty>}"
return 1
fi
for endpoint in "${PERMIT_OPENS[@]}"; do
if [[ " ${permit_open} " != *" ${endpoint} "* ]]; then
log "Эффективный PermitOpen не содержит ${endpoint}"
return 1
fi
done
if awk -v user="$TUNNEL_USER" '
$1 == "allowusers" {
for (i = 2; i <= NF; i++) if ($i == user) found = 1
}
END {exit !found}
' <<< "$effective"; then
allow_user_found=true
fi
if [[ "$allow_user_found" != "true" ]]; then
log "Эффективный AllowUsers не содержит ${TUNNEL_USER}"
return 1
fi
}
detect_ssh_service() {
if systemctl cat ssh.service >/dev/null 2>&1; then
printf 'ssh\n'
elif systemctl cat sshd.service >/dev/null 2>&1; then
printf 'sshd\n'
else
die "Не найден systemd-сервис ssh.service или sshd.service"
fi
}
prepare_authorized_keys_dir() {
local owner mode
if [[ -e "$AUTHORIZED_KEYS_DIR" || -L "$AUTHORIZED_KEYS_DIR" ]]; then
[[ -d "$AUTHORIZED_KEYS_DIR" && ! -L "$AUTHORIZED_KEYS_DIR" ]] \
|| die "${AUTHORIZED_KEYS_DIR} должен быть обычным каталогом, не симлинком"
owner=$(stat -c '%U:%G' "$AUTHORIZED_KEYS_DIR")
[[ "$owner" == "root:root" ]] \
|| die "${AUTHORIZED_KEYS_DIR} принадлежит ${owner}, ожидался root:root"
mode=$(stat -c '%a' "$AUTHORIZED_KEYS_DIR")
(( (8#$mode & 8#022) == 0 )) \
|| die "${AUTHORIZED_KEYS_DIR} доступен для записи группе или остальным: mode ${mode}"
else
install -d -o root -g root -m 0755 "$AUTHORIZED_KEYS_DIR"
fi
}
install_and_reload_sshd() {
step "Атомарная установка, проверка и reload sshd"
local work_dir=$1 main_candidate=$2 tunnel_candidate=$3 key_candidate=$4
local ssh_service
ssh_service=$(detect_ssh_service)
prepare_authorized_keys_dir
backup_file "$SSHD_MAIN_CONF" main.conf "$work_dir"
backup_file "$SSHD_TUNNEL_CONF" tunnel.conf "$work_dir"
backup_file "$AUTHORIZED_KEYS_FILE" authorized_keys "$work_dir"
if ! atomic_install "$main_candidate" "$SSHD_MAIN_CONF" 0644; then
rollback_transaction "$work_dir"
die "Не удалось установить ${SSHD_MAIN_CONF}; выполнен откат"
fi
if ! atomic_install "$tunnel_candidate" "$SSHD_TUNNEL_CONF" 0644; then
rollback_transaction "$work_dir"
die "Не удалось установить ${SSHD_TUNNEL_CONF}; выполнен откат"
fi
if ! sshd -t; then
rollback_transaction "$work_dir"
die "sshd -t не прошёл; исходная конфигурация восстановлена"
fi
if ! verify_effective_sshd_config; then
rollback_transaction "$work_dir"
die "Эффективная конфигурация sshd не прошла проверку; выполнен откат"
fi
if ! atomic_install "$key_candidate" "$AUTHORIZED_KEYS_FILE" 0644; then
rollback_transaction "$work_dir"
die "Не удалось установить ${AUTHORIZED_KEYS_FILE}; выполнен откат"
fi
if ! systemctl reload "$ssh_service"; then
rollback_transaction "$work_dir"
if sshd -t; then
systemctl reload "$ssh_service" \
|| log "КРИТИЧНО: не удалось reload исходной конфигурации ${ssh_service}"
else
log "КРИТИЧНО: исходная конфигурация после отката не проходит sshd -t"
fi
die "Reload ${ssh_service} не выполнен; конфигурация восстановлена"
fi
log "sshd reload выполнен; ключ хранится в root-owned ${AUTHORIZED_KEYS_FILE}"
}
print_usage() {
cat <<EOF
Готово.
Проверка с вашей рабочей машины (не закрывая текущую admin-сессию):
ssh -i ~/.ssh/han_vm2_tunnel -N \\
-L 16433:${PG_HOST}:${PG_PORT} \\
${TUNNEL_USER}@<VM2_PUBLIC_IP>
В другом терминале:
nc -zv 127.0.0.1 16433
Подключение к БД через туннель (пример):
psql "postgresql://<USER>:<PASSWORD>@127.0.0.1:16433/<DB>?sslmode=verify-full&sslrootcert=<PATH_TO_CA>"
Что запрещено для ${TUNNEL_USER}:
- sudo / shell / доступ к секретам приложения
- remote forwarding (-R), произвольный SOCKS, agent/X11
- forwarding на адреса вне PermitOpen
Политика источника ключа:
- CIDR: ${TUNNEL_SOURCE_CIDRS:-не задан}
- осознанно разрешён любой источник: ${TUNNEL_ALLOW_ANY_SOURCE}
EOF
}
main() {
local work_dir main_candidate tunnel_candidate key_candidate
require_root
require_commands
validate_tunnel_user
validate_source_policy
validate_cidrs "$TUNNEL_SOURCE_CIDRS"
validate_key_file "$TUNNEL_AUTHORIZED_KEY_FILE"
collect_permit_opens
verify_network_from_host
create_or_update_user
work_dir=$(mktemp -d /run/setup-tunnel-user.XXXXXX)
trap "rm -rf -- '$work_dir'" EXIT
main_candidate="${work_dir}/main.conf.candidate"
tunnel_candidate="${work_dir}/tunnel.conf.candidate"
key_candidate="${work_dir}/authorized_keys.candidate"
build_main_config_candidate "$main_candidate"
build_sshd_tunnel_candidate "$tunnel_candidate"
build_authorized_keys_candidate "$key_candidate"
install_and_reload_sshd \
"$work_dir" "$main_candidate" "$tunnel_candidate" "$key_candidate"
print_usage
}
main "$@"