Поднимаем OpenConnect SSL VPN сервер (ocserv) в docker для внутренних проектов
Приветствую!

Сегодня будем разворачивать свой 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')
Нажмите, чтобы развернуть и увидеть больше

Введение

Пару слов о том, в чем смысОл всего происходящего.

В корпоративном мире существует продукт Cisco AnyConnect, который представляет собой проприетарное ПО для организации виртуальных частных сетей (VPN), разработанное компанией Cisco.

Как это часто бывает в 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 решение.

Основные преимущества такого решения:

Предварительная подготовка

Для удобства и простоты использования мы с вами будем разворачивать 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 внутрни контейнера был автоматизирован.

Вот краткое описание проекта:

Так, воды налил, теперь давайте приступать к установке и настройке нашего сервера.

Обновление системы

Первым делом рекомендуется обновить вашу систему до актуального состояния:

BASH
sudo -s

apt update && apt upgrade -y
Нажмите, чтобы развернуть и увидеть больше

Клонирование репозитория с исходными файлами проекта

Для клонирования git репозитория нам потребуется утилита командной строки, из состава пакета одноимённой системы контроля версий — git. Если эта система у вас не установлена, то выполняем:

BASH
apt install git
Нажмите, чтобы развернуть и увидеть больше

Теперь клонируем репозиторий с подготовленными файлами из моего GitHub, далее копируем директорию v1.3-sid по пути /opt/openconnect и переходим в неё:

BASH
git clone https://github.com/r4ven-me/openconnect /tmp/openconnect

cp -rv /tmp/openconnect/src/server/v1.3-sid /opt/openconnect && rm -rf /tmp/openconnect

cd /opt/openconnect
Нажмите, чтобы развернуть и увидеть больше

Директория /opt (optional) предназначена для установки «дополнительного» программного обеспечения, которое не является стандартным для данной ОС.

Файлы проекта имеет следующую структуру:

Где:

.env - файл с переменными:

BASH
 1TZ="Europe/Moscow"
 2SRV_PORT="43443"
 3SRV_CN="example.com"
 4SRV_CA="Example CA"
 5USER_EMAIL="mail@example.com"
 6#OTP_ENABLE="true"
 7#OTP_SEND_BY_EMAIL="true"
 8#MSMTP_HOST="smtp.example.com"
 9#MSMTP_PORT="465"
10#MSMTP_USER="mail@example.com"
11#MSMTP_PASSWORD="SuPeRsEcReTpAsSwOrD"
12#MSMTP_FROM="mail@example.com"
13#OTP_SEND_BY_TELEGRAM="true"
14#TG_TOKEN="1234567890:QWERTYuio-PA1DFGHJ2_KlzxcVBNmqWEr3t"
Нажмите, чтобы развернуть и увидеть больше

docker-compose.yml - файл описания сервисов (контейнеров) openconnect и certbot (Let’s Encrypt):

YAML
 1---
 2
 3networks:
 4
 5  vpn:
 6    ipam:
 7      driver: default
 8      config:
 9        - subnet: 172.22.22.0/24
10          gateway: 172.22.22.1
11
12services:
13
14  certbot:
15    image: certbot/certbot
16    container_name: certbot
17    hostname: certbot
18    env_file:
19      - .env
20    volumes:
21      - ./data/ssl:/etc/letsencrypt
22    ports:
23      - 80:80
24    command: certonly --non-interactive --keep-until-expiring --standalone --preferred-challenges http --agree-tos --email ${USER_EMAIL} -d ${SRV_CN}
25
26  openconnect:
27    depends_on:
28      certbot:
29        condition: service_completed_successfully
30    build: .
31    image: openconnect
32    #image: r4venme/openconnect:v1.3-sid
33    container_name: openconnect
34    restart: unless-stopped
35    deploy:
36      resources:
37        limits:
38          cpus: '0.50'
39          memory: 200M
40    cap_add:
41      - NET_ADMIN
42    hostname: openconnect
43    env_file:
44      - .env
45    volumes:
46      - ./data:/etc/ocserv
47      #- /etc/passwd:/etc/passwd:ro
48      #- /etc/group:/etc/group:ro
49      #- /etc/shadow:/etc/shadow:ro
50    devices:
51      - /dev/net/tun:/dev/net/tun
52    ports:
53      - ${SRV_PORT}:443/tcp
54    networks:
55      vpn:
56        ipv4_address: 172.22.22.22
Нажмите, чтобы развернуть и увидеть больше

Dockerfile - файл описания docker образа сервера openconnect для сборки:

BASH
 1FROM debian:sid
 2
 3LABEL maintainer="Ivan Cherniy <kar-kar@r4ven.me>"
 4
 5ENV OCSERV_DIR="/etc/ocserv"
 6ENV CERTS_DIR="${OCSERV_DIR}/certs"
 7ENV SSL_DIR="${OCSERV_DIR}/ssl"
 8ENV SECRETS_DIR="${OCSERV_DIR}/secrets"
 9ENV SCRIPTS_DIR="${OCSERV_DIR}/scripts"
