(Обновлено) Сегодня будем разворачивать свой VPN на базе OpenConnect сервера (ocserv), работающего поверх HTTPS и который совместим с Cisco Anyconnect. Все это добро мы упакуем в docker контейнер для простоты использования и лёгкой переносимости.
Данный сервер планируется использовать, как основной связующий элемент, с помощью которого будем настраивать сетевое взаимодействие будущих проектов и сервисов. Сама установка сервера выполняется всего в несколько команд. Будет интересно 😉
Пожалуйста не пугайтесь “длинности” поста. В статье описано множество подробностей, но сама процедура развертывания очень проста и выполняется в несколько команд (загляните в TLDR). Настройка клиентов и то занимает больше времени.
TLDR
#############################################################
## при использовании домена для получения SSL сертификатов ##
## расскоментируйте сервис certbot и параметр depends_on ##
## у сервиса openconnect + укажите свои значения вместо ##
## *example* в файле docker-compose.yml ##
#############################################################
### установка и настройка сервера с помощью готового образа ###
# создание директории проекта
mkdir /opt/openconnect && cd /opt/openconnect
# копирование файла docker-compose.yml
curl -sSLO https://raw.githubusercontent.com/r4ven-me/openconnect/main/docker-compose.yml
# запуск OpenConnect сервера
docker compose up -d && docker compose logs -f
# создание пользователя с id "exampleuser" и именем "Example User"
# файл сертификата .p12 будет создан в ./data/secrets
docker exec -it openconnect ocuser exampleuser 'Example User'
# настройка автозапуска с systemd
docker compose down
curl -fLo /etc/systemd/system/openconnect.service https://raw.githubusercontent.com/r4ven-me/openconnect/main/src/server/v1.3-sid/openconnect.service
systemctl daemon-reload
systemctl enable --now openconnect
### пример подключения клиента с помощью утилиты openconnect ###
# без домена
sudo openconnect -c /home/exampleuser/exampleuser.p12 12.345.67.89:43443 <<< $(echo "examplepassword"$'\n'yes$'\n')
# с доменом
sudo openconnect -c /home/exampleuser/exampleuser.p12 example.com:43443 <<< $(echo "examplepassword"$'\n')
Обращаю ваше внимание, что предоставленная в данной статье информация предназначена исключительно для образовательных целей. Любые действия, основанные на этой информации, осуществляются на ваш собственный риск и ответственность.
Как это часто бывает в IT индустрии, если образуется весомый спрос на продукт, который имеет закрытый исходный код, появляется его “открытая” реализация. Самым популярным примером такого явления является Linux.
Как вы могли догадаться, рассматриваемый нами сегодня OpenConnect – это open source реализация ПО для организации VPN сервиса, совместимого с Cisco AnyConnect.
Вот что говорит зарубежная вики про OpenConnect:
OpenConnect is a free and open-source cross-platform multi-protocol virtual private network (VPN) client software which implement secure point-to-point connections.
Wikipedia
Проект OpenConnect также включает в себя сервер, который называется ocserv (OpenConnect server) и тем самым обеспечивает полноценное клиент-серверное VPN решение.
Основные преимущества такого решения:
Кроссплатформенность: клиенты под Linux, Windows, MacOS, Android, iOS, HarmonyOS;
Гибкость настройки, в т.ч. для каждого пользователя;
Шифрование соединения на основе SSL/TLS;
Простота настройки и использования;
Множество способов авторизации + двухфакторная и многое другое.
Предварительная подготовка
Для удобства и простоты использования мы с вами будем разворачивать OpenConnect сервер в docker контейнере, путем сборки образа и его запуска с помощью docker-compose. Обязательным условием является настроенный Linux сервер с установленным и запущенным в нем docker engine. А также наличие root привилегий (для моего примера) или группы docker у пользователя, если будете выполнять установку по своему.
Если с какой-то из пунктов отсутствует, возможно вам будут полезны следующие статьи:
В статье про установку docker’а также есть ссылки на полезные материалы, где доступным языком объясняется, что это такое, для чего нужен и из чего состоит. Если вы ранее не работали с данной технологией контейнерной виртуализации, то я настоятельно рекомендую их к прочтению.
Так, как ocserv работает поверх HTTPS c SSL шифрованием, рекомендованным (но необязательным) условиям является наличие валидного доменного имени. Конкретнее – DNS записи типа A, указывающую на внешний IP адрес вашего сервера. Добавление такой записи производится в настройках DNS вашего провайдера домена. В моём примере используется адрес: vpn.r4ven.me. При настройке OpenConnect адрес DNS необходим для бесплатного получения официальных SSL сертификатов от проекта Let’s Encrypt.
Если домена у вас нет, то при настройке сервера вместо DNS имени указывайте его IP адрес. При такой настройке на VPN клиентах при подключении будет выплывать уведомление о подключении к неподтвержденному источнику. В этом нет ничего страшного, просто это неудобно, и если честно, раздражает)
Схема проекта
Для визуального подкрепления, ниже представлена схема проекта будущего OpenConnect VPN сервера:
Сохранив картинку в голову, приступаем к установке и настройке.
Развертывание OpenConnect VPN сервера в docker
Действия из статьи выполнялись в среде дистрибутива:
Debian 12
Образ docker контейнера с ocserv основан на:
Debian sid
Версия ocserv внутри контейнера:
1.3
Вводные данные
Для быстрого и удобного поднятия своего VPN сервера я подготовил небольшой проект с использованием docker, docker-compose и bash.
Весь процесс настройки сервера OpenConnect внутрни контейнера был автоматизирован.
Вот краткое описание проекта:
Сборка образа docker на основе Debian sid;
Установка в образ сервера ocserv и вспомогательных утилит;
Копирование в образ bash скрипта oscerv.sh;
При первом старте скрипт создает необходимые файлы и папки, в т.ч. основной конфиг ocserv.conf, скрипты подключения/отключения и файлы сертификатов;
Всё это сохраняется в docker volume, который монтируется в папочку ./data;
Файлы сервера можно с легкостью переносить на другие системы/платформы, где работает docker (просто перенесите папку /opt/openconnect);
Конфиг файлы сервера можно кастомизировать под свои нужды;
При старте контейнера скрипт oscserv.sh проверяет наличие файлов в директории ./data, если они есть, то просто запускается сервер, если нет, файлы создаются заново, и после запускается сервер;
Все необходимые значения для конфигов и сертификатов берутся из переменных окружения, определенные в файле .env (если не указать, будут использованы дефолтные значения);
Так, воды налил, теперь давайте приступать к установке и настройке нашего сервера.
Обновление системы
Первым делом рекомендуется обновить вашу систему до актуального состояния:
sudo -s
apt update && apt upgrade -y
Клонирование репозитория с исходными файлами проекта
Для клонирования git репозитория нам потребуется утилита командной строки, из состава пакета одноимённой системы контроля версий – git. Если эта система у вас не установлена, то выполняем:
apt install git
Теперь клонируем репозиторий с подготовленными файлами из моего GitHub, далее копируем директорию v1.3-sid по пути /opt/openconnect и переходим в неё:
ocserv.sh - bash скрипт, запускаемый при старте контейнера, который выполняет начальную конфигурацию и инициализацию процесса ocserv
#!/bin/bash
# Some protection
set -Eeuo pipefail
# Define default server vars if they are not set
SRV_CN="${SRV_CN:=example.com}"
SRV_CA="${SRV_CA:=Example CA}"
OTP_ENABLE="${OTP_ENABLE:=false}"
OTP_SEND_BY_EMAIL="${OTP_SEND_BY_EMAIL:=false}"
OTP_SEND_BY_TELEGRAM="${OTP_SEND_BY_TELEGRAM:=false}"
MSMTP_HOST="${MSMTP_HOST:=smtp.example.com}"
MSMTP_PORT="${MSMTP_PORT:=465}"
MSMTP_USER="${MSMTP_USER:=mail@example.com}"
MSMTP_PASSWORD="${MSMTP_PASSWORD:=PaSsw0rD}"
MSMTP_FROM="${MSMTP_FROM:=mail@example.com}"
TG_TOKEN="${TG_TOKEN:=1234567890:QWERTYuio-PA1DFGHJ2_KlzxcVBNmqWEr3t}"
# Ocserv vars (do not modify)
OCSERV_DIR="/etc/ocserv"
CERTS_DIR="${OCSERV_DIR}/certs"
SSL_DIR="${OCSERV_DIR}/ssl"
SECRETS_DIR="${OCSERV_DIR}/secrets"
SCRIPTS_DIR="${OCSERV_DIR}/scripts"
# Create certs dirs
for sub_dir in "${OCSERV_DIR}"/{"ssl/live/${SRV_CN}","certs","secrets","scripts"}; do
if [[ ! -d "$sub_dir" ]]; then
mkdir -p "$sub_dir"
fi
done
if [[ -r /usr/share/doc/ocserv/sample.config && ! -e "${OCSERV_DIR}"/sample.config ]]; then
cp /usr/share/doc/ocserv/sample.config "${OCSERV_DIR}"/
fi
# Create ocserv config file
if [[ ! -e "${OCSERV_DIR}"/ocserv.conf ]]; then
cat << _EOF_ > "${OCSERV_DIR}"/ocserv.conf
auth = "certificate"
#auth = "plain[passwd=${OCSERV_DIR}/ocpasswd]"
#auth = "plain[passwd=/etc/ocserv/ocpasswd,otp=/etc/ocserv/secrets/users.oath]"
#enable-auth = "certificate"
#enable-auth = "pam"
tcp-port = 443
socket-file = /run/ocserv-socket
server-cert = ${SSL_DIR}/live/${SRV_CN}/fullchain.pem
server-key = ${SSL_DIR}/live/${SRV_CN}/privkey.pem
ca-cert = ${CERTS_DIR}/ca-cert.pem
isolate-workers = true
max-clients = 20
max-same-clients = 2
rate-limit-ms = 200
server-stats-reset-time = 604800
keepalive = 10
dpd = 120
mobile-dpd = 1800
switch-to-tcp-timeout = 25
try-mtu-discovery = false
cert-user-oid = 0.9.2342.19200300.100.1.1
tls-priorities = "NORMAL:%SERVER_PRECEDENCE:%COMPAT:-VERS-SSL3.0:-VERS-TLS1.0:-VERS-TLS1.1:-VERS-TLS1.3"
auth-timeout = 1000
min-reauth-time = 300
max-ban-score = 100
ban-reset-time = 1200
cookie-timeout = 600
deny-roaming = false
rekey-time = 172800
rekey-method = ssl
connect-script = ${SCRIPTS_DIR}/connect
disconnect-script = ${SCRIPTS_DIR}/disconnect
use-occtl = true
pid-file = /run/ocserv.pid
log-level = 1
device = vpns
predictable-ips = true
default-domain = $SRV_CN
ipv4-network = 10.10.10.0
ipv4-netmask = 255.255.255.0
tunnel-all-dns = true
dns = 8.8.8.8
ping-leases = false
config-per-user = ${OCSERV_DIR}/config-per-user/
cisco-client-compat = true
dtls-legacy = true
client-bypass-protocol = false
crl = /etc/ocserv/certs/crl.pem
#camouflage = true
#camouflage_secret = "secretword"
#camouflage_realm = "Welcome to admin panel"
_EOF_
fi
# Create template for CA SSL cert
if [[ ! -e "${CERTS_DIR}"/ca.tmpl ]]; then
cat << _EOF_ > "${CERTS_DIR}"/ca.tmpl
organization = $SRV_CN
cn = $SRV_CA
serial = 001
expiration_days = -1
ca
signing_key
cert_signing_key
crl_signing_key
_EOF_
fi
# Create template for users SSL certs
if [[ ! -e "${CERTS_DIR}"/users.cfg ]]; then
cat << _EOF_ > "${CERTS_DIR}"/users.cfg
organization = $SRV_CN
cn = Example User
uid = exampleuser
expiration_days = -1
tls_www_client
signing_key
encryption_key
_EOF_
fi
# Create template for server self-signed SSL cert
if [[ ! -e "${SSL_DIR}"/server.tmpl ]]; then
cat << _EOF_ > "${SSL_DIR}"/server.tmpl
cn = $SRV_CA
dns_name = $SRV_CN
organization = $SRV_CN
expiration_days = -1
signing_key
encryption_key #only if the generated key is an RSA one
tls_www_server
_EOF_
fi
# Generate empty revoke file
if [[ ! -e "${CERTS_DIR}"/crl.tmpl ]]; then
cat << _EOF_ > "${CERTS_DIR}"/crl.tmpl
crl_next_update = 365
crl_number = 1
_EOF_
fi
# Create connect script which runs for every user connection
if [[ ! -e "${SCRIPTS_DIR}"/connect ]]; then
cat << _EOF_ > "${SCRIPTS_DIR}"/connect && chmod +x "${SCRIPTS_DIR}"/connect
#!/bin/bash
set -Eeuo pipefail
echo "\$(date) User \${USERNAME} Connected - Server: \${IP_REAL_LOCAL} VPN IP: \${IP_REMOTE} Remote IP: \${IP_REAL} Device:\${DEVICE}"
echo "Running iptables MASQUERADE for User \${USERNAME} connected with VPN IP \${IP_REMOTE}"
iptables -t nat -A POSTROUTING -s "\${IP_REMOTE}"/32 -o eth0 -j MASQUERADE
_EOF_
fi
# Create disconnect script which runs for every user disconnection
if [[ ! -e "${SCRIPTS_DIR}"/disconnect ]]; then
cat << _EOF_ > "${SCRIPTS_DIR}"/disconnect && chmod +x "${SCRIPTS_DIR}"/disconnect
#!/bin/bash
set -Eeuo pipefail
echo "\$(date) User \${USERNAME} Disconnected - Bytes In: \${STATS_BYTES_IN} Bytes Out: \${STATS_BYTES_OUT} Duration:\${STATS_DURATION}"
iptables -t nat -D POSTROUTING -s "\${IP_REMOTE}"/32 -o eth0 -j MASQUERADE
_EOF_
fi
# Create script to create new users
if [[ ! -e "${SCRIPTS_DIR}"/ocuser ]]; then
cat << _EOF_ > "${SCRIPTS_DIR}"/ocuser && chmod +x "${SCRIPTS_DIR}"/ocuser
#!/bin/bash
set -Eeuo pipefail
# Check and set script params
if [[ \$# -eq 2 ]]; then
USER_UID="\$1"
USER_CN="\$2"
elif [[ \$# -eq 3 ]]; then
if [[ "\$1" == "-A" ]]; then
USER_UID="\$2"
USER_CN="\$3"
else
echo "Use -A key as a first param to generate cert for IOS devices" >&2
exit 1
fi
else
echo "Please run script with two params: username and 'Common Username'" >&2
echo "Example: ocuser john 'John Doe'" >&2
echo "For IOS or HarmonyOS devices add -A key as first param in command" >&2
echo "Example: ocuser -A steve 'Steve Jobs'" >&2
exit 1
fi
# Modify user cert template and generate user key, cert and protected .p12 file
sed -i -e "s/^organization.*/organization = \$SRV_CN/" -e "s/^cn.*/cn = \$USER_CN/" -e "s/^uid.*/uid = \$USER_UID/g" "\${CERTS_DIR}"/users.cfg
echo "\$(tr -cd "[:alnum:]" < /dev/urandom | head -c 60)" | ocpasswd -c "\${OCSERV_DIR}"/ocpasswd "\$USER_UID"
certtool --generate-privkey --outfile "\${CERTS_DIR}"/"\${USER_UID}"-privkey.pem
certtool --generate-certificate --load-privkey "\${CERTS_DIR}"/"\${USER_UID}"-privkey.pem --load-ca-certificate "\${CERTS_DIR}"/ca-cert.pem --load-ca-privkey "\${CERTS_DIR}"/ca-key.pem --template "\${CERTS_DIR}"/users.cfg --outfile "\${CERTS_DIR}"/"\${USER_UID}"-cert.pem
if [[ "\$1" == "-A" ]]; then
sleep 1 && certtool --to-p12 --load-certificate "\${CERTS_DIR}"/"\${USER_UID}"-cert.pem --load-privkey "\${CERTS_DIR}"/"\${USER_UID}"-privkey.pem --pkcs-cipher 3des-pkcs12 --hash SHA1 --outder --outfile "\${SECRETS_DIR}"/"\${USER_UID}".p12
else
sleep 1 && certtool --load-certificate "\${CERTS_DIR}"/"\${USER_UID}"-cert.pem --load-privkey "\${CERTS_DIR}"/"\${USER_UID}"-privkey.pem --pkcs-cipher aes-256 --to-p12 --outder --outfile "\${SECRETS_DIR}"/"\${USER_UID}".p12
fi
_EOF_
fi
# Add revoke script
if [[ ! -e "${SCRIPTS_DIR}"/ocrevoke ]]; then
cat << _EOF_ > "${SCRIPTS_DIR}"/ocrevoke && chmod +x "${SCRIPTS_DIR}"/ocrevoke
#!/bin/bash
set -Eeuo pipefail
if [[ ! -e "\${CERTS_DIR}"/crl.tmpl ]]; then
cat << __EOF__ > "\${CERTS_DIR}"/crl.tmpl
crl_next_update = 365
crl_number = 1
__EOF__
fi
if [[ \$# -eq 1 ]]; then
if [[ "\$1" == "HELP" ]]; then
echo "Usage:
CMD to revoke cert of some user: ocrevoke <exist_user>
CMD to apply current revoked.pem: ocrevoke RELOAD
CMD to reset all revokes: ocrevoke RESET
CMD to print this help: ocrevoke HELP"
elif [[ "\$1" == "RESET" ]]; then
certtool --generate-crl --load-ca-privkey "\${CERTS_DIR}"/ca-key.pem --load-ca-certificate "\${CERTS_DIR}"/ca-cert.pem --template "\${CERTS_DIR}"/crl.tmpl --outfile "\${CERTS_DIR}"/crl.pem
occtl reload
elif [[ "\$1" == "RELOAD" ]]; then
certtool --generate-crl --load-ca-privkey "\${CERTS_DIR}"/ca-key.pem --load-ca-certificate "\${CERTS_DIR}"/ca-cert.pem --load-certificate "\${CERTS_DIR}"/revoked.pem --template "\${CERTS_DIR}"/crl.tmpl --outfile "\${CERTS_DIR}"/crl.pem
else
USER_UID="\$1"
cat "\${CERTS_DIR}"/"\${USER_UID}"-cert.pem >> "\${CERTS_DIR}"/revoked.pem
certtool --generate-crl --load-ca-privkey "\${CERTS_DIR}"/ca-key.pem --load-ca-certificate "\${CERTS_DIR}"/ca-cert.pem --load-certificate "\${CERTS_DIR}"/revoked.pem --template "\${CERTS_DIR}"/crl.tmpl --outfile "\${CERTS_DIR}"/crl.pem
occtl reload
fi
else
echo "Usage:
CMD to revoke cert of some user: ocrevoke <exist_user>
CMD to apply current revoked.pem: ocrevoke RELOAD
CMD to reset all revokes: ocrevoke RESET
CMD to print this help: ocrevoke HELP"
fi
_EOF_
fi
# Add ocuser2fa script
if [[ "$OTP_ENABLE" == "true" && ! -e "${SCRIPTS_DIR}"/ocuser2fa ]]; then
cat << _EOF_ > "${SCRIPTS_DIR}"/ocuser2fa && chmod +x "${SCRIPTS_DIR}"/ocuser2fa
#!/bin/bash
set -Eeuo pipefail
if [[ \$# -eq 1 ]]; then
USER_ID="\$1"
OTP_SECRET="\$(head -c 16 /dev/urandom | xxd -c 256 -ps)"
OTP_SECRET_BASE32="\$(echo 0x"\${OTP_SECRET}" | xxd -r -c 256 | base32)"
OTP_SECRET_QR="otpauth://totp/\$USER_ID?secret=\$OTP_SECRET_BASE32&issuer=$SRV_CA&algorithm=SHA1&digits=6&period=30"
if [[ ! -e "\${SECRETS_DIR}"/users.oath ]] || ! grep -qP "(?<!\\S)\${USER_ID}(?!\\S)" "\${SECRETS_DIR}"/users.oath; then
echo "HOTP/T30 \$USER_ID - \$OTP_SECRET" >> "\${SECRETS_DIR}"/users.oath
echo "OTP secret for \$USER_ID: \$OTP_SECRET"
echo "OTP secret in base32: \$OTP_SECRET_BASE32"
echo "OTP secret in QR code:"
qrencode -t ANSIUTF8 "\$OTP_SECRET_QR"
qrencode "\$OTP_SECRET_QR" -s 10 -o "\${SECRETS_DIR}"/otp_"\${USER_ID}".png
echo "TOTP secret in png image saved at: \${SECRETS_DIR}/otp_\${USER_ID}.png"
send_qr_by_email() {
EMAIL_REGEX="^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
if [[ \$USER_ID =~ \$EMAIL_REGEX ]]; then
cat << EOF | msmtp --file="\${SCRIPTS_DIR}"/msmtprc "\$USER_ID"
Subject: TOTP QR code for OpenConnect auth
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="boundary"
--boundary
Content-Type: text/plain
TOTP secret for OpenConnect (base32):
\$OTP_SECRET_BASE32
--boundary
Content-Type: image/png; name="file.png"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="file.png"
\$(base64 "\${SECRETS_DIR}"/otp_"\${USER_ID}".png)
--boundary--
EOF
echo "[\$(date '+%F %T')] - TOTP secret and QR code successfully sent to \$USER_ID via Email" | tee -a "\${OCSERV_DIR}"/pam.log
else
return 0
fi
}
if [[ "\$OTP_SEND_BY_EMAIL" == "true" ]]; then send_qr_by_email; fi
send_qr_by_telegram() {
TG_REGEX="^[a-zA-Z][a-zA-Z0-9_]{4,31}\$"
if [[ \$USER_ID =~ \$TG_REGEX ]]; then
TG_MESSAGE="TOTP secret for OpenConnect (base32):
\$OTP_SECRET_BASE32"
TG_USER_FILE="\${SCRIPTS_DIR}/tg_users.txt"
if grep -qP "(?<!\\S)\${USER_ID}(?!\\S)" "\$TG_USER_FILE" 2> /dev/null; then
TG_CHAT_ID=\$(grep -P "(?<!\\S)\${USER_ID}(?!\\S)" "\$TG_USER_FILE" | awk '{print \$1}')
else
TG_RESPONSE="\$(curl -s "https://api.telegram.org/bot\$TG_TOKEN/getUpdates")"
TG_CHAT_ID=\$(echo "\$TG_RESPONSE" | jq -r --arg USERNAME "\$USER_ID" '.result[] | select(.message.from.username == \$USERNAME) | .message.chat.id')
if [[ -z "\$TG_CHAT_ID" ]]; then
echo "[\$(date '+%F %T')] - User was not found or did not interact with the bot" >> "\${OCSERV_DIR}"/pam.log
return 0
fi
echo "\$TG_CHAT_ID \$USER_ID" >> "\$TG_USER_FILE"
fi
curl -s -X POST "https://api.telegram.org/bot\$TG_TOKEN/sendPhoto" \\
-H "Content-Type: multipart/form-data" \\
-F "chat_id=\$TG_CHAT_ID" \\
-F "photo=@\${SECRETS_DIR}/otp_\${USER_ID}.png" \\
-F "caption=\$TG_MESSAGE" > /dev/null 2>> "\${OCSERV_DIR}"/pam.log
echo "[\$(date '+%F %T')] - TOTP secret and QR code successfully sent to \$USER_ID via Telegram" | tee -a "\${OCSERV_DIR}"/pam.log
fi
}
if [[ "\$OTP_SEND_BY_TELEGRAM" == "true" ]]; then send_qr_by_telegram; fi
else
echo "OTP token already exists for \$USER_ID in \${SECRETS_DIR}/users.oath"
exit 1
fi
else
echo "Usage: \$(basename "\$0") <user_id>"
exit 1
fi
_EOF_
fi
if [[ "$OTP_ENABLE" == "true" && ! -e "${SCRIPTS_DIR}"/otp_sender ]]; then
cat << _EOF_ > "${SCRIPTS_DIR}"/otp_sender && chmod +x "${SCRIPTS_DIR}"/otp_sender
#!/bin/bash
set -Eeuo pipefail
OCSERV_DIR="$OCSERV_DIR"
SECRETS_DIR="$SECRETS_DIR"
SCRIPTS_DIR="$SCRIPTS_DIR"
OTP_SEND_BY_EMAIL="$OTP_SEND_BY_EMAIL"
OTP_SEND_BY_TELEGRAM="$OTP_SEND_BY_TELEGRAM"
TG_TOKEN="$TG_TOKEN"
echo "[\$(date '+%F %T')] - PAM user \$PAM_USER is trying to connect to ocserv" >> "\${OCSERV_DIR}"/pam.log
otp_sender_by_email() {
EMAIL_REGEX="^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
if [[ \$PAM_USER =~ \$EMAIL_REGEX ]]; then true; else return 0; fi
if [[ -e "\${SECRETS_DIR}"/users.oath ]] && grep -qP "(?<!\\S)\${PAM_USER}(?!\\S)" "\${SECRETS_DIR}"/users.oath; then
OTP_TOKEN="\$(oathtool --totp=SHA1 --time-step-size=30 --digits=6 \$(grep -P "(?<!\\S)\${PAM_USER}(?!\\S)" \${SECRETS_DIR}/users.oath | awk '{print \$4}'))"
echo -e "Subject: TOTP token for OpenConnect\n\n\${OTP_TOKEN}" | msmtp --file="\${SCRIPTS_DIR}"/msmtprc "\$PAM_USER"
echo "[\$(date '+%F %T')] - TOTP token successfully sent to \$PAM_USER" >> "\${OCSERV_DIR}"/pam.log
fi
}
otp_sender_by_telegram() {
TG_REGEX="^[a-zA-Z][a-zA-Z0-9_]{4,31}\$"
if [[ \$PAM_USER =~ \$TG_REGEX ]]; then true; else return 0; fi
if grep -qP "(?<!\\S)\${PAM_USER}(?!\\S)" "\${SECRETS_DIR}"/users.oath 2> /dev/null; then
OTP_TOKEN="\$(oathtool --totp=SHA1 --time-step-size=30 --digits=6 \$(grep -P "(?<!\\S)\${PAM_USER}(?!\\S)" \${SECRETS_DIR}/users.oath | awk '{print \$4}'))"
TG_MESSAGE="TOTP token for OpenConnect: \$OTP_TOKEN"
TG_USER_FILE="\${SCRIPTS_DIR}/tg_users.txt"
if grep -qP "(?<!\\S)\$PAM_USER(?!\\S)" "\$TG_USER_FILE"; then
TG_CHAT_ID=\$(grep -P "(?<!\\S)\${PAM_USER}(?!\\S)" "\$TG_USER_FILE" | awk '{print \$1}')
else
TG_RESPONSE="\$(curl -s "https://api.telegram.org/bot\$TG_TOKEN/getUpdates")"
TG_CHAT_ID=\$(echo "\$TG_RESPONSE" | jq -r --arg USERNAME "\$PAM_USER" '.result[] | select(.message.from.username == \$USERNAME) | .message.chat.id')
if [[ -z "\$TG_CHAT_ID" ]]; then
echo "[\$(date '+%F %T')] - User was not found or did not interact with the bot" >> "\${OCSERV_DIR}"/pam.log
return 0
fi
echo "\$TG_CHAT_ID \$PAM_USER" >> "\$TG_USER_FILE"
fi
curl -s -X POST "https://api.telegram.org/bot\$TG_TOKEN/sendMessage" -d "chat_id=\$TG_CHAT_ID" -d "text=\$TG_MESSAGE" 2>> "\${OCSERV_DIR}"/pam.log
echo "[\$(date '+%F %T')] - TOTP token successfully sent to \$PAM_USER" >> "\${OCSERV_DIR}"/pam.log
fi
}
if [[ "\$OTP_SEND_BY_EMAIL" == "true" ]]; then otp_sender_by_email; fi &
if [[ "\$OTP_SEND_BY_TELEGRAM" == "true" ]]; then otp_sender_by_telegram; fi &
_EOF_
elif [[ "$OTP_ENABLE" == "true" && -e "${SCRIPTS_DIR}"/otp_sender ]]; then
sed -i "s|OCSERV_DIR=.*|OCSERV_DIR=\"$OCSERV_DIR\"|" "${SCRIPTS_DIR}"/otp_sender
sed -i "s|SECRETS_DIR=.*|SECRETS_DIR=\"$SECRETS_DIR\"|" "${SCRIPTS_DIR}"/otp_sender
sed -i "s|SCRIPTS_DIR=.*|SCRIPTS_DIR=\"$SCRIPTS_DIR\"|" "${SCRIPTS_DIR}"/otp_sender
sed -i "s|OTP_SEND_BY_EMAIL=.*|OTP_SEND_BY_EMAIL=\"$OTP_SEND_BY_EMAIL\"|" "${SCRIPTS_DIR}"/otp_sender
sed -i "s|OTP_SEND_BY_TELEGRAM=.*|OTP_SEND_BY_TELEGRAM=\"$OTP_SEND_BY_TELEGRAM\"|" "${SCRIPTS_DIR}"/otp_sender
sed -i "s|TG_TOKEN=.*|TG_TOKEN=\"$TG_TOKEN\"|" "${SCRIPTS_DIR}"/otp_sender
fi
# Add msmtprc config
if [[ "$OTP_ENABLE" == "true" && "$OTP_SEND_BY_EMAIL" == "true" && ! -e "${OCSERV_DIR}"/msmtprc ]]; then
cat << _EOF_ > "${SCRIPTS_DIR}"/msmtprc && chmod 400 "${SCRIPTS_DIR}"/msmtprc
account default
host $MSMTP_HOST
port $MSMTP_PORT
auth on
user $MSMTP_USER
password $MSMTP_PASSWORD
from $MSMTP_FROM
tls on
tls_starttls off
logfile $OCSERV_DIR/pam.log
_EOF_
fi
# Config OTP with PAM
pam_otp() {
if [[ $OTP_ENABLE == "true" ]]; then
until [[ -e /etc/pam.d/ocserv ]]; do sleep 5; done
if grep -q 'otp_sender' /etc/pam.d/ocserv && grep -q 'users.oath' /etc/pam.d/ocserv; then return 0; fi
sleep 3
echo "auth optional pam_exec.so ${SCRIPTS_DIR}/otp_sender" >> /etc/pam.d/ocserv
echo "auth requisite pam_oath.so debug usersfile=${SECRETS_DIR}/users.oath window=20" >> /etc/pam.d/ocserv
fi
}
# Start ocserv service
if [[ -e "${SSL_DIR}"/live/"${SRV_CN}"/privkey.pem && -e "${SSL_DIR}"/live/"${SRV_CN}"/fullchain.pem && -e "${CERTS_DIR}"/ca-key.pem && -e "${CERTS_DIR}"/ca-cert.pem ]]; then
pam_otp &
echo "Starting OpenConnect Server"
exec "$@" || { echo "Starting failed" >&2; exit 1; }
else
# Server certificates generation
certtool --generate-privkey --outfile "${CERTS_DIR}"/ca-key.pem
certtool --generate-self-signed --load-privkey "${CERTS_DIR}"/ca-key.pem --template "${CERTS_DIR}"/ca.tmpl --outfile "${CERTS_DIR}"/ca-cert.pem
certtool --generate-crl --load-ca-privkey "${CERTS_DIR}"/ca-key.pem --load-ca-certificate "${CERTS_DIR}"/ca-cert.pem --template "${CERTS_DIR}"/crl.tmpl --outfile "${CERTS_DIR}"/crl.pem
if [[ ! -e "${SSL_DIR}"/live/"${SRV_CN}"/privkey.pem && ! -e "${SSL_DIR}"/live/"${SRV_CN}"/fullchain.pem ]]; then
certtool --generate-privkey --outfile "${SSL_DIR}"/live/"${SRV_CN}"/privkey.pem
certtool --generate-certificate --load-privkey "${SSL_DIR}"/live/"${SRV_CN}"/privkey.pem --load-ca-certificate "${CERTS_DIR}"/ca-cert.pem --load-ca-privkey "${CERTS_DIR}"/ca-key.pem --template "${SSL_DIR}"/server.tmpl --outfile "${SSL_DIR}"/live/"${SRV_CN}"/fullchain.pem
fi
pam_otp &
echo "Starting OpenConnect Server"
exec "$@" || { echo "Starting failed" >&2; exit 1; }
fi
openconnect.service - файл unit'а systemd для автозапуска
[Unit]
Description=OpenConnect VPN service
Requires=docker.service
After=docker.service
[Service]
Restart=on-failure
RestartSec=5
WorkingDirectory=/opt/openconnect
ExecStart=/usr/bin/docker compose up
ExecStop=/usr/bin/docker compose down
[Install]
WantedBy=multi-user.target
ssl_update.sh - bash скрипт обновления SSL сертификатов от Let's Encrypt
#!/usr/bin/env bash
set -e
WORK_DIR="/opt/openconnect"
CONTAINER_NAME="openconnect"
to_log () {
local text="$1"
echo "[$(date '+%F %T')] ${text}"
}
cd "$WORK_DIR" || exit 1
if [[ -r ./docker-compose.yml ]]; then
to_log "Run certbot service container"
docker compose up certbot
sleep 3
to_log "Reload ocserv config"
docker exec "$CONTAINER_NAME" occtl reload
to_log "Delete all unused docker images"
docker system prune -af
fi
Переопределение переменных: домен, имя сервера, email и пр.
Теперь необходимо задать переменные окружения, которые будут использованы для автоматической подстановки при сборке и запуске контейнера. Для этого открываем любым редактором файл .env, лежащий в директории с файлами проекта:
Файл .env обычно используется для определения переменных окружения, которые считываются утилитой docker-compose, при обращении к ним в файле описания сервисов – docker-compose.yml. Также эти переменные мы передадим внутрь контейнера openconnect.
vim .env
Всего 5 переменных:
TZ – временная зона;
SRV_PORT – сетевой порт подключения к серверу;
SRV_CN – тут задаём доменное имя, которое указывает на Linux сервер (если домена нет, то указываем произвольное имя);
SRV_CA – названия центра сертификации для самоподписанных сертификатов (произвольное, задать обязательно);
USER_EMAIL – адрес электронной почты, если используется доменное имя. Данный параметр обязателен для получения SSL сертификатов от letsencrypt.
В комментариях подсказали, что в случае подключения к ocserv роутером Keenetic, “то порт в .env нужно ставить дефолтный 443 (который не нужно указывать в строке подключения)“. Иные порты, данный роутер, игнорирует.
Сборка образа и первый запуск контейнера с помощью docker-compose
Если у вас нет домена, переходите к шагам внутри спойлера ниже: “Клик сюда, если домена нет”.
Теперь просто запускаем команду сборки образа с последующим запуском контейнера в фоновом режиме и после подключаемся к стандартному выводу docker-compose:
docker compose up -d && docker compose logs -f
Обратите внимание, что сервис openconnect, описанный в файле docker-compose.yml намеренно ограничен в аппаратных ресурсах на использование cpus: '0.50' и memory: 200M, т.е максимально разрешенное использование CPU составляет 50% одного ядра и 200 мб RAM. При необходимости скорректируйте данные параметры в соответствии со своими потребностями.
Подробнее про лимиты ресурсов сервисов при использовании docker-compose читайте тут.
В docker-compose.yml параметром depends-on указан порядок запуска сервисов. Сперва отрабатывает certbot (Let’s Encrypt), который запрашивает и получает валидные SSL сертификаты, и только затем стартует контейнер с VPN сервером:
Подробную документацию по использованию certbot смотрите тут.
В случае успеха вывод будет, как на скрине выше. Просмотр вывода можно прервать комбинацией Ctrl+C.
Чтобы убедится, что контейнер запущен выполняем:
docker compose ps
ls -l ./data
ls -l ./data/ssl/live/vpn.r4ven.me/
Должна создаться папка ./data с файлами сервера + сертификаты, полученные контейнером cerbot.
ЛКМ для просмотра описания содержимого папки data, которое создаёт скрипт ocserv.sh при первом старте
ocserv.conf – файл конфигурации нашего VPN сервера. Также можете модифицировать под свои нужды. К слову, полный конфиг ocserv с описанием доступных параметров можно изучить на официальном GitLab проекта по ссылке;
bin – директория с внутренними скриптами/командами контейнера openconnect:
connect – bash скрипт, который выполняется для каждого клиента при подключении к нашему VPN серверу. Его можно расширять и кастоимизировать под любые ваши нужды. Главное не нарушьте инзначальную функциональность);
#!/bin/bash
set -Eeuo pipefail
echo "$(date) User ${USERNAME} Connected - Server: ${IP_REAL_LOCAL} VPN IP: ${IP_REMOTE} Remote IP: ${IP_REAL} Device:${DEVICE}"
echo "Running iptables MASQUERADE for User ${USERNAME} connected with VPN IP ${IP_REMOTE}"
iptables -t nat -A POSTROUTING -s ${IP_REMOTE}/32 -o eth0 -j MASQUERADE
disconnect – bash скрипт, который выполняется при отключении пользователя;
ocuser – bash скрипт для создания новых пользователей и сертификатов подключения .p12;
#!/bin/bash
set -Eeuo pipefail
# Check and set script params
if [[ $# -eq 2 ]]; then
USER_UID="$1"
USER_CN="$2"
elif [[ $# -eq 3 ]]; then
if [[ "$1" == "-A" ]]; then
USER_UID="$2"
USER_CN="$3"
else
echo "Use -A key as a first param to generate cert for IOS devices" >&2
exit 1
fi
else
echo "Please run script with two params: username and 'Common Username'" >&2
echo "Example: ocuser john 'John Doe'" >&2
echo "For IOS or HarmonyOS devices add -A key as first param in command" >&2
echo "Example: ocuser -A steve 'Steve Jobs'" >&2
exit 1
fi
# Modify user cert template and generate user key, cert and protected .p12 file
sed -i -e "s/^organization.*/organization = $SRV_CN/" -e "s/^cn.*/cn = $USER_CN/" -e "s/^uid.*/uid = $USER_UID/g" "${CERTS_DIR}"/users.cfg
echo "$(tr -cd "[:alnum:]" < /dev/urandom | head -c 60)" | ocpasswd -c "${OCSERV_DIR}"/ocpasswd "$USER_UID"
certtool --generate-privkey --outfile "${CERTS_DIR}"/"${USER_UID}"-privkey.pem
certtool --generate-certificate --load-privkey "${CERTS_DIR}"/"${USER_UID}"-privkey.pem --load-ca-certificate "${CERTS_DIR}"/ca-cert.pem --load-ca-privkey "${CERTS_DIR}"/ca-key.pem --template "${CERTS_DIR}"/users.cfg --outfile "${CERTS_DIR}"/"${USER_UID}"-cert.pem
if [[ "$1" == "-A" ]]; then
sleep 1 && certtool --to-p12 --load-certificate "${CERTS_DIR}"/"${USER_UID}"-cert.pem --load-privkey "${CERTS_DIR}"/"${USER_UID}"-privkey.pem --pkcs-cipher 3des-pkcs12 --hash SHA1 --outder --outfile "${SECRETS_DIR}"/"${USER_UID}".p12
else
sleep 1 && certtool --load-certificate "${CERTS_DIR}"/"${USER_UID}"-cert.pem --load-privkey "${CERTS_DIR}"/"${USER_UID}"-privkey.pem --pkcs-cipher aes-256 --to-p12 --outder --outfile "${SECRETS_DIR}"/"${USER_UID}".p12
fi
ocuser2fa – bash скрипт для генерации секретов TOTP при использовании двухфакторной авторизации;
#!/bin/bash
set -Eeuo pipefail
if [[ $# -eq 1 ]]; then
USER_ID="$1"
OTP_SECRET="$(head -c 16 /dev/urandom | xxd -c 256 -ps)"
OTP_SECRET_BASE32="$(echo 0x"${OTP_SECRET}" | xxd -r -c 256 | base32)"
OTP_SECRET_QR="otpauth://totp/$USER_ID?secret=$OTP_SECRET_BASE32&issuer=Example CA&algorithm=SHA1&digits=6&period=30"
if [[ ! -e "${SECRETS_DIR}"/users.oath ]] || ! grep -qP "(?<!\S)${USER_ID}(?!\S)" "${SECRETS_DIR}"/users.oath; then
echo "HOTP/T30 $USER_ID - $OTP_SECRET" >> "${SECRETS_DIR}"/users.oath
echo "OTP secret for $USER_ID: $OTP_SECRET"
echo "OTP secret in base32: $OTP_SECRET_BASE32"
echo "OTP secret in QR code:"
qrencode -t ANSIUTF8 "$OTP_SECRET_QR"
qrencode "$OTP_SECRET_QR" -s 10 -o "${SECRETS_DIR}"/otp_"${USER_ID}".png
echo "TOTP secret in png image saved at: ${SECRETS_DIR}/otp_${USER_ID}.png"
send_qr_by_email() {
EMAIL_REGEX="^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
if [[ $USER_ID =~ $EMAIL_REGEX ]]; then
cat << EOF | msmtp --file="${SCRIPTS_DIR}"/msmtprc "$USER_ID"
Subject: TOTP QR code for OpenConnect auth
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="boundary"
--boundary
Content-Type: text/plain
TOTP secret for OpenConnect (base32):
$OTP_SECRET_BASE32
--boundary
Content-Type: image/png; name="file.png"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="file.png"
$(base64 "${SECRETS_DIR}"/otp_"${USER_ID}".png)
--boundary--
EOF
echo "[$(date '+%F %T')] - TOTP secret and QR code successfully sent to $USER_ID via Email" | tee -a "${OCSERV_DIR}"/pam.log
else
return 0
fi
}
if [[ "$OTP_SEND_BY_EMAIL" == "true" ]]; then send_qr_by_email; fi
send_qr_by_telegram() {
TG_REGEX="^[a-zA-Z][a-zA-Z0-9_]{4,31}$"
if [[ $USER_ID =~ $TG_REGEX ]]; then
TG_MESSAGE="TOTP secret for OpenConnect (base32):
$OTP_SECRET_BASE32"
TG_USER_FILE="${SCRIPTS_DIR}/tg_users.txt"
if grep -qP "(?<!\S)${USER_ID}(?!\S)" "$TG_USER_FILE" 2> /dev/null; then
TG_CHAT_ID=$(grep -P "(?<!\S)${USER_ID}(?!\S)" "$TG_USER_FILE" | awk '{print $1}')
else
TG_RESPONSE="$(curl -s "https://api.telegram.org/bot$TG_TOKEN/getUpdates")"
TG_CHAT_ID=$(echo "$TG_RESPONSE" | jq -r --arg USERNAME "$USER_ID" '.result[] | select(.message.from.username == $USERNAME) | .message.chat.id')
if [[ -z "$TG_CHAT_ID" ]]; then
echo "[$(date '+%F %T')] - User was not found or did not interact with the bot" >> "${OCSERV_DIR}"/pam.log
return 0
fi
echo "$TG_CHAT_ID $USER_ID" >> "$TG_USER_FILE"
fi
curl -s -X POST "https://api.telegram.org/bot$TG_TOKEN/sendPhoto" \
-H "Content-Type: multipart/form-data" \
-F "chat_id=$TG_CHAT_ID" \
-F "photo=@${SECRETS_DIR}/otp_${USER_ID}.png" \
-F "caption=$TG_MESSAGE" > /dev/null 2>> "${OCSERV_DIR}"/pam.log
echo "[$(date '+%F %T')] - TOTP secret and QR code successfully sent to $USER_ID via Telegram" | tee -a "${OCSERV_DIR}"/pam.log
fi
}
if [[ "$OTP_SEND_BY_TELEGRAM" == "true" ]]; then send_qr_by_telegram; fi
else
echo "OTP token already exists for $USER_ID in ${SECRETS_DIR}/users.oath"
exit 1
fi
else
echo "Usage: $(basename "$0") <user_id>"
exit 1
fi
otp_sender – bash скрипт для отправки TOTP токенов с помощью Email и Telegram при использовании PAM авторизации и включенном втором факторе (2FA);
#!/bin/bash
set -Eeuo pipefail
OCSERV_DIR="/etc/ocserv"
SECRETS_DIR="/etc/ocserv/secrets"
SCRIPTS_DIR="/etc/ocserv/scripts"
OTP_SEND_BY_EMAIL="true"
OTP_SEND_BY_TELEGRAM="true"
TG_TOKEN="1234567890:QWERTYuio-PA1DFGHJ2_KlzxcVBNmqWEr3t"
echo "[$(date '+%F %T')] - PAM user $PAM_USER is trying to connect to ocserv" >> "${OCSERV_DIR}"/pam.log
otp_sender_by_email() {
EMAIL_REGEX="^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
if [[ $PAM_USER =~ $EMAIL_REGEX ]]; then true; else return 0; fi
if [[ -e "${SECRETS_DIR}"/users.oath ]] && grep -qP "(?<!\S)${PAM_USER}(?!\S)" "${SECRETS_DIR}"/users.oath; then
OTP_TOKEN="$(oathtool --totp=SHA1 --time-step-size=30 --digits=6 $(grep -P "(?<!\S)${PAM_USER}(?!\S)" ${SECRETS_DIR}/users.oath | awk '{print $4}'))"
echo -e "Subject: TOTP token for OpenConnect\n\n${OTP_TOKEN}" | msmtp --file="${SCRIPTS_DIR}"/msmtprc "$PAM_USER"
echo "[$(date '+%F %T')] - TOTP token successfully sent to $PAM_USER" >> "${OCSERV_DIR}"/pam.log
fi
}
otp_sender_by_telegram() {
TG_REGEX="^[a-zA-Z][a-zA-Z0-9_]{4,31}$"
if [[ $PAM_USER =~ $TG_REGEX ]]; then true; else return 0; fi
if grep -qP "(?<!\S)${PAM_USER}(?!\S)" "${SECRETS_DIR}"/users.oath 2> /dev/null; then
OTP_TOKEN="$(oathtool --totp=SHA1 --time-step-size=30 --digits=6 $(grep -P "(?<!\S)${PAM_USER}(?!\S)" ${SECRETS_DIR}/users.oath | awk '{print $4}'))"
TG_MESSAGE="TOTP token for OpenConnect: $OTP_TOKEN"
TG_USER_FILE="${SCRIPTS_DIR}/tg_users.txt"
if grep -qP "(?<!\S)$PAM_USER(?!\S)" "$TG_USER_FILE"; then
TG_CHAT_ID=$(grep -P "(?<!\S)${PAM_USER}(?!\S)" "$TG_USER_FILE" | awk '{print $1}')
else
TG_RESPONSE="$(curl -s "https://api.telegram.org/bot$TG_TOKEN/getUpdates")"
TG_CHAT_ID=$(echo "$TG_RESPONSE" | jq -r --arg USERNAME "$PAM_USER" '.result[] | select(.message.from.username == $USERNAME) | .message.chat.id')
if [[ -z "$TG_CHAT_ID" ]]; then
echo "[$(date '+%F %T')] - User was not found or did not interact with the bot" >> "${OCSERV_DIR}"/pam.log
return 0
fi
echo "$TG_CHAT_ID $PAM_USER" >> "$TG_USER_FILE"
fi
curl -s -X POST "https://api.telegram.org/bot$TG_TOKEN/sendMessage" -d "chat_id=$TG_CHAT_ID" -d "text=$TG_MESSAGE" 2>> "${OCSERV_DIR}"/pam.log
echo "[$(date '+%F %T')] - TOTP token successfully sent to $PAM_USER" >> "${OCSERV_DIR}"/pam.log
fi
}
if [[ "$OTP_SEND_BY_EMAIL" == "true" ]]; then otp_sender_by_email; fi &
if [[ "$OTP_SEND_BY_TELEGRAM" == "true" ]]; then otp_sender_by_telegram; fi &
ocrevoke – bash скрипт для отзыва сертификатов пользователей;
#!/bin/bash
set -Eeuo pipefail
if [[ ! -e "${CERTS_DIR}"/crl.tmpl ]]; then
cat << __EOF__ > "${CERTS_DIR}"/crl.tmpl
crl_next_update = 365
crl_number = 1
__EOF__
fi
if [[ $# -eq 1 ]]; then
if [[ "$1" == "HELP" ]]; then
echo "Usage:
CMD to revoke cert of some user: ocrevoke <exist_user>
CMD to apply current revoked.pem: ocrevoke RELOAD
CMD to reset all revokes: ocrevoke RESET
CMD to print this help: ocrevoke HELP"
elif [[ "$1" == "RESET" ]]; then
certtool --generate-crl --load-ca-privkey "${CERTS_DIR}"/ca-key.pem --load-ca-certificate "${CERTS_DIR}"/ca-cert.pem --template "${CERTS_DIR}"/crl.tmpl --outfile "${CERTS_DIR}"/crl.pem
occtl reload
elif [[ "$1" == "RELOAD" ]]; then
certtool --generate-crl --load-ca-privkey "${CERTS_DIR}"/ca-key.pem --load-ca-certificate "${CERTS_DIR}"/ca-cert.pem --load-certificate "${CERTS_DIR}"/revoked.pem --template "${CERTS_DIR}"/crl.tmpl --outfile "${CERTS_DIR}"/crl.pem
else
USER_UID="$1"
cat "${CERTS_DIR}"/"${USER_UID}"-cert.pem >> "${CERTS_DIR}"/revoked.pem
certtool --generate-crl --load-ca-privkey "${CERTS_DIR}"/ca-key.pem --load-ca-certificate "${CERTS_DIR}"/ca-cert.pem --load-certificate "${CERTS_DIR}"/revoked.pem --template "${CERTS_DIR}"/crl.tmpl --outfile "${CERTS_DIR}"/crl.pem
occtl reload
fi
else
echo "Usage:
CMD to revoke cert of some user: ocrevoke <exist_user>
CMD to apply current revoked.pem: ocrevoke RELOAD
CMD to reset all revokes: ocrevoke RESET
CMD to print this help: ocrevoke HELP"
fi
certs – директория с SSL файлами нашего “центра сертификации”;
secrets – директория, в которой хранятся сертификаты (.p12) пользователей после их создания;
ssl – SSL сертификаты openconnect сервера, самоподписанные или полученные от проекта Let’s Encrypt.
Также проверяем прослушиваемый и проброшенный в хост контейнером TCP порт, такой командой:
ss -tnap | grep -E '43443'
Всё, сервер запущен и работает. Осталось создать пользователей и настроить клиентское подключение.
Клик сюда, если домена нет
В таком случае правим файл описания сервисов docker-compose.yml:
vim docker-compose.yml
В этом файле необходимо закоментировать сервис certbot и директиву depends_on у сервиса openconnect, как показано на скриншоте:
После чего запускаем сборку и смотрим вывод:
docker compose up -d && docker compose logs -f
Если все отработало корректно, выходим из режима просмотра логов клавишей Ctrl+C и проверяем запущенный сервис:
docker compose ps
ss -tnap | grep -E '43443'
Также проверяем наличие системных файлов ocserv:
ls -l ./data
Отлично, идём дальше.
Создание пользователей
Пару слов про способы авторизации на OpenConnect сервере.
Т.к. ocserv популярен в корпоративном мире, он поддерживает множество способов авторизации пользователей. От банального логина/пароля, до интеграции с централизованными инструментами, такими, как LDAP/Active Directory, PAM, Radius и пр. Из коробки даже имеется поддержка двухфакторной аутентификации на основе токенов – этот функционал + возможность отправки токенов TOTP по почте и в Telegram добавлен в контейнер, описание приведу позже в этой статье.
В качестве надежного и в тоже время удобного способа авторизации я выбрал подключение на основе пользовательского сертификата, который упаковывается в шифрованный (при указании пароля) файл с расширением .p12. В результате со стороны клиента необходимо будет лишь указать путь до файла сертификата и пароль для его расшифровки. Обычно клиенты способны запоминать пароли и вводить их более не требуется.
Стоит отметить, что для создания пользователя ему все таки необходимо задать пароль, хоть и такой способ авторизации отключен в конфиг файле сервера. Если вы проявили любопытство и заглянули в содержимое скрипта создания пользователей ocuser, то могли заметить команду генерации псевдослучайной строки: tr -cd “[:alnum:]” < /dev/urandom | head -c 60
Вывод которой задается в качестве пароля для пользователя при его создании. Небольшая подстраховка)
Создание пользователей для Linux/Windows/Android
И так, создаём пользователя такой командой:
docker exec -it openconnect ocuser ivan 'Ivan Cherniy'
Где ivan – это имя и идентификатор пользователя в ocserv (старайтесь указывать идентификатор без пробелов, дабы избежать ошибок), а ‘Ivan Cherniy’ это полное имя пользователя, которое будет отображено в метаданных пользовательского сертификата.
После ввода команды необходимо будет в интерактивном режиме указать имя файла сертификата и пароль для его шифрования.
Если команда создания отработала успешно, для указанного пользователя будет создан файл сертификата .p12 в директории secrets:
ls -l ./data/secrets
Список пользователей можно посмотреть в файле ./data/ocpasswd.
Создание пользователей для HarmonyOS/iOS
При создании пользователей для мобильных ОС от Huawei и Apple команде ocuser необходимо передать ключ -A. В таком случае будет использован другой алгоритм шифрования сертификата.
Пример команды:
docker exec -it openconnect ocuser -A steve 'Steve Jobs'
Проверяем созданный сертификат:
ls -l ./data/secrets
Если для устройств под управлением HarmonyOS и iOS сгенерировать сертификаты без ключа -A, то при настройке клиентского подключения, вы будете получать ошибку на этапе импорта сертификата.
Note that the Ciso AnyConnect app on iOS doesn’t support AES-256 cipher. It will refuse to import the client certificate. If the user is using iOS device, then you can choose the 3des-pkcs12cipher.
linuxbabe.com
Скачивание файла p12
Скачиваем созданный файл сертификата пользователя удобным для вас способом. Например:
cp ./data/secrets/ivan.p12 /tmp
А на клиентской машине:
scp vpn.r4ven.me:/tmp/ivan.p12 .
ls -l ivan.p12
На этом этапе уже можно переходить к настройке клиентов. Но если вам нужны персональные настройки каждого пользователя, такие как IP адрес клиента, маршруты, DNS, а также настройка автозапуска сервиса OpenConnect и автопродление SSL сертификатов, то идём последовательно.
Опционально: индивидуальные настройки ocserv для каждого пользователя
Создаём системную директорию config-per-user и отдельный файл под каждого пользователя, с таким же именем, какое указывали при создании (если забыли, посмотрите в файле ./data/ocpasswd):
mkdir ./data/config-per-user
vim ./data/config-per-user/ivan
Название параметров в файле конфига пользователя в большинстве своем идентичны основному конфигу сервера:
В примере выше я задаю для пользователя ivan конкретный IP (из подсети, что указана в основном конфиге), отдельные маршруты и адрес DNS сервера.
Upd. Если вы предпочитаете повысить уровень конфиденциальности, то при тунелировании трафика желательно иметь собственный DNS сервер. Вот инструкция по его настройке и интеграции с OpenConnect:
При такой конфигурации данный пользователь будет обращаться к указанным ресурсам через VPN сервер, а остальной трафик будет идти через его шлюз по умолчанию (или нет, в зависимости от настроек сети).
После внесения изменений в конфиг необходимо выполнить перечитывание файла конфигурации процессом ocserv внутри контейнера. Делается такой командой:
docker exec -it openconnect occtl reload
Автозапуск сервера OpenConnect с помощью systemd
Теперь настроим автоматический запуск/перезапуск нашего VPN сервера при помощи системы инициализации systemd.
Из директории с файлами проекта копируем файл openconnect.service в системную директорию systemd, перезагружаем конфигурацию systemd и останавливаем наши запущенные контейнеры:
cp ./openconnect.service /etc/systemd/system/
systemctl daemon-reload
docker compose down
Теперь активируем автозапуск и пробуем выполнить старт контейнера с OpenConnect сервером как сервис systemd:
systemctl enable --now openconnect
systemctl status openconnect
docker compose ps
Все отлично.
Настройка автоматического обновления SSL сертификатов (если есть домен)
Особенностью сертификатов от центра сертификации Let’s Encrypt является то, что они выдаются на 3 месяца. В такой ситуации предусмотрительно будет настроить автоматическое продление. Для этого добавляем в системный планировщик заданий cron запуск скрипта ssl_update.sh раз в неделю, например, в воскресенье в 3 часа утра:
Синтаксис фигурных скобок в bash { command1; command2 } позволяет объединить несколько команд в одну. Тут мы с его помощью первой командой выводим список заданий cron текущего пользователя, второй командой выводим строку с новым заданием и потом с помощью механизма перенаправления передаем весь вывод команде crontab -. Если команде crontab - передать только одну задачу, она затрет все предыдущие. Так делать не стоит)
Хоть и в моём примере нет других заданий в планировщике, команда специально составлена с “защитой от дурака”)
Для проверки работоспособности скрипта продления сертификатов, можем выполнить его вручную:
UPD 16.05.2024
В команде обновления certbot заменил параметр --force-renewal на --keep-until-expiring, который выполняет проверку срока действия сертификата, и если он не близок к завершению, то обновление сертификатов не происходит.
/opt/openconnect/ssl_update.sh
Скрипт также почистит дисковое пространство от неиспользуемых образов docker:
Проверяем время обновления SSL файлов:
ls -l ./data/ssl/live/vpn.r4ven.me/
Так, вроде все хорошо)
Обратите внимание, что если слишком часто обновлять сертификаты, то certbot (letsencrypt) будет выпадать в ошибку по этой причине.
Настройка подключения клиентов
Настройка OpenConnect клиента для Linux
Способ №1: менеджер сетевых соединений – NetworkManager
Если вы пользователь популярных дестопных дистрибутивов, то вероятнее всего вашей сетевой подсистемой управлет NetworkManager.
Сразу отмечу, что данный способ предпочтителен для десктопных систем, т.к. при использовании NM для подключения к ocserv не происходит конфликтов системы DNS.
В случае deb based систем, выполняем установку специального пакета-плагина NetworkManager для подключения к ocserv.
По традиции, продемонстрирую на примере Linux Mint 21 Cinnamon (Ubuntu 22.04).
Заходим в “Сетевые соединения” через трей или любым удобным вам способом;
Затем нажимаем кнопку добавления нового соединения, в списке выбираем “Cisco AntConnect or openconnect (OpenConnect)” и нажимаем “Создать“;
В открвшемся окошке указываем
“Имя соединения” – произвольное
“Шлюз” – DNS имя нашего сервера или его IP адрес (если без домена) + порт подключения. Пример: vpn.r4ven.me:43443
В разделе “Аутентификации по сертификату” в параметре “Сертификат пользователя” выбираем наш .p12 файл, который мы сгнерировали на этапе создания пользователя. В старых версиях nmapplet’а есть баг, когда при выборе сертификата он не видит файлы с расширенияем .p12. В таком случае просто перетащите (drag-and-drop) файл с файлового менеджера в поле данного параметра, как показано на скриншоте ниже;
После нажимаем “Сохранить”.
Если все сделано корректно, пробуем подключиться к нашему серверу. Кликаем на апплет сетевых соединений, затем на переключатель “VPN подключения“. Должно появиться окошко, где нас любезно попросят ввести пароль от файла-сертификата (если вы его задавали), который мы устанавливали на этапе создания пользователей ocserv. Ставим галочку “Сохранить пароль” и затем “Подключиться“. При успешном соединении на рабочем столе появится соответствующее уведомление:
Также новое подключение для NetworkManager можно создать с помощью консольной утилиты nmcli. Вот пример:
Посмотреть сетевые параметры нашего VPN подключения можно с помощью команд:
ip -c address
nmcli
Узнать внешний IP в консоли можно такой командой:
curl ifconfig.me
В выводе команды будет одна строка с вашим внешним IP адресом.
Способ №2: утилита командной строки – openconnect
Другой способ клиентского подключения в Linux, где отсутствует NetworkManager – это одноименная консольная утилита openconnect. Такой способ подключения чаще всего используется для клиентов без GUI, т.е. на Linux серверах.
Пример подключения все также для deb based систем.
Подключится к серверу OpenConnect можно такой командой:
# если с доменом
sudo openconnect -c /home/ivan/ivan.p12 vpn.r4ven.me:43443 <<< $(echo "password"$'\n')
# если без домена (с доп. подтверждением самоподписанного сертификата)
sudo openconnect -c /home/ivan/ivan.p12 12.345.67.89:43443 <<< $(echo "password"$'\n'yes$'\n')
Где ключу -c передается путь до файла сертификата .p12 а с помощью механизма here string (<<<) и подстановки передается вывод команды echo "password"$'\n', которая выводит текстовую строку и выполняет перевод строки, в данном случае, это имитация ввода Enter, чтобы не вводить вручную.
Результат команды подключения должен быть примерно таким:
Если вы используете подключение с помощью утилиты openconnect на Linux сервере, и планируете туннелировать весь его трафик, то необходимо добавить специальные правила маршрутизации, без которых вы потеряете доступ до своего сервера, в т.ч. и по SSH:
ip rule add table 128 from <public-ip>
ip route add table 128 to <public_ip_subnet> dev <ineteface_name>
ip route add table 128 default via <gateway>
Где:
128 – имя новой таблицы маршрутизации;
<piblic-ip> – основной IP вашего сервера;
<public_ip_subnet> – подсеть основного IP сервера;
<interface_name> – имя физического интерфейса, к которому подключен основной IP;
<gateway> – шлюз сети, через который ходит основной IP.
Далее устанавливаем программу на рабочий стол Windows обычными кликами “Далее, далее..”. А вот как установить клиента на MacOS я вам не подскажу. Тут уже вы сами изучите вопрос, ссылки на все материалы будут внизу.
После установки и запуска программы OpenConnectVPN выполняем настройку, как показано на скриншотах ниже. Отмечу лишь один момент: обязательно активируйте параметр “Disable UDP“, т.к. в нашей конфигурации сервера его использование отключено:
Проверяем сетевые параметры:
Все работает.
Настройка OpenConnect клиента для Android/HarmonyOS
Для мобильных ОС необходимо установить приложение Cisco Secure Client-AnyConnect.
Пожалуйста, обратите внимание, что данное мобильное приложение не имеет открытый исходный код. При его использовании придется довериться разработчику.
Существует open source мобильный клиент, но его разработка прекратилась n-е количество лет назад и у меня оно работало некорректно.
После открытия приложения необходимо создать подключение, указать адрес шлюза + порт и импортировать сертификат пользователя. Пошаговая инструкция показана на скриншотах ниже:
Настройка OpenConnect клиента для iOS
Инструкция любезно предоставлена подписчиком в Вороньем чате
Скачиваешь файл на телефон. Открываешь файлы и зажимаешь этот значок (просто при тычке он будет писать техническое сообщение о том, что профиль загружен). Выбираешь пункт поделиться – Еще – Листаешь в самый низ, там будет Any Connect. Открывается приложение и запрашивает пароль. Вводишь и тыкаешь импорт. И дальше уже по твоей схеме в настройках туннеля выбираешь сертификат.
Ну вот, вроде бы всё.
Блокировка пользователей путём отзыва сертификата
При необходимости заблокировать какого-то пользователя, сделать это можно с помощью отзыва его SSL сертификата.
Если вы только что подняли ocserv по этой инструкции, просто воспользуйтесь командой ocrevoke внутри контейнера:
# отозвать сертификат пользователя ivan
docker exec -it openconnect ocrevoke ivan
# перечитать черный список пользователей
docker exec -it openconnect ocrevoke RELOAD
# удалить всех из черного списка
docker exec -it openconnect ocrevoke RESET
Где вместо ivan подставьте имя пользователя, доступ по сертификату которого нужно заблокировать.
При следующем подключении пользователь получит ошибку:
Если же ваш сервер был поднят по этой инструкции некоторое время назад, придется выполнить немного ручной работы.
Сперва в конец конфига ./data/ocserv.conf добавляем параметр:
При отсутствии необходимости данного функционала, просто закомментируйте строку crl = /etc/ocserv/certs/crl.pem в конфиге ocserv.
Авторизация по логину/паролю
В некоторых случаях может понадобиться включение возможности авторизации по логину и паролю. Например при настройке openconnect на роутерах Keenetic, который не поддерживает подключение на основе сертификата.
В файле конфигурации ocserv – /opt/openconnect/data/ocserv.conf разрешаем авторизацию по логину/паролю, путем добавления такого параметра:
Стоит отметить, что при такой конфигурации авторизация по сертификату останется.
Затем перезапускаем сервер:
docker compose restart openconnect
Далее создаём нового пользователя:
docker exec -it openconnect ocpasswd exampleuser
И задаем ему пароль.
После чего можно подключаться с помощью пары логин/пароль. Обязательно указывайте только длинные и сложные пароли.
Настройка двухфактороной аутентификации (2FA/TOTP) с помощью ocpasswd и PAM
Ocserv из коробки поддерживает двухфакторную аутентификацию двух типов: HOTP, TOTP. В моей конфигурации используется TOTP, как более распространенный вариант.
Настроить 2FA в ocserv можно для двух способов авторизации на сервере: для внутренних пользователей в файле ocpasswd и внешних пользователей хоста с помощью модуля PAM.
Обратите, внимание, что в ocserv одновременно может использоваться только один вариант парольной аутентификации. Комментарий из оф. документации:
Note that authentication methods utilizing passwords cannot be combined (e.g., the plain, pam or radius methods).
Стоит отметить, что PAM аутентификация предоставляет более гибкий подход к настройке аутентификации пользователей на сервере. Именно с помощью PAM внутри контейнера реализован функционал отправки секретов и токенов TOTP по Email и в Telegram (об этом ниже).
Для активации 2FA внутри контейнера необходимо отредактировать файлы .env и ocserv.conf.
Правка .env
Добавить/изменить параметр:
OTP_ENABLE="true"
Правка ocserv.conf
Выбрать один из способов аутентификации, для которой будет применяться второй фактор:
Обратите внимание, что при каждом добавлении нового пользователя в хостовой системе для синхронизации примонтированных файлов необходимо перезапускать контейнер.
Пример создания локального пользователя Linux без домашней директории и без возможности входа в сеанс оболочки ОС:
Двухфакторная аутентификация активирована, осталось настроить ее для существующих пользователей. Для этого воспользуйтесь специальной командой/скриптом ocuser2fa. Пример для пользователя ivan:
docker exec -it openconnect ocuser2fa ivan
Команда выведет в консоль секрет, сгенерированный для указанного пользователя, а также этот секрет в формате base32 и в виде QR кода для сканирования-сохранения его в специальном приложении аутентификаторе на смартфоне, например: FreeOTP, Google authenticator, Yandex ключ и др.
Также для удобства QR код сохраняется в виде png файла в директории /opt/openconnect/data/secrets/otp_<user_id>.png.
Чаще всего при сканировании QR проблем с генерацией корректных TOTP не возникает, но с добавлением секрета вручную есть нюансы. Вот правильные параметры TOTP, поддерживаемые ocserv при ручном добавлении секрета на примере приложения FreeOTP:
Username — произвольное значение
Company — произвольное значение
Secret — секрет в кодировке base32
Type — totp
Digits — 6
Algorithm — SHA1
Interval — 30
Получать одноразовые пароли можно также в консоли Linux с помощью утилиты oathtool:
Замените <totp_base32_secret> на сгенерированный командой ocuser2fa секрет.
Каждый одноразовый токен доступен для применения в течении 20 “окон”, т.е. 20 * 30 сек = 10 мин.
(Опиционально) Отправка TOTP секрета + QR код и одноразовых паролей по Email и в Telegram
В контейнере реализован функционал отправки секрета TOTP в виде строки base32 и QR кода в виде png файла на Email адрес/Telegram чат пользователя во время работы команды ocuser2fa.
А также, при наличии соответствующей переменной, активируется возможность отправки по указанным каналам одноразовых паролей при каждом подключении к VPN серверу – работает только при использовании PAM авторизации, т.к. сам ocserv не умеет запускать кастомные скрипты конкретно в процессе аутентификации пользователя.
Отправка по Email
Отправка по почте осуществляется через внешний почтовый сервис с помощью консольного SMTP клиента – msmtp. Для его работы необходимо задать соответствующие параметры через переменные среды в файле .env. Вот пример:
MSMTP_HOST="smtp.example.com"
MSMTP_PORT="465"
MSMTP_USER="email@example.com"
MSMTP_PASSWORD="supersecretpassword"
MSMTP_FROM="email@example.com"
# для отправки одноразовых паролей при каждой авторизации
OTP_SEND_BY_EMAIL="true"
При необходимости скорректируйте параметры msmtp в файле /opt/openconnect/data/scrits/msmtprc после пересоздания контейнера.
В качестве пароля рекомендуется использовать “пароль приложения”, а не полноценный пароль от почтового аккаунта. Реквизиты SMTP серверов всех популярных провайдеров легко гуглятся.
Данные отправляются по Email, только если в качестве имени пользователя ocserv используется email адрес в стандартном формате: username@example.com.
Отправка в Telegram
Отправка сообщений в телегу осуществляется путем отправки HTTP запросов с помощью curl на api.telegram.org. Для работы этой функции требуется:
1) токен телеграм бота (инструкция по получению из оф. доки или более понятно тут), который необходимо задать в файле .env, пример:
TG_TOKEN="1234567890:QWERTYqwerty-QWERTY123_QwErTy123qWeRtY123"
# для отправки одноразовых паролей при каждой авторизации
OTP_SEND_BY_TELEGRAM="true"
2) пользователю, которому будут отправляться секрет с QR кодом и одноразовые токены необходимо написать вашему боту в чат любое сообщение в течение последних 24 часов (требуется единожды)
3) имя пользователя ocserv должно четко совпадать с nickname пользователя в Telegram
Перезапуск сервиса
Для применения изменений перезапускаем сервис:
systemctl restart openconnect
Проверка
Настраиваем 2FA для существующего пользователя с именем ввиде email адреса:
Для пользователя с именем, совпадающим с именем телеграм:
docker exec -it openconnect ocuser2fa ivan
В чате с ботом будет подобное:
Защита от автоматизированного доступа – Camouflage
С версии ocserv >= 1.23 доступен дополнительный слой защиты от сканирования интернет ботами, которые могут в автоматическом режиме подобирать резкизиты для доступа к внутренней сети. Для активации защиты внесите следующие параметры в ваш ocserv.conf:
# включение защиты от active probing
camouflage = true
# дополнительная часть url для подключения
camouflage_secret = "secretword"
# активация ложного окна авторизации, которое отклоняет любые попытки входа
#camouflage_realm = "My admin panel"
И перезапустите сервер:
systemctl restart openconnect
Теперь для подключения к серверу на клиенте необходимо указывать адрес с дополнительным сабурлом, указанного в конфиге. Пример:
https://vpn.r4ven.me:43443/?secretword
А все запросы к базовому урлу: https://vpn.r4ven.me:43443/ будут завершаться ошибкой клиента – 404. А если активен параметр camouflage_realm = "My admin panel", то запросы будут упираться в ложное окно авторизации.
Полезные команды systemctl/docker/docker-compose/ocserv
Небольшой список команд, который может вам пригодится при настройке и обслуживании OpenConnect сервера.
# управление сервисом openconnect с помощью systemd
systemctl stop openconnect
systemctl start openconnect
systemctl restart openconnect
# показывает список всех доступных образов docker
docker image ls
# показывает список всех контейнеров docker, включая работающие и остановленные
docker container ls -a
# удаляет все ненужные ресурсы docker, такие как неиспользуемые контейнеры, образы, сети и тома, без запроса подтверждения
docker system prune -af
# показывает статус и информацию о контейнерах, запущенных с помощью docker-compose
docker compose ps
# запускает контейнеры из файла docker-compose.yml в фоновом режиме
docker compose up -d
# останавливает и удаляет все контейнеры, сети, запущенные с помощью docker-compose
docker compose down
# отображает стандартный вывод контейнеров, запущенных с помощью docker-compose
docker compose logs -f
# перезапуск контейнера с именем "openconnect"
docker compose restart openconnect
# запускает интерактивную оболочку bash внутри контейнера "openconnect"
docker exec -it openconnect bash
# выводит справку по команде утилиты управления VPN сервером
docker exec -it openconnect occtl --help
# перечитывает файл конфигурации ocserv
docker exec -it openconnect occtl reload
# показывает статус ocserv
docker exec -it openconnect occtl show status
# показывает список пользователей ocserv
docker exec -it openconnect occtl show users
# показывает список активных сеансов ocserv
docker exec -it openconnect occtl show sessions all
# создает пользователя с id "ivan" и именем "Ivan Cherniy" для ocserv
docker exec -it openconnect ocuser ivan 'Ivan Cherniy'
Заключение
Фух! Создание данного материала заняло приличное количество времени и сил, но они было потрачены не зря. В процессе подготовки я узнал много нового и про работу сети в Linux, и множество нюансов bash при написании скриптов проекта и многое другое.
В будущем буду писать статьи по развертыванию различных персональных сервисов, и для доступа к ним будет использоваться VPN подключения на базе OpenConnect.
Если у вас возникли трудности с настройкой или остались вопросы, то смело оставляйте комментарии к данной статье или в нашем чате телеги: @r4ven_me_chat.
Также не забудьте подписаться на наш телеграм канал:@r4ven_me. Ссылки на все новые статьи появляются там в момент публикации.
Спасибо, что вместе со мной осилили данную статью. Успехов вам!
Когда вы первый раз заходите с помощью соцсетей, мы получаем публичную информацию из вашей учетной записи, предоставляемой провайдером услуги соцсети в рамках ваших настроек конфиденциальности. Мы также автоматически получаем ваш e-mail адрес для создания вашей учетной записи на нашем веб сайте. Когда она будет создана, вы будете авторизованы под этой учетной записью.
Не согласенСогласен
Я разрешаю создать мне учетную запись
Когда вы первый раз заходите с помощью соцсетей, мы получаем публичную информацию из вашей учетной записи, предоставляемой провайдером услуги соцсети в рамках ваших настроек конфиденциальности. Мы также автоматически получаем ваш e-mail адрес для создания вашей учетной записи на нашем веб сайте. Когда она будет создана, вы будете авторизованы под этой учетной записью.
Добрый день.
В статье устанавливается стабильная версия ovserv – 1.1, из репозитория Debian. Она не поддерживает указанный вами параметр.
Но по просьбе подписчиков, я вручную собрал отдельный образ с последней версий ocserv – 1.3.
Вы можете использовать ее, отредактировав параметр:
В статье есть упоминание про подключение к серверу с Keenetic… Так вот в последней текущей бете (а может и во всех), Keenetic не умеет сохранять адрес с портом… он его просто удаляет.. Ну и не коннектится, соответственно. Имеет смысл добавить комментарий, что если планируется подсоединяться через эти роутеры, то порт в .env нужно ставить дефолтный 443 (который не нужно указывать в строке подключения)
Приветствую!
Пожалуйста, уточните, в чем именно проблема? ufw / iptables на хостовой системе или в контейнере?
Т.к. по моей инструкции контейнер с ocserv разворачивается с помощью docker, то в таком случае, при пробрасывании порта из контейнера в хостовую систему, docker самостоятельно добавляет нужные правила в iptables / ufw, в свою отдельную цепочку правил.
Вы можете в этом убедиться выполнив:
sudo iptables -L -n
Или
sudo iptables-save
Последний раз редактировалось 5 месяцев назад Иван Чёрный ем
Сообщения из вывода указывают на проблемы с сертификатами локального центр сертификации (CA), которые генерируются скриптом в контейнере при первом запуске.
В директории
/opt/openconnect/data/certs
Должны быть файлы
ca-key.pem
ca-cert.pem
Проверьте существуют ли эти файлы.
Если да, проверить файл сертификата можно такой командой:
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 1 (0x1)
Signature Algorithm: sha256WithRSAEncryption
Issuer: O = vpn.r4ven.me, CN = My VPN
Validity
Not Before: May 23 13:19:12 2024 GMT
Not After : Dec 31 23:59:59 9999 GMT
Subject: O = vpn.r4ven.me, CN = My VPN
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (3072 bit)
Modulus:
00:f2:fe:18:f6:a2:29:34:5e:89:de:fb:b4:9c:2e:
Также стоит проверить сертификаты от cerbot, если вы используете подключение по доменному имени.
Рекомендую написать нам в чат телеги: https://t.me/r4ven_me_chat
А то тут можно долго перебирать варианты.
Добрый день.
Сообщите пожалуйста подробности.
В статье демонстрируется установка ocserv с использованием 43443 порта, а не 443.
Предположу, что вы хотите поднять ocserv на 443 порту, но при этом он уже используется другими сервисами. В таком случае выходом будет настройка обратного прокси сервера, типа nginx или haproxy.
Тема обратных прокси выходит за рамки данной статьи. Но наши подсписчики в чате телеграм уже обсуждали решение данного вопроса. При желании можете поискать ответ в истории сообщений или задать вопрос😌.
Денис Тутельян
05.01.2025 15:31
Здравствуйте. Подскажите, с чем связана эта ошибка? Происходит на стадии сборки образа.
ERROR: The Compose file ‘./docker-compose.yml’ is invalid because:
services.openconnect.ports is invalid: Invalid port “”43443″:443/tcp”, should be [[remote_ip:]remote_port[-remote_port]:]port[/protocol]
networks.vpn.ipam.config value Additional properties are not allowed (‘gateway’ was unexpected)
services.openconnect.depends_on contains an invalid type, it should be an array
Добрый день.
Судя по всему ошибка в синтаксисе файла docker-compose.yml. Конкретно, “”43443″:443/tcp” кажется тут лишние кавычки.
Попробуйте просто указать – 43443:443/tcp
Также предположу, что лишние кавычки могут подтягиваться из значения переменной, ${SRV_PORT} если вы ее в docker-compose.yml обрамили дополнительными кавычками.
Синтаксис проверял, прочел, что это связано с новым docker-compose, в нем не поддерживается поле gateway. Образ собрался с командой docker compose, а не docker-compose
А какая у вас версия docker engine? Проверил у себя, на нескольких машинах, везде сетевой параметр gateway указан и контейнеры запускаются корректно.
По поводу docker-compose – раньше это был не официальный плагин от сторонних разработчиков. В последствии docker взяли его под свое крыло и с определенной версии docker compose стал встроенной командой.
Engine 27.4.1, а вот compose 1.2.5, так что ошибся, не новый compose, а наоборот слишком старый в debian 11
Kirill NS
16.01.2025 00:12
Доброго.
Пока не ставил, интересуюсь заранее. Такое подключение позволят получать доступ к ресурсам сети где этот linux сервер находится? В статье не нашёл явного описания.
Аутентификация через LDAP (AD) не рассматривалась? Подскажите пожалуйста, где можно посмотреть настройку? Что бы не сильно отличалась от вашей конфигурации ocserv. Иначе, придётся всё переделывать заново.
Юнит, что вы привели в своем сообщении не подошел?
Эй, обратите внимание, что "Вороний блог" использует куки. Если Вы продолжите использовать данный сайт, это будет считаться что Вас это устраивает ;)Ok
camouflage поддерживает эта версия ?
Добрый день.
В статье устанавливается стабильная версия ovserv – 1.1, из репозитория Debian. Она не поддерживает указанный вами параметр.
Но по просьбе подписчиков, я вручную собрал отдельный образ с последней версий ocserv – 1.3.
Вы можете использовать ее, отредактировав параметр:
Или же собрать его самостоятельно по отдельной статье:
Собираем OpenConnect (ocserv) версии 1.3 из исходников в Debian 12 + docker образ
В статье есть упоминание про подключение к серверу с Keenetic… Так вот в последней текущей бете (а может и во всех), Keenetic не умеет сохранять адрес с портом… он его просто удаляет.. Ну и не коннектится, соответственно.
Имеет смысл добавить комментарий, что если планируется подсоединяться через эти роутеры, то порт в .env нужно ставить дефолтный 443 (который не нужно указывать в строке подключения)
Привет!
Полезная информация. Добавил в статью. Благодарю👍
Добрый день.
А есть какие-то требования по настройке ufw и iptables?
Я второй день мучаюсь этими настройками.
Приветствую!
Пожалуйста, уточните, в чем именно проблема? ufw / iptables на хостовой системе или в контейнере?
Т.к. по моей инструкции контейнер с ocserv разворачивается с помощью docker, то в таком случае, при пробрасывании порта из контейнера в хостовую систему, docker самостоятельно добавляет нужные правила в iptables / ufw, в свою отдельную цепочку правил.
Вы можете в этом убедиться выполнив:
Или
Для более оперативной связи, рекомендую позадавать вопросы в нашем чате: @r4ven_me_chat. У нас там дружелюбное сообщество 🙂
У меня скорость на Download 0.70 а на Upload 27 мбит, как исправить скорость?
Зависит от большого числа факторов. Чаще всего дело не в ocserv, а в канале связи до вашего сервера.
Как можно проверить скорость Download|Upload в реальном времени с помощью ssh, чтобы определить проблема в VPN или нет.
Установите утилиту контроля пайплайна:
Затем отключите VPN и проверьте скорость download:
Затем upload:
Затем включите VPN и проверьте снова. Если без VPN скорость намного выше, значит дело в канале.
При коннекте получаю такую ошибку,хотя ссл рукопожатие таки проходит.
траффик проксирую в контейнер через нжинкс,но пробовал и хапрокси.результат аналогичный.Не могу понять в чем дело
Покажите, как настроили nginx для проксирования.
Если подключаться напрямую, без nginx, все работает корректно?
конфиг хапрокси
слушаю 8443 в контейнере хапрокси,отправляю на 9443 ocerv
напрямую без прокси
| 198c | Failed to open HTTPS connection to example.com
| 198c | Authentication error; cannot obtain cookie
| 2d98 | Disconnected
с хапрокси вот такое. Смущает signer not found,но похоже это пол беды
Сообщения из вывода указывают на проблемы с сертификатами локального центр сертификации (CA), которые генерируются скриптом в контейнере при первом запуске.
В директории
Должны быть файлы
Проверьте существуют ли эти файлы.
Если да, проверить файл сертификата можно такой командой:
Вывод должен начинаться примерно так:
Также стоит проверить сертификаты от cerbot, если вы используете подключение по доменному имени.
Рекомендую написать нам в чат телеги: https://t.me/r4ven_me_chat
А то тут можно долго перебирать варианты.
Решение пользователя из чата в телеграм:
Добрый день.
А если порты 80 и 443 уже заняты, то как быть?
Добрый день.
Сообщите пожалуйста подробности.
В статье демонстрируется установка ocserv с использованием 43443 порта, а не 443.
Предположу, что вы хотите поднять ocserv на 443 порту, но при этом он уже используется другими сервисами. В таком случае выходом будет настройка обратного прокси сервера, типа nginx или haproxy.
Тема обратных прокси выходит за рамки данной статьи. Но наши подсписчики в чате телеграм уже обсуждали решение данного вопроса. При желании можете поискать ответ в истории сообщений или задать вопрос😌.
Здравствуйте. Подскажите, с чем связана эта ошибка? Происходит на стадии сборки образа.
ERROR: The Compose file ‘./docker-compose.yml’ is invalid because:
services.openconnect.ports is invalid: Invalid port “”43443″:443/tcp”, should be [[remote_ip:]remote_port[-remote_port]:]port[/protocol]
networks.vpn.ipam.config value Additional properties are not allowed (‘gateway’ was unexpected)
services.openconnect.depends_on contains an invalid type, it should be an array
Добрый день.
Судя по всему ошибка в синтаксисе файла docker-compose.yml. Конкретно, “”43443″:443/tcp” кажется тут лишние кавычки.
Попробуйте просто указать – 43443:443/tcp
Также предположу, что лишние кавычки могут подтягиваться из значения переменной, ${SRV_PORT} если вы ее в docker-compose.yml обрамили дополнительными кавычками.
Синтаксис проверял, прочел, что это связано с новым docker-compose, в нем не поддерживается поле gateway. Образ собрался с командой docker compose, а не docker-compose
А какая у вас версия docker engine? Проверил у себя, на нескольких машинах, везде сетевой параметр gateway указан и контейнеры запускаются корректно.
По поводу docker-compose – раньше это был не официальный плагин от сторонних разработчиков. В последствии docker взяли его под свое крыло и с определенной версии docker compose стал встроенной командой.
Engine 27.4.1, а вот compose 1.2.5, так что ошибся, не новый compose, а наоборот слишком старый в debian 11
Доброго.
Пока не ставил, интересуюсь заранее. Такое подключение позволят получать доступ к ресурсам сети где этот linux сервер находится? В статье не нашёл явного описания.
Привет!
Сеть хоста по умолчанию доступна через бридж docker, который создаётся при запуске контейнера.
А вот для связи с клиентами из хоста нужно добавлять маршрут:
Условно:
Где:
10.10.10.2 – адрес контейнера ocserv
10.11.11.0/24 – внутренняя сеть ocserv
Если нужно больше контроля, то для каждого клиента при его подключении выполняется bash скрипт /opt/openconnect/data/connect.sh
Ограничить доступ, например к сети 10.10.200.0/24 можно с помощью iptables так:
Понятно, спасибо.
Аутентификация через LDAP (AD) не рассматривалась? Подскажите пожалуйста, где можно посмотреть настройку? Что бы не сильно отличалась от вашей конфигурации ocserv. Иначе, придётся всё переделывать заново.
Т.к. в LDAP у меня нет особой необходимости, поэтому пока не рассматривал. Но вопрос интересный, возможно добавлю функционал в будущем.
Для добавления нового функционала и сохранении конфигурации из статьи — вам придется пересобирать docker контейнер.
Подробно, как настраивается PAM авторизация, в т.ч. внутри контейнера расписал в одном из постов в телеге тут.
Авторизацию по LDAP в Linux можно реализовать через sssd и модуль PAM – pam_sss.so.
Нужно подробнее изучить вопрос. Пока не готов дать конкретных рекомендаций.
Есть у кого рабочий, не могу запустить , в статье нет пояснений нашёл га хабре ))
[Unit]
Description=OpenConnect SSL VPN server
Documentation=man:ocserv(8)
After=network-online.target
[Service]
PrivateTmp=true
PIDFile=/run/ocserv.pid
Type=simple
ExecStart=/usr/sbin/ocserv –foreground –pid-file /run/ocserv.pid –config /etc/ocserv/ocserv.conf
ExecReload=/bin/kill -HUP $MAINPID
[Install]
WantedBy=multi-user.target
Все есть на сайте.
Пишем bash скрипт для подключения к OpenConnect VPN серверу
service unit occlient.service: вот что там есть , как я понял это клиент )) А нужен сервер.
Юнит, что вы привели в своем сообщении не подошел?