10ENV PATH="${SCRIPTS_DIR}:${PATH}"
11ENV SRV_CN="example.com" 
12ENV SRV_CA="Example CA"
13ENV OTP_ENABLE="false"
14ENV OTP_SEND_BY_EMAIL="false"
15ENV OTP_SEND_BY_TELEGRAM="false"
16ENV MSMTP_HOST="smtp.example.com"
17ENV MSMTP_PORT="465"
18ENV MSMTP_USER="mail@example.com"
19ENV MSMTP_PASSWORD="PaSsw0rD"
20ENV MSMTP_FROM="mail@example.com"
21ENV TG_TOKEN="1234567890:QWERTYuio-PA1DFGHJ2_KlzxcVBNmqWEr3t"
22
23WORKDIR $OCSERV_DIR
24
25ARG DEBIAN_FRONTEND=noninteractive
26
27RUN apt update && \
28    apt install --yes --no-install-recommends \
29        tini \
30        ocserv \
31        gnutls-bin \
32        iptables \
33        iproute2 \
34        iputils-ping \
35        less \
36        ca-certificates \
37        xxd \
38        libpam-oath \
39        oathtool \
40        qrencode \
41        curl \
42        jq \
43        msmtp && \
44    apt autoremove --yes && \
45    apt clean --yes && \
46    rm -rf /var/lib/{apt,dpkg,cache,log}/*
47
48COPY ./ocserv.sh /
49
50ENTRYPOINT ["/ocserv.sh"]
51
52CMD ["/usr/bin/tini", "--", "/usr/sbin/ocserv", "--config", "/etc/ocserv/ocserv.conf", "--foreground"]
53
54HEALTHCHECK --interval=5m --timeout=3s \
55    CMD curl -k https://localhost:443/ || exit 1
Нажмите, чтобы развернуть и увидеть больше

ocserv.sh - bash скрипт, запускаемый при старте контейнера, который выполняет начальную конфигурацию и инициализацию процесса ocserv:

BASH
  1#!/bin/bash
  2
  3# Some protection
  4set -Eeuo pipefail
  5
  6# Define default server vars if they are not set
  7SRV_CN="${SRV_CN:=example.com}" 
  8SRV_CA="${SRV_CA:=Example CA}"
  9OTP_ENABLE="${OTP_ENABLE:=false}"
 10OTP_SEND_BY_EMAIL="${OTP_SEND_BY_EMAIL:=false}"
 11OTP_SEND_BY_TELEGRAM="${OTP_SEND_BY_TELEGRAM:=false}"
 12MSMTP_HOST="${MSMTP_HOST:=smtp.example.com}"
 13MSMTP_PORT="${MSMTP_PORT:=465}"
 14MSMTP_USER="${MSMTP_USER:=mail@example.com}"
 15MSMTP_PASSWORD="${MSMTP_PASSWORD:=PaSsw0rD}"
 16MSMTP_FROM="${MSMTP_FROM:=mail@example.com}"
 17TG_TOKEN="${TG_TOKEN:=1234567890:QWERTYuio-PA1DFGHJ2_KlzxcVBNmqWEr3t}"
 18
 19# Ocserv vars (do not modify)
 20OCSERV_DIR="/etc/ocserv"
 21CERTS_DIR="${OCSERV_DIR}/certs"
 22SSL_DIR="${OCSERV_DIR}/ssl"
 23SECRETS_DIR="${OCSERV_DIR}/secrets"
 24SCRIPTS_DIR="${OCSERV_DIR}/scripts"
 25
 26# Create certs dirs
 27for sub_dir in "${OCSERV_DIR}"/{"ssl/live/${SRV_CN}","certs","secrets","scripts"}; do
 28    if [[ ! -d "$sub_dir" ]]; then
 29        mkdir -p "$sub_dir"
 30    fi
 31done
 32
 33if [[ -r /usr/share/doc/ocserv/sample.config && ! -e "${OCSERV_DIR}"/sample.config ]]; then
 34    cp /usr/share/doc/ocserv/sample.config "${OCSERV_DIR}"/
 35fi
 36
 37# Create ocserv config file
 38if [[ ! -e "${OCSERV_DIR}"/ocserv.conf ]]; then
 39cat << _EOF_ > "${OCSERV_DIR}"/ocserv.conf
 40auth = "certificate"
 41#auth = "plain[passwd=${OCSERV_DIR}/ocpasswd]"
 42#auth = "plain[passwd=/etc/ocserv/ocpasswd,otp=/etc/ocserv/secrets/users.oath]"
 43#enable-auth = "certificate"
 44#enable-auth = "pam"
 45tcp-port = 443
 46socket-file = /run/ocserv-socket
 47server-cert = ${SSL_DIR}/live/${SRV_CN}/fullchain.pem
 48server-key = ${SSL_DIR}/live/${SRV_CN}/privkey.pem
 49ca-cert = ${CERTS_DIR}/ca-cert.pem
 50isolate-workers = true
 51max-clients = 20
 52max-same-clients = 2
 53rate-limit-ms = 200
 54server-stats-reset-time = 604800
 55keepalive = 10
 56dpd = 120
 57mobile-dpd = 1800
 58switch-to-tcp-timeout = 25
 59try-mtu-discovery = false
 60cert-user-oid = 0.9.2342.19200300.100.1.1
 61tls-priorities = "NORMAL:%SERVER_PRECEDENCE:%COMPAT:-VERS-SSL3.0:-VERS-TLS1.0:-VERS-TLS1.1:-VERS-TLS1.3"
 62auth-timeout = 1000
 63min-reauth-time = 300
 64max-ban-score = 100
 65ban-reset-time = 1200
 66cookie-timeout = 600
 67deny-roaming = false
 68rekey-time = 172800
 69rekey-method = ssl
 70connect-script = ${SCRIPTS_DIR}/connect
 71disconnect-script = ${SCRIPTS_DIR}/disconnect
 72use-occtl = true
 73pid-file = /run/ocserv.pid
 74log-level = 1
 75device = vpns
 76predictable-ips = true
 77default-domain = $SRV_CN
 78ipv4-network = 10.10.10.0
 79ipv4-netmask = 255.255.255.0
 80tunnel-all-dns = true
 81dns = 8.8.8.8
 82ping-leases = false
 83config-per-user = ${OCSERV_DIR}/config-per-user/
 84cisco-client-compat = true
 85dtls-legacy = true
 86client-bypass-protocol = false
 87crl = /etc/ocserv/certs/crl.pem
 88#camouflage = true
 89#camouflage_secret = "secretword"
 90#camouflage_realm = "Welcome to admin panel"
 91_EOF_
 92fi
 93
 94# Create template for CA SSL cert
 95if [[ ! -e "${CERTS_DIR}"/ca.tmpl ]]; then
 96cat << _EOF_ > "${CERTS_DIR}"/ca.tmpl
 97organization = $SRV_CN
 98cn = $SRV_CA
 99serial = 001
100expiration_days = -1
101ca
102signing_key
103cert_signing_key
104crl_signing_key
105_EOF_
106fi
107
108# Create template for users SSL certs
109if [[ ! -e "${CERTS_DIR}"/users.cfg ]]; then
110cat << _EOF_ > "${CERTS_DIR}"/users.cfg
111organization = $SRV_CN
112cn = Example User
113uid = exampleuser
114expiration_days = -1
115tls_www_client
116signing_key
117encryption_key
118_EOF_
119fi
120
121# Create template for server self-signed SSL cert
122if [[ ! -e "${SSL_DIR}"/server.tmpl ]]; then
123cat << _EOF_ > "${SSL_DIR}"/server.tmpl
124cn = $SRV_CA
125dns_name = $SRV_CN
126organization = $SRV_CN
127expiration_days = -1
128signing_key
129encryption_key #only if the generated key is an RSA one
130tls_www_server
131_EOF_
132fi
133
134# Generate empty revoke file
135if [[ ! -e "${CERTS_DIR}"/crl.tmpl ]]; then
136cat << _EOF_ > "${CERTS_DIR}"/crl.tmpl
137crl_next_update = 365
138crl_number = 1
139_EOF_
140fi
141
142# Create connect script which runs for every user connection
143if [[ ! -e "${SCRIPTS_DIR}"/connect ]]; then
144cat << _EOF_ > "${SCRIPTS_DIR}"/connect && chmod +x "${SCRIPTS_DIR}"/connect
145#!/bin/bash
146
147set -Eeuo pipefail
148
149echo "\$(date) User \${USERNAME} Connected - Server: \${IP_REAL_LOCAL} VPN IP: \${IP_REMOTE}  Remote IP: \${IP_REAL} Device:\${DEVICE}"
150echo "Running iptables MASQUERADE for User \${USERNAME} connected with VPN IP \${IP_REMOTE}"
151iptables -t nat -A POSTROUTING -s "\${IP_REMOTE}"/32 -o eth0 -j MASQUERADE
152_EOF_
153fi
154
155# Create disconnect script which runs for every user disconnection
156if [[ ! -e "${SCRIPTS_DIR}"/disconnect ]]; then
157cat << _EOF_ > "${SCRIPTS_DIR}"/disconnect && chmod +x "${SCRIPTS_DIR}"/disconnect
158#!/bin/bash
159
160set -Eeuo pipefail
161
162echo "\$(date) User \${USERNAME} Disconnected - Bytes In: \${STATS_BYTES_IN} Bytes Out: \${STATS_BYTES_OUT} Duration:\${STATS_DURATION}"
163iptables -t nat -D POSTROUTING -s "\${IP_REMOTE}"/32 -o eth0 -j MASQUERADE
164_EOF_
165fi
166
167# Create script to create new users
168if [[ ! -e "${SCRIPTS_DIR}"/ocuser ]]; then
169cat << _EOF_ > "${SCRIPTS_DIR}"/ocuser && chmod +x "${SCRIPTS_DIR}"/ocuser
170#!/bin/bash
171
172set -Eeuo pipefail
173
174# Check and set script params
175if [[ \$# -eq 2 ]]; then
176    USER_UID="\$1"
177    USER_CN="\$2"
178elif [[ \$# -eq 3 ]]; then
179	if [[ "\$1" == "-A" ]]; then
180    		USER_UID="\$2"
181    		USER_CN="\$3"
182	else
183		echo "Use -A key as a first param to generate cert for IOS devices" >&2
184        exit 1
185	fi
186else
187    echo "Please run script with two params: username and 'Common Username'" >&2
188    echo "Example: ocuser john 'John Doe'" >&2
189    echo "For IOS or HarmonyOS devices add -A key as first param in command" >&2
190    echo "Example: ocuser -A steve 'Steve Jobs'" >&2
191    exit 1
192fi
193
194# Modify user cert template and generate user key, cert and protected .p12 file
195sed -i -e "s/^organization.*/organization = \$SRV_CN/" -e "s/^cn.*/cn = \$USER_CN/" -e "s/^uid.*/uid = \$USER_UID/g" "\${CERTS_DIR}"/users.cfg
196echo "\$(tr -cd "[:alnum:]" < /dev/urandom | head -c 60)" | ocpasswd -c "\${OCSERV_DIR}"/ocpasswd "\$USER_UID"
197certtool --generate-privkey --outfile "\${CERTS_DIR}"/"\${USER_UID}"-privkey.pem
198certtool --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
199if [[ "\$1" == "-A" ]]; then
200	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
201else
202	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
203fi
204_EOF_
205fi
206
207# Add revoke script
208if [[ ! -e "${SCRIPTS_DIR}"/ocrevoke ]]; then
209cat << _EOF_ > "${SCRIPTS_DIR}"/ocrevoke && chmod +x "${SCRIPTS_DIR}"/ocrevoke
210#!/bin/bash
211
212set -Eeuo pipefail
213
214if [[ ! -e "\${CERTS_DIR}"/crl.tmpl ]]; then
215cat << __EOF__ > "\${CERTS_DIR}"/crl.tmpl
216crl_next_update = 365
217crl_number = 1
218__EOF__
219fi
220
221if [[ \$# -eq 1 ]]; then
222    if [[ "\$1" == "HELP" ]]; then
223        echo "Usage:
224        CMD to revoke cert of some user: ocrevoke <exist_user> 
225        CMD to apply current revoked.pem: ocrevoke RELOAD
226        CMD to reset all revokes: ocrevoke RESET
227        CMD to print this help: ocrevoke HELP"
228    elif [[ "\$1" == "RESET" ]]; then
229        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
230        occtl reload
231    elif [[ "\$1" == "RELOAD" ]]; then
232        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
233    else
234        USER_UID="\$1"
235        cat "\${CERTS_DIR}"/"\${USER_UID}"-cert.pem >> "\${CERTS_DIR}"/revoked.pem
236        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
237        occtl reload
238    fi
239else
240    echo "Usage:
241    CMD to revoke cert of some user: ocrevoke <exist_user> 
242    CMD to apply current revoked.pem: ocrevoke RELOAD
243    CMD to reset all revokes: ocrevoke RESET
244    CMD to print this help: ocrevoke HELP"
245fi
246_EOF_
247fi
248
249# Add ocuser2fa script
250if [[ "$OTP_ENABLE" == "true" && ! -e "${SCRIPTS_DIR}"/ocuser2fa ]]; then
251cat << _EOF_ > "${SCRIPTS_DIR}"/ocuser2fa && chmod +x "${SCRIPTS_DIR}"/ocuser2fa
252#!/bin/bash
253
254set -Eeuo pipefail
255
256if [[ \$# -eq 1 ]]; then
257    USER_ID="\$1"
258    OTP_SECRET="\$(head -c 16 /dev/urandom | xxd -c 256 -ps)"
259    OTP_SECRET_BASE32="\$(echo 0x"\${OTP_SECRET}" | xxd -r -c 256 | base32)"
260    OTP_SECRET_QR="otpauth://totp/\$USER_ID?secret=\$OTP_SECRET_BASE32&issuer=$SRV_CA&algorithm=SHA1&digits=6&period=30"
261
262    if [[ ! -e "\${SECRETS_DIR}"/users.oath ]] || ! grep -qP "(?<!\\S)\${USER_ID}(?!\\S)" "\${SECRETS_DIR}"/users.oath; then
263        echo "HOTP/T30 \$USER_ID - \$OTP_SECRET" >> "\${SECRETS_DIR}"/users.oath
264        echo "OTP secret for \$USER_ID: \$OTP_SECRET"
265        echo "OTP secret in base32: \$OTP_SECRET_BASE32"
266        echo "OTP secret in QR code:"
267        qrencode -t ANSIUTF8 "\$OTP_SECRET_QR"
268        qrencode "\$OTP_SECRET_QR" -s 10 -o "\${SECRETS_DIR}"/otp_"\${USER_ID}".png
269        echo "TOTP secret in png image saved at: \${SECRETS_DIR}/otp_\${USER_ID}.png"
270
271        send_qr_by_email() {
272            EMAIL_REGEX="^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
273
274            if [[ \$USER_ID =~ \$EMAIL_REGEX ]]; then
275                cat << EOF | msmtp --file="\${SCRIPTS_DIR}"/msmtprc "\$USER_ID"
276Subject: TOTP QR code for OpenConnect auth
277MIME-Version: 1.0
278Content-Type: multipart/mixed; boundary="boundary"
279
280--boundary
281Content-Type: text/plain
282
283TOTP secret for OpenConnect (base32):
284\$OTP_SECRET_BASE32
285
286--boundary
287Content-Type: image/png; name="file.png"
288Content-Transfer-Encoding: base64
289Content-Disposition: attachment; filename="file.png"
290
291\$(base64 "\${SECRETS_DIR}"/otp_"\${USER_ID}".png)
292--boundary--
293EOF
294                echo "[\$(date '+%F %T')] - TOTP secret and QR code successfully sent to \$USER_ID via Email" | tee -a "\${OCSERV_DIR}"/pam.log
295            else
296                return 0
297            fi
298        }
299
300        if [[ "\$OTP_SEND_BY_EMAIL" == "true" ]]; then send_qr_by_email; fi
301
302        send_qr_by_telegram() {
303            TG_REGEX="^[a-zA-Z][a-zA-Z0-9_]{4,31}\$"
304
305            if [[ \$USER_ID =~ \$TG_REGEX ]]; then
306                TG_MESSAGE="TOTP secret for OpenConnect (base32):
307\$OTP_SECRET_BASE32"
308                TG_USER_FILE="\${SCRIPTS_DIR}/tg_users.txt"
309                
310                if grep -qP "(?<!\\S)\${USER_ID}(?!\\S)" "\$TG_USER_FILE" 2> /dev/null; then
311                    TG_CHAT_ID=\$(grep -P "(?<!\\S)\${USER_ID}(?!\\S)" "\$TG_USER_FILE" | awk '{print \$1}')
312                else
313                    TG_RESPONSE="\$(curl -s "https://api.telegram.org/bot\$TG_TOKEN/getUpdates")"
314                    TG_CHAT_ID=\$(echo "\$TG_RESPONSE" | jq -r --arg USERNAME "\$USER_ID" '.result[] | select(.message.from.username == \$USERNAME) | .message.chat.id')
315
316                    if [[ -z "\$TG_CHAT_ID" ]]; then
317                        echo "[\$(date '+%F %T')] - User was not found or did not interact with the bot" >> "\${OCSERV_DIR}"/pam.log
318                        return 0
319                    fi
320                    echo "\$TG_CHAT_ID \$USER_ID" >> "\$TG_USER_FILE"
321                fi
322
323                curl -s -X POST "https://api.telegram.org/bot\$TG_TOKEN/sendPhoto" \\
324                    -H "Content-Type: multipart/form-data" \\
325                    -F "chat_id=\$TG_CHAT_ID" \\
326                    -F "photo=@\${SECRETS_DIR}/otp_\${USER_ID}.png" \\
327                    -F "caption=\$TG_MESSAGE" > /dev/null 2>> "\${OCSERV_DIR}"/pam.log
328
329                echo "[\$(date '+%F %T')] - TOTP secret and QR code successfully sent to \$USER_ID via Telegram" | tee -a "\${OCSERV_DIR}"/pam.log
330            fi
331        }
332
333        if [[ "\$OTP_SEND_BY_TELEGRAM" == "true" ]]; then send_qr_by_telegram; fi
334
335    else
336        echo "OTP token already exists for \$USER_ID in \${SECRETS_DIR}/users.oath"
337        exit 1
338    fi
339else
340    echo "Usage: \$(basename "\$0") <user_id>"
341    exit 1
342fi
343_EOF_
344fi
345
346if [[ "$OTP_ENABLE" == "true" && ! -e "${SCRIPTS_DIR}"/otp_sender ]]; then
347cat << _EOF_ > "${SCRIPTS_DIR}"/otp_sender && chmod +x "${SCRIPTS_DIR}"/otp_sender
348#!/bin/bash
349
350set -Eeuo pipefail
351
352OCSERV_DIR="$OCSERV_DIR"
353SECRETS_DIR="$SECRETS_DIR"
354SCRIPTS_DIR="$SCRIPTS_DIR"
355OTP_SEND_BY_EMAIL="$OTP_SEND_BY_EMAIL"
356OTP_SEND_BY_TELEGRAM="$OTP_SEND_BY_TELEGRAM"
357TG_TOKEN="$TG_TOKEN"
358
359echo "[\$(date '+%F %T')] - PAM user \$PAM_USER is trying to connect to ocserv" >> "\${OCSERV_DIR}"/pam.log
360
361otp_sender_by_email() {
362    EMAIL_REGEX="^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
363    if [[ \$PAM_USER =~ \$EMAIL_REGEX ]]; then true; else return 0; fi
364
365    if [[ -e "\${SECRETS_DIR}"/users.oath ]] && grep -qP "(?<!\\S)\${PAM_USER}(?!\\S)" "\${SECRETS_DIR}"/users.oath; then
366        OTP_TOKEN="\$(oathtool --totp=SHA1 --time-step-size=30 --digits=6 \$(grep -P "(?<!\\S)\${PAM_USER}(?!\\S)" \${SECRETS_DIR}/users.oath | awk '{print \$4}'))"
367
368        echo -e "Subject: TOTP token for OpenConnect\n\n\${OTP_TOKEN}" | msmtp --file="\${SCRIPTS_DIR}"/msmtprc "\$PAM_USER"
369        echo "[\$(date '+%F %T')] - TOTP token successfully sent to \$PAM_USER" >> "\${OCSERV_DIR}"/pam.log
370    fi
371}
372
373otp_sender_by_telegram() {
374    TG_REGEX="^[a-zA-Z][a-zA-Z0-9_]{4,31}\$"
375    if [[ \$PAM_USER =~ \$TG_REGEX ]]; then true; else return 0; fi
376
377    if grep -qP "(?<!\\S)\${PAM_USER}(?!\\S)" "\${SECRETS_DIR}"/users.oath 2> /dev/null; then
378        OTP_TOKEN="\$(oathtool --totp=SHA1 --time-step-size=30 --digits=6 \$(grep -P "(?<!\\S)\${PAM_USER}(?!\\S)" \${SECRETS_DIR}/users.oath | awk '{print \$4}'))"
379        TG_MESSAGE="TOTP token for OpenConnect: \$OTP_TOKEN"
380        TG_USER_FILE="\${SCRIPTS_DIR}/tg_users.txt"
381        
382        if grep -qP "(?<!\\S)\$PAM_USER(?!\\S)" "\$TG_USER_FILE"; then
383            TG_CHAT_ID=\$(grep -P "(?<!\\S)\${PAM_USER}(?!\\S)" "\$TG_USER_FILE" | awk '{print \$1}')
384        else
385            TG_RESPONSE="\$(curl -s "https://api.telegram.org/bot\$TG_TOKEN/getUpdates")"
386            TG_CHAT_ID=\$(echo "\$TG_RESPONSE" | jq -r --arg USERNAME "\$PAM_USER" '.result[] | select(.message.from.username == \$USERNAME) | .message.chat.id')
387    
388            if [[ -z "\$TG_CHAT_ID" ]]; then
389                echo "[\$(date '+%F %T')] - User was not found or did not interact with the bot" >> "\${OCSERV_DIR}"/pam.log
390                return 0
391            fi
392            echo "\$TG_CHAT_ID \$PAM_USER" >> "\$TG_USER_FILE"
393        fi  
394        
395        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
396        echo "[\$(date '+%F %T')] - TOTP token successfully sent to \$PAM_USER" >> "\${OCSERV_DIR}"/pam.log
397    fi
398}
399
400if [[ "\$OTP_SEND_BY_EMAIL" == "true" ]]; then otp_sender_by_email; fi &
401
402if [[ "\$OTP_SEND_BY_TELEGRAM" == "true" ]]; then otp_sender_by_telegram; fi &
403_EOF_
404elif [[ "$OTP_ENABLE" == "true" && -e "${SCRIPTS_DIR}"/otp_sender ]]; then
405    sed -i "s|OCSERV_DIR=.*|OCSERV_DIR=\"$OCSERV_DIR\"|" "${SCRIPTS_DIR}"/otp_sender
406    sed -i "s|SECRETS_DIR=.*|SECRETS_DIR=\"$SECRETS_DIR\"|" "${SCRIPTS_DIR}"/otp_sender
407    sed -i "s|SCRIPTS_DIR=.*|SCRIPTS_DIR=\"$SCRIPTS_DIR\"|" "${SCRIPTS_DIR}"/otp_sender
408    sed -i "s|OTP_SEND_BY_EMAIL=.*|OTP_SEND_BY_EMAIL=\"$OTP_SEND_BY_EMAIL\"|" "${SCRIPTS_DIR}"/otp_sender
409    sed -i "s|OTP_SEND_BY_TELEGRAM=.*|OTP_SEND_BY_TELEGRAM=\"$OTP_SEND_BY_TELEGRAM\"|" "${SCRIPTS_DIR}"/otp_sender
410    sed -i "s|TG_TOKEN=.*|TG_TOKEN=\"$TG_TOKEN\"|" "${SCRIPTS_DIR}"/otp_sender
411fi
412
413# Add msmtprc config
414if [[ "$OTP_ENABLE" == "true" && "$OTP_SEND_BY_EMAIL" == "true" && ! -e "${OCSERV_DIR}"/msmtprc ]]; then
415cat << _EOF_ > "${SCRIPTS_DIR}"/msmtprc && chmod 400 "${SCRIPTS_DIR}"/msmtprc
416account default
417host $MSMTP_HOST
418port $MSMTP_PORT
419auth on
420user $MSMTP_USER
421password $MSMTP_PASSWORD
422from $MSMTP_FROM
423tls on
424tls_starttls off
425logfile $OCSERV_DIR/pam.log
426_EOF_
427fi
428
429# Config OTP with PAM
430pam_otp() {
431    if [[ $OTP_ENABLE == "true" ]]; then
432        until [[ -e /etc/pam.d/ocserv ]]; do sleep 5; done
433        if grep -q 'otp_sender' /etc/pam.d/ocserv && grep -q 'users.oath' /etc/pam.d/ocserv; then return 0; fi
434        sleep 3
435        echo "auth optional pam_exec.so ${SCRIPTS_DIR}/otp_sender" >> /etc/pam.d/ocserv
436        echo "auth requisite pam_oath.so debug usersfile=${SECRETS_DIR}/users.oath window=20" >> /etc/pam.d/ocserv
437    fi
438}
439
440# Start ocserv service
441if [[ -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
442    pam_otp &
443    echo "Starting OpenConnect Server"
444    exec "$@" || { echo "Starting failed" >&2; exit 1; }
445else
446    # Server certificates generation
447    certtool --generate-privkey --outfile "${CERTS_DIR}"/ca-key.pem
448    certtool --generate-self-signed --load-privkey "${CERTS_DIR}"/ca-key.pem --template "${CERTS_DIR}"/ca.tmpl --outfile "${CERTS_DIR}"/ca-cert.pem
449    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
450    if [[ ! -e "${SSL_DIR}"/live/"${SRV_CN}"/privkey.pem && ! -e "${SSL_DIR}"/live/"${SRV_CN}"/fullchain.pem ]]; then
451        certtool --generate-privkey --outfile "${SSL_DIR}"/live/"${SRV_CN}"/privkey.pem
452        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
453    fi
454    
455    pam_otp &
456    echo "Starting OpenConnect Server"
457    exec "$@" || { echo "Starting failed" >&2; exit 1; }
458fi
Нажмите, чтобы развернуть и увидеть больше

openconnect.service - файл unit’а systemd для автозапуска:

INI
 1[Unit]
 2Description=OpenConnect VPN service
 3Requires=docker.service
 4After=docker.service
 5
 6[Service]
 7Restart=on-failure
 8RestartSec=5
 9WorkingDirectory=/opt/openconnect
10ExecStart=/usr/bin/docker compose up
11ExecStop=/usr/bin/docker compose down
12
13[Install]
14WantedBy=multi-user.target
Нажмите, чтобы развернуть и увидеть больше

ssl_update.sh - bash скрипт обновления SSL сертификатов от Let’s Encrypt:

BASH
 1#!/usr/bin/env bash
 2
 3set -e
 4
 5WORK_DIR="/opt/openconnect"
 6CONTAINER_NAME="openconnect"
 7
 8to_log () {
 9    local text="$1"
10    echo "[$(date '+%F %T')] ${text}"
11}
12
13cd "$WORK_DIR" || exit 1
14
15if [[ -r ./docker-compose.yml ]]; then
16    to_log "Run certbot service container"
17    docker compose up certbot
18    sleep 3
19    to_log "Reload ocserv config"
20    docker exec "$CONTAINER_NAME" occtl reload
21    to_log "Delete all unused docker images"
22    docker system prune -af
23fi
Нажмите, чтобы развернуть и увидеть больше

Переопределение переменных: домен, имя сервера, email и пр.

Теперь необходимо задать переменные окружения, которые будут использованы для автоматической подстановки при сборке и запуске контейнера. Для этого открываем любым редактором файл .env, лежащий в директории с файлами проекта:

BASH
vim .env
Нажмите, чтобы развернуть и увидеть больше

Всего 5 переменных:

В комментариях подсказали, что в случае подключения к ocserv роутером Keenetic, «то порт в .env нужно ставить дефолтный 443 (который не нужно указывать в строке подключения)«. Иные порты, данный роутер, игнорирует.

Сохраняем файл и выходим:

VIM
:wq
Нажмите, чтобы развернуть и увидеть больше

Сборка образа и первый запуск контейнера с помощью docker-compose

Если у вас нет домена, переходите к шагам внутри спойлера ниже: «Клик сюда, если домена нет».

Теперь просто запускаем команду сборки образа с последующим запуском контейнера в фоновом режиме и после подключаемся к стандартному выводу docker-compose:

BASH
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.

Чтобы убедится, что контейнер запущен выполняем:

BASH
docker compose ps

ls -l ./data

ls -l ./data/ssl/live/vpn.r4ven.me/
Нажмите, чтобы развернуть и увидеть больше

Должна создаться папка ./data с файлами сервера + сертификаты, полученные контейнером cerbot.

Также проверяем прослушиваемый и проброшенный в хост контейнером TCP порт, такой командой:

BASH
ss -tnap | grep -E '43443'
Нажмите, чтобы развернуть и увидеть больше

Всё, сервер запущен и работает. Осталось создать пользователей и настроить клиентское подключение.

Отлично, идём дальше.

Создание пользователей

Пару слов про способы авторизации на OpenConnect сервере.

Т.к. ocserv популярен в корпоративном мире, он поддерживает множество способов авторизации пользователей. От банального логина/пароля, до интеграции с централизованными инструментами, такими, как LDAP/Active Directory, PAM, Radius и пр. Из коробки даже имеется поддержка двухфакторной аутентификации на основе токенов — этот функционал + возможность отправки токенов TOTP по почте и в Telegram добавлен в контейнер, описание приведу позже в этой статье.

В качестве надежного и в тоже время удобного способа авторизации я выбрал подключение на основе пользовательского сертификата, который упаковывается в шифрованный (при указании пароля) файл с расширением .p12. В результате со стороны клиента необходимо будет лишь указать путь до файла сертификата и пароль для его расшифровки. Обычно клиенты способны запоминать пароли и вводить их более не требуется.

Стоит отметить, что для создания пользователя ему все таки необходимо задать пароль, хоть и такой способ авторизации отключен в конфиг файле сервера. Если вы проявили любопытство и заглянули в содержимое скрипта создания пользователей ocuser, то могли заметить команду генерации псевдослучайной строки: tr -cd «[:alnum:]» < /dev/urandom | head -c 60

Вывод которой задается в качестве пароля для пользователя при его создании. Небольшая подстраховка)

Создание пользователей для Linux/Windows/Android

И так, создаём пользователя такой командой:

BASH
docker exec -it openconnect ocuser ivan 'Ivan Cherniy'
Нажмите, чтобы развернуть и увидеть больше

Где ivan — это имя и идентификатор пользователя в ocserv (старайтесь указывать идентификатор без пробелов, дабы избежать ошибок), а ‘Ivan Cherniy’ это полное имя пользователя, которое будет отображено в метаданных пользовательского сертификата.

После ввода команды необходимо будет в интерактивном режиме указать имя файла сертификата и пароль для его шифрования.

Если команда создания отработала успешно, для указанного пользователя будет создан файл сертификата .p12 в директории secrets:

BASH
ls -l ./data/secrets
Нажмите, чтобы развернуть и увидеть больше

Список пользователей можно посмотреть в файле ./data/ocpasswd.

Создание пользователей для HarmonyOS/iOS

При создании пользователей для мобильных ОС от Huawei и Apple команде ocuser необходимо передать ключ -A. В таком случае будет использован другой алгоритм шифрования сертификата.

Пример команды:

BASH
docker exec -it openconnect ocuser -A steve 'Steve Jobs'
Нажмите, чтобы развернуть и увидеть больше

Проверяем созданный сертификат:

BASH
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

Скачиваем созданный файл сертификата пользователя удобным для вас способом. Например:

BASH
cp ./data/secrets/ivan.p12 /tmp
Нажмите, чтобы развернуть и увидеть больше

А на клиентской машине:

BASH
scp vpn.r4ven.me:/tmp/ivan.p12 .
Нажмите, чтобы развернуть и увидеть больше

BASH
ls -l ivan.p12
Нажмите, чтобы развернуть и увидеть больше

На этом этапе уже можно переходить к настройке клиентов. Но если вам нужны персональные настройки каждого пользователя, такие как IP адрес клиента, маршруты, DNS, а также настройка автозапуска сервиса OpenConnect и автопродление SSL сертификатов, то идём последовательно.

Опционально: индивидуальные настройки ocserv для каждого пользователя

Создаём системную директорию config-per-user и отдельный файл под каждого пользователя, с таким же именем, какое указывали при создании (если забыли, посмотрите в файле ./data/ocpasswd):

BASH
mkdir ./data/config-per-user

vim ./data/config-per-user/ivan
Нажмите, чтобы развернуть и увидеть больше

Название параметров в файле конфига пользователя в большинстве своем идентичны основному конфигу сервера:

INI
explicit-ipv4 = 10.10.10.5
route = 10.10.10.50/32
route = 64.233.164.139/32
dns = 1.1.1.1
Нажмите, чтобы развернуть и увидеть больше

В примере выше я задаю для пользователя ivan конкретный IP (из подсети, что указана в основном конфиге), отдельные маршруты и адрес DNS сервера.

Upd. Если вы предпочитаете повысить уровень конфиденциальности, то при тунелировании трафика желательно иметь собственный DNS сервер. Вот инструкция по его настройке и интеграции с OpenConnect:

Поднимаем свой DNS сервер Unbound и блокировщик рекламы Pihole в docker

При такой конфигурации данный пользователь будет обращаться к указанным ресурсам через VPN сервер, а остальной трафик будет идти через его шлюз по умолчанию (или нет, в зависимости от настроек сети).

После внесения изменений в конфиг необходимо выполнить перечитывание файла конфигурации процессом ocserv внутри контейнера. Делается такой командой:

BASH
docker exec -it openconnect occtl reload
Нажмите, чтобы развернуть и увидеть больше

Автозапуск сервера OpenConnect с помощью systemd

Теперь настроим автоматический запуск/перезапуск нашего VPN сервера при помощи системы инициализации systemd.

Из директории с файлами проекта копируем файл openconnect.service в системную директорию systemd, перезагружаем конфигурацию systemd и останавливаем наши запущенные контейнеры:

BASH
cp ./openconnect.service /etc/systemd/system/

systemctl daemon-reload

docker compose down
Нажмите, чтобы развернуть и увидеть больше

Теперь активируем автозапуск и пробуем выполнить старт контейнера с OpenConnect сервером как сервис systemd:

BASH
systemctl enable --now openconnect

systemctl status openconnect

docker compose ps
Нажмите, чтобы развернуть и увидеть больше

Все отлично.

Настройка автоматического обновления SSL сертификатов (если есть домен)

Особенностью сертификатов от центра сертификации Let’s Encrypt является то, что они выдаются на 3 месяца. В такой ситуации предусмотрительно будет настроить автоматическое продление. Для этого добавляем в системный планировщик заданий cron запуск скрипта ssl_update.sh раз в неделю, например, в воскресенье в 3 часа утра:

BASH
{ crontab -l; echo "0 3 * * 0 /opt/openconnect/ssl_update.sh"; } | crontab -

crontab -l
Нажмите, чтобы развернуть и увидеть больше

Синтаксис фигурных скобок в bash { command1; command2 } позволяет объединить несколько команд в одну. Тут мы с его помощью первой командой выводим список заданий cron текущего пользователя, второй командой выводим строку с новым заданием и потом с помощью механизма перенаправления передаем весь вывод команде crontab -. Если команде crontab - передать только одну задачу, она затрет все предыдущие. Так делать не стоит)

Хоть и в моём примере нет других заданий в планировщике, команда специально составлена с «защитой от дурака»)

Для проверки работоспособности скрипта продления сертификатов, можем выполнить его вручную:

UPD 16.05.2024

В команде обновления certbot заменил параметр --force-renewal на --keep-until-expiring, который выполняет проверку срока действия сертификата, и если он не близок к завершению, то обновление сертификатов не происходит.

BASH
/opt/openconnect/ssl_update.sh
Нажмите, чтобы развернуть и увидеть больше

Скрипт также почистит дисковое пространство от неиспользуемых образов docker:

Проверяем время обновления SSL файлов:

BASH
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).

Команда установки плагина:

BASH
sudo apt update && sudo apt install network-manager-openconnect-gnome
Нажмите, чтобы развернуть и увидеть больше

После установки:

  1. Заходим в «Сетевые соединения» через трей или любым удобным вам способом;
  2. Затем нажимаем кнопку добавления нового соединения, в списке выбираем «Cisco AntConnect or openconnect (OpenConnect)» и нажимаем «Создать«;
  3. В открвшемся окошке указываем
    • «Имя соединения» — произвольное
    • «Шлюз» — DNS имя нашего сервера или его IP адрес (если без домена) + порт подключения. Пример: vpn.r4ven.me:43443
  4. В разделе «Аутентификации по сертификату» в параметре «Сертификат пользователя» выбираем наш .p12 файл, который мы сгнерировали на этапе создания пользователя. В старых версиях nmapplet’а есть баг, когда при выборе сертификата он не видит файлы с расширенияем .p12. В таком случае просто перетащите (drag-and-drop) файл с файлового менеджера в поле данного параметра, как показано на скриншоте ниже;
  5. После нажимаем «Сохранить».

Если все сделано корректно, пробуем подключиться к нашему серверу. Кликаем на апплет сетевых соединений, затем на переключатель «VPN подключения«. Должно появиться окошко, где нас любезно попросят ввести пароль от файла-сертификата (если вы его задавали), который мы устанавливали на этапе создания пользователей ocserv. Ставим галочку «Сохранить пароль» и затем «Подключиться«. При успешном соединении на рабочем столе появится соответствующее уведомление:

Также новое подключение для NetworkManager можно создать с помощью консольной утилиты nmcli. Вот пример:

BASH
nmcli connection add \
    type vpn \
    con-name "vpn.r4ven.me" \
    ifname '*' \
    vpn-type openconnect \
    vpn.data "gateway=vpn.r4ven.me:43443, usercert=/home/ivan/ivan.p12"
Нажмите, чтобы развернуть и увидеть больше

Пробуем подключиться:

BASH
nmcli connection up vpn.r4ven.me
Нажмите, чтобы развернуть и увидеть больше

Успешно.

Посмотреть сетевые параметры нашего VPN подключения можно с помощью команд:

BASH
ip -c address
Нажмите, чтобы развернуть и увидеть больше

BASH
nmcli
Нажмите, чтобы развернуть и увидеть больше

Узнать внешний IP в консоли можно такой командой:

BASH
curl ifconfig.me
Нажмите, чтобы развернуть и увидеть больше

В выводе команды будет одна строка с вашим внешним IP адресом.

Способ №2: утилита командной строки — openconnect

Другой способ клиентского подключения в Linux, где отсутствует NetworkManager — это одноименная консольная утилита openconnect. Такой способ подключения чаще всего используется для клиентов без GUI, т.е. на Linux серверах.

Пример подключения все также для deb based систем.

Выполняем установку клиентского пакета openconnect:

BASH
sudo apt update && sudo apt install openconnect
Нажмите, чтобы развернуть и увидеть больше

Подключится к серверу OpenConnect можно такой командой:

BASH
# если с доменом
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, чтобы не вводить вручную.

Замените password на ваш пароль от сертификата.

Для автоматизации процесса подключения/переподключения с помощью утилиты openconnect я написал небольшой скрипт. Изучить его можно в статье: Пишем bash скрипт для подключения к OpenConnect VPN серверу.

Результат команды подключения должен быть примерно таким:

Если вы используете подключение с помощью утилиты openconnect на Linux сервере, и планируете туннелировать весь его трафик, то необходимо добавить специальные правила маршрутизации, без которых вы потеряете доступ до своего сервера, в т.ч. и по SSH:

SQL
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>
Нажмите, чтобы развернуть и увидеть больше

Где:

PLAINTEXT
- **128** — имя новой таблицы маршрутизации;
- **<piblic-ip>** — основной IP вашего сервера;
- **<public\_ip\_subnet>** — подсеть основного IP сервера;
- **<interface\_name>** — имя физического интерфейса, к которому подключен основной IP;
- **<gateway>** — шлюз сети, через который ходит основной IP.
Нажмите, чтобы развернуть и увидеть больше

Настройка OpenConnect клиента для Windows/MacOS

Для настройки клиента на Windows и MacOS необходимо скачать графическую программу-клиент с официального сайта https://gui.openconnect-vpn.net/download/ или на странице релизов в GitLab: https://gitlab.com/openconnect/openconnect-gui/-/releasesGitLab:

Далее устанавливаем программу на рабочий стол Windows обычными кликами «Далее, далее..». А вот как установить клиента на MacOS я вам не подскажу. Тут уже вы сами изучите вопрос, ссылки на все материалы будут внизу.

После установки и запуска программы OpenConnectVPN выполняем настройку, как показано на скриншотах ниже. Отмечу лишь один момент: обязательно активируйте параметр «Disable UDP«, т.к. в нашей конфигурации сервера его использование отключено:

Проверяем сетевые параметры:

Все работает.

Настройка OpenConnect клиента для Android/HarmonyOS

Для мобильных ОС необходимо установить приложение Cisco Secure Client-AnyConnect.

Пожалуйста, обратите внимание, что данное мобильное приложение не имеет открытый исходный код. При его использовании придется довериться разработчику.

Существует open source мобильный клиент, но его разработка прекратилась n-е количество лет назад и у меня оно работало некорректно.

После открытия приложения необходимо создать подключение, указать адрес шлюза + порт и импортировать сертификат пользователя. Пошаговая инструкция показана на скриншотах ниже:

Настройка OpenConnect клиента для iOS

Инструкция любезно предоставлена подписчиком в Вороньем чате

Скачиваешь файл на телефон. Открываешь файлы и зажимаешь этот значок (просто при тычке он будет писать техническое сообщение о том, что профиль загружен). Выбираешь пункт поделиться — Еще — Листаешь в самый низ, там будет Any Connect. Открывается приложение и запрашивает пароль. Вводишь и тыкаешь импорт.
И дальше уже по твоей схеме в настройках туннеля выбираешь сертификат.

Ну вот, вроде бы всё.

Настройка OpenConnect клиента для OpenWrt

Задача не тривиальная, поэтому вынесена в отдельную статью:

Блокировка пользователей путём отзыва сертификата

При необходимости заблокировать какого-то пользователя, сделать это можно с помощью отзыва его SSL сертификата.

Если вы только что подняли ocserv по этой инструкции, просто воспользуйтесь командой ocrevoke внутри контейнера:

BASH
# отозвать сертификат пользователя ivan
docker exec -it openconnect ocrevoke ivan

# перечитать черный список пользователей
docker exec -it openconnect ocrevoke RELOAD

# удалить всех из черного списка
docker exec -it openconnect ocrevoke RESET
Нажмите, чтобы развернуть и увидеть больше

Где вместо ivan подставьте имя пользователя, доступ по сертификату которого нужно заблокировать.

При следующем подключении пользователь получит ошибку:

Если же ваш сервер был поднят по этой инструкции некоторое время назад, придется выполнить немного ручной работы.

Сперва в конец конфига ./data/ocserv.conf добавляем параметр:

BASH
crl = /etc/ocserv/certs/crl.pem
Нажмите, чтобы развернуть и увидеть больше

Затем выполняем команды:

BASH
cat ./data/certs/ivan-cert.pem >> ./data/certs/revoked.pem

docker exec -it openconnect certtool --generate-crl --load-ca-privkey /etc/ocserv/certs/ca-key.pem --load-ca-certificate /etc/ocserv/certs/ca-cert.pem --load-certificate /etc/ocserv/certs/revoked.pem --template /etc/ocserv/certs/crl.tmpl --outfile /etc/ocserv/certs/crl.pem

docker exec -it openconnect occtl reload
Нажмите, чтобы развернуть и увидеть больше

Где вместо ivan в первой команде подставьте имя пользователя, доступ по сертификату которого нужно заблокировать.

Как вы догадались, ключевым тут является файл ./data/certs/revoked.pem, куда добавляются публичные сертификаты пользователей <user_name>-cert.pem.

Для очистки всего черного списка выполните:

BASH
docker exec -it openconnect certtool --generate-crl --load-ca-privkey /etc/ocserv/certs/ca-key.pem --load-ca-certificate /etc/ocserv/certs/ca-cert.pem --template /etc/ocserv/certs/crl.tmpl --outfile /etc/ocserv/certs/crl.pem

docker exec -it openconnect occtl reload
Нажмите, чтобы развернуть и увидеть больше

При отсутствии необходимости данного функционала, просто закомментируйте строку crl = /etc/ocserv/certs/crl.pem в конфиге ocserv.

Авторизация по логину/паролю

В некоторых случаях может понадобиться включение возможности авторизации по логину и паролю. Например при настройке openconnect на роутерах Keenetic, который не поддерживает подключение на основе сертификата.

В файле конфигурации ocserv — /opt/openconnect/data/ocserv.conf разрешаем авторизацию по логину/паролю, путем добавления такого параметра:

BASH
enable-auth = "plain[passwd=/etc/ocserv/ocpasswd]"
Нажмите, чтобы развернуть и увидеть больше

Стоит отметить, что при такой конфигурации авторизация по сертификату останется.

Затем перезапускаем сервер:

BASH
docker compose restart openconnect
Нажмите, чтобы развернуть и увидеть больше

Далее создаём нового пользователя:

BASH
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

Добавить/изменить параметр:

BASH
OTP_ENABLE="true"
Нажмите, чтобы развернуть и увидеть больше

Правка ocserv.conf

Выбрать один из способов аутентификации, для которой будет применяться второй фактор:

INI
auth = "plain[passwd=/etc/ocserv/ocpasswd,otp=/etc/ocserv/secrets/users.oath]"
Нажмите, чтобы развернуть и увидеть больше

При работе такой аутентификации необходимо создать пользователя стандартным способом и задать ему пароль. Пример:

BASH
docker exec -it openconnect ocpasswd ivan
Нажмите, чтобы развернуть и увидеть больше
INI
auth = "pam"
Нажмите, чтобы развернуть и увидеть больше

В случае PAM, вам также необходимо прокинуть файлы со списком локальных пользователей хостовой ОС в контейнер в файле docker-compose.yml:

YAML
volumes:
  - ./data:/etc/ocserv
  - /etc/passwd:/etc/passwd:ro
  - /etc/group:/etc/group:ro
  - /etc/shadow:/etc/shadow:ro
Нажмите, чтобы развернуть и увидеть больше

Пример создания локального пользователя Linux без домашней директории и без возможности входа в сеанс оболочки ОС:

BASH
adduser --no-create-home --allow-bad-names --quiet --shell /bin/false --comment "Ocserv user" ivan@r4ven.me
Нажмите, чтобы развернуть и увидеть больше

Перезапуск сервиса

Теперь пересоздаем контейнер:

BASH
systemctl restart openconnect
Нажмите, чтобы развернуть и увидеть больше

Двухфакторная аутентификация активирована, осталось настроить ее для существующих пользователей. Для этого воспользуйтесь специальной командой/скриптом ocuser2fa. Пример для пользователя ivan:

BASH
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:

Получать одноразовые пароли можно также в консоли Linux с помощью утилиты oathtool:

BASH
apt install -y oathtool

oathtool --base32 --totp=SHA1 --time-step-size=30 --digits=6 <totp_base32_secret>
Нажмите, чтобы развернуть и увидеть больше

Замените <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. Вот пример:

BASH
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, пример:

BASH
TG_TOKEN="1234567890:QWERTYqwerty-QWERTY123_QwErTy123qWeRtY123"
# для отправки одноразовых паролей при каждой авторизации
OTP_SEND_BY_TELEGRAM="true"
Нажмите, чтобы развернуть и увидеть больше

2) пользователю, которому будут отправляться секрет с QR кодом и одноразовые токены необходимо написать вашему боту в чат любое сообщение в течение последних 24 часов (требуется единожды)

3) имя пользователя ocserv должно четко совпадать с nickname пользователя в Telegram

Перезапуск сервиса

Для применения изменений перезапускаем сервис:

BASH
systemctl restart openconnect
Нажмите, чтобы развернуть и увидеть больше

Проверка

Настраиваем 2FA для существующего пользователя с именем ввиде email адреса:

BASH
docker exec -it openconnect ocuser2fa ivan@r4ven.me
Нажмите, чтобы развернуть и увидеть больше

На почту должны приходить подобные письма:

Для пользователя с именем, совпадающим с именем телеграм:

BASH
docker exec -it openconnect ocuser2fa ivan
Нажмите, чтобы развернуть и увидеть больше

В чате с ботом будет подобное:

Защита от автоматизированного доступа — Camouflage

С версии ocserv >= 1.23 доступен дополнительный слой защиты от сканирования интернет ботами, которые могут в автоматическом режиме подобирать резкизиты для доступа к внутренней сети. Для активации защиты внесите следующие параметры в ваш ocserv.conf:

INI
# включение защиты от active probing
camouflage = true
# дополнительная часть url для подключения
camouflage_secret = "secretword"
# активация ложного окна авторизации, которое отклоняет любые попытки входа
#camouflage_realm = "My admin panel"
Нажмите, чтобы развернуть и увидеть больше

И перезапустите сервер:

BASH
systemctl restart openconnect
Нажмите, чтобы развернуть и увидеть больше

Теперь для подключения к серверу на клиенте необходимо указывать адрес с дополнительным сабурлом, указанного в конфиге. Пример:

BASH
https://vpn.r4ven.me:43443/?secretword
Нажмите, чтобы развернуть и увидеть больше

А все запросы к базовому урлу: https://vpn.r4ven.me:43443/ будут завершаться ошибкой клиента — 404. А если активен параметр camouflage_realm = "My admin panel", то запросы будут упираться в ложное окно авторизации.

Полезные команды systemctl/docker/docker-compose/ocserv

Небольшой список команд, который может вам пригодится при настройке и обслуживании OpenConnect сервера.

BASH
# управление сервисом 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. Ссылки на все новые статьи появляются там в момент публикации.

Спасибо, что вместе со мной осилили данную статью. Успехов вам!

Используемые материалы

Комментарии












Авторские права

Автор: Иван Чёрный

Ссылка: https://r4ven.me/networking/podnimaem-openconnect-ssl-vpn-server-ocserv-v-docker-dlya-vnutrennih-proektov/

Лицензия: CC BY-NC-SA 4.0

Использование материалов блога разрешается при условии: указания авторства/источника, некоммерческого использования и сохранения лицензии.

Начать поиск

Введите ключевые слова для поиска статей

↑↓
ESC
⌘K Горячая клавиша