Building OpenConnect (ocserv) Version 1.3 From Source in Debian 12 + Docker Image
Greetings!

At the request of some subscribers from our raven’s chat, today we’ll build the OpenConnect VPN server, also known as ocserv, latest version — 1.3, from open source code, on the Debian 12 distribution. We’ll also create a Docker image based on the same distribution.

If you’re not sure what kind of beast this is, I recommend reading my other article: Setting up an OpenConnect SSL VPN server (ocserv) in Docker for internal projects

All actions in this article will be performed in the following configuration:

Let’s go 🙃

Building ocserv from source

First, let’s update our current system to the latest version with this command:

BASH
sudo apt update && sudo apt upgrade -y
Click to expand and view more

Next, we add the Debian Sid repository — the “unstable” packages repository of the distribution, since it contains the library versions we need. Afterward, be sure to update the cache to apply the changes:

BASH
echo "deb http://deb.debian.org/debian sid main" | sudo tee -a /etc/apt/sources.list

sudo apt update
Click to expand and view more

The tee command takes input data and writes it to a file, while also printing it to standard output (stdout). The -a (append) flag means append the received content to the file, rather than overwriting it entirely.

Next, we need to install a large list of dependencies 😳 Make sure you have free space on your disk for this.

BASH
sudo DEBIAN_FRONTEND=noninteractive apt install -y build-essential fakeroot devscripts \
        iputils-ping ruby-ronn openconnect libuid-wrapper \
        libnss-wrapper libsocket-wrapper gss-ntlmssp git-core make autoconf \
        libtool autopoint gettext automake nettle-dev libwrap0-dev \
        libpam0g-dev liblz4-dev libseccomp-dev libreadline-dev libnl-route-3-dev \
        libkrb5-dev liboath-dev libradcli-dev libprotobuf-dev libtalloc-dev \
        libhttp-parser-dev libpcl1-dev protobuf-c-compiler gperf liblockfile-bin \
        nuttcp libpam-oath libev-dev libgnutls28-dev gnutls-bin haproxy \
        yajl-tools libcurl4-gnutls-dev libcjose-dev libjansson-dev libssl-dev \
        iproute2 libpam-wrapper tcpdump libopenconnect-dev iperf3 ipcalc-ng \
        freeradius libfreeradius-dev
Click to expand and view more

The DEBIAN_FRONTEND=noninteractive variable tells the apt package manager to perform the installation without user interaction. That is, when resolving conflicts during installation, apt will apply the default action. Remember that you’re doing all of this at your own risk and responsibility.

After installing all the necessary packages, we download the ocserv source archive using the curl utility. Then we extract the archive using the tar archiver:

BASH
curl -fLO https://www.infradead.org/ocserv/download/ocserv-1.3.0.tar.xz

ls

tar -xvf ./ocserv-1.3.0.tar.xz
Click to expand and view more

Flags used with tar:

  • -x — eXtract, obviously the extraction itself;
  • -vVerbose, detailed output of the extraction process;
  • -fFile, followed by the archive file name.

After extraction, we go into the created source directory and run the build, as well as the check, for ocserv, all as root:

BASH
cd ./ocserv-1.3.0

sudo sh -c './configure --enable-oidc-auth && make && make check'
Click to expand and view more

Using the construct sh -c 'commands', we combined several commands into one. This way, it’s easier to run them by specifying sudo only once.

And the --enable-oidc-auth parameter in the configure command tells it to build with OpenID Connect support.

As a result, a build with an almost default configuration will be performed:

The process can take a long time, depending on your hardware and compiler configuration.

So the build is complete, but… there are 2 failed tests visible:

Installing and running ocserv

To install the built files onto your system, run:

BASH
sudo make install
Click to expand and view more

Here you’ll see what was installed/copied and where.

Now let’s check:

BASH
whereis ocserv

sudo ocserv --version
Click to expand and view more

Note that ocserv is installed to /usr/local/sbin by default.

I won’t describe in full detail what’s needed to run ocserv 😐 Otherwise the article would get too long, and that’s not its purpose anyway. For ease of setup, you can use my ready-made bash script, which I made for running ocserv in Docker in the previous article.

To do this, you need to download it from my GitHub and run it as follows:

BASH
sudo mkdir /etc/ocserv

curl -fLO https://raw.githubusercontent.com/r4ven-me/openconnect/main/src/server/v1.3/ocserv.sh

chmod +x ./ocserv.sh

sudo ./ocserv.sh ocserv --foreground
Click to expand and view more

On the first run, the script will generate all the necessary certificates with default values. If needed, edit them at the beginning of the ocserv.sh script.

Checking it in a neighboring tab:

BASH
ss -tulnap | grep 443

curl --insecure https://localhost:443
Click to expand and view more

The ss utility lets you view the OS’s network activity. The --insecure parameter in curl lets you make requests while ignoring warnings about self-signed SSL certificates.

Excellent, everything’s working. To exit, press Ctrl+c in the main tab.

Building from source inside Docker

Now something a bit more interesting 😌

My subscribers know that I prefer to deploy personal services in Docker. This greatly simplifies their maintenance, scalability, and portability.

So now, we’ll build ocserv 1.3 in one Docker container with Debian 12, and in another, identical one, install a couple of dependencies and simply copy the ready-made executable files from the build image, so that the final one ends up small in size.

Essentially, it’s all the same as what we did before, just inside containers without cluttering up the main system.

To build it, we’ll need Docker engine installed. If that’s not done yet, I recommend my article: Installing Docker Engine on a Linux Server Running Debian.

Creating a Docker image for building ocserv 1.3

So, we create a new directory and copy the Docker file describing the build container from my GitHub into it:

BASH
mkdir ~/ocserv && cd ~/ocserv/

curl -fLO https://raw.githubusercontent.com/r4ven-me/openconnect/main/src/server/v1.3/Dockerfile_build
Click to expand and view more

Contents of Dockerfile_build:

BASH
FROM debian:12

LABEL maintainer="Ivan Cherniy <kar-kar@r4ven.me>"

ARG DEBIAN_FRONTEND=noninteractive

SHELL ["/bin/bash", "-Eeuo", "pipefail", "-c"]

RUN echo "deb http://deb.debian.org/debian sid main" >> /etc/apt/sources.list && \
    apt update && \
    apt upgrade -y && \
    apt install --yes curl build-essential fakeroot devscripts \
        iputils-ping ruby-ronn openconnect libuid-wrapper \
        libnss-wrapper libsocket-wrapper gss-ntlmssp git-core make autoconf \
        libtool autopoint gettext automake nettle-dev libwrap0-dev \
        libpam0g-dev liblz4-dev libseccomp-dev libreadline-dev libnl-route-3-dev \
        libkrb5-dev liboath-dev libradcli-dev libprotobuf-dev libtalloc-dev \
        libhttp-parser-dev libpcl1-dev protobuf-c-compiler gperf liblockfile-bin \
        nuttcp libpam-oath libev-dev libgnutls28-dev gnutls-bin haproxy \
        yajl-tools libcurl4-gnutls-dev libcjose-dev libjansson-dev libssl-dev \
        iproute2 libpam-wrapper tcpdump libopenconnect-dev iperf3 ipcalc-ng \
        freeradius libfreeradius-dev &&\
    curl -fLO https://www.infradead.org/ocserv/download/ocserv-1.3.0.tar.xz && \
    tar -xvf ./ocserv-1.3.0.tar.xz && \
    cd ./ocserv-1.3.0/ && \
    ./configure --enable-oidc-auth && make && \
    mkdir /usr/share/doc/ocserv/ && \
    cp ./doc/sample.config /usr/share/doc/ocserv/

WORKDIR /ocserv-1.3.0

CMD [ "sleep", "999999" ]
Click to expand and view more

And we run the build with the command (will take some time):

BASH
docker build -f Dockerfile_build ./ -t openconnect-build:v1.3
Click to expand and view more

In this command, the -f flag explicitly specifies the build description file, ./ means to use the current directory as the working directory, and openconnect-build:v1.3 is the name of the image being built along with its tag (v1.3), which we’ll refer to when building the final image.

Done. After it finishes, let’s check:

BASH
docker image ls
Click to expand and view more

A full 1.4 GB, a bit chunky 😳 But, as I mentioned earlier, this is the build image. Next, we’ll only copy the compiled ocserv files from it into the final image.

Creating the final Docker image with ocserv 1.3

Again, let’s copy from my GitHub the Docker file describing the final image + the previously mentioned bash script for running ocserv:

BASH
curl -fLO https://raw.githubusercontent.com/r4ven-me/openconnect/main/src/server/v1.3/Dockerfile

curl -fLO https://raw.githubusercontent.com/r4ven-me/openconnect/main/src/server/v1.3/ocserv.sh

chmod +x ./ocserv.sh
Click to expand and view more

ocserv.sh will be copied into the image during the build.

Contents of Dockerfile:

YAML
FROM openconnect-build:v1.3 AS builder

FROM debian:12

LABEL maintainer="Ivan Cherniy <kar-kar@r4ven.me>"

STOPSIGNAL SIGTERM

COPY --from=builder ["/ocserv-1.3.0/src/occtl/occtl", "/usr/bin"]
COPY --from=builder ["/ocserv-1.3.0/src/ocpasswd/ocpasswd", "/usr/bin"]
COPY --from=builder ["/ocserv-1.3.0/src/ocserv-fw", "/usr/libexec"]
COPY --from=builder ["/ocserv-1.3.0/src/ocserv", "/usr/sbin"]
COPY --from=builder ["/ocserv-1.3.0/src/ocserv-worker", "/usr/sbin"]
COPY --from=builder ["/usr/share/doc/ocserv", "/usr/share/doc/ocserv"]

ENV OCSERV_DIR="/etc/ocserv"
ENV CERTS_DIR="${OCSERV_DIR}/certs"
ENV SSL_DIR="${OCSERV_DIR}/ssl"
ENV SECRETS_DIR="${OCSERV_DIR}/secrets"
ENV PATH="${OCSERV_DIR}:${PATH}"

ARG DEBIAN_FRONTEND=noninteractive

RUN echo "deb http://deb.debian.org/debian sid main" >> /etc/apt/sources.list && \
    apt update && \
    apt install -y --no-install-recommends \
        libllhttp9.1 \ 
        libtalloc2 \
        libradcli4 \
        liboath0 \
        libpcl1 \
        libev4 \
        libprotobuf-c1 \
        libreadline8 \
        libnl-route-3-200 \
        libcurl3-gnutls \
        libcjose0 \
        less \
        gnutls-bin \
        iptables \
        iproute2 \
        iputils-ping && \
    mkdir /etc/ocserv/ && \
    apt autoremove --yes && \
    apt clean -y && \
    rm -rf /var/lib/{apt,dpkg,cache,log}/*

COPY ./ocserv.sh /

ENTRYPOINT ["/ocserv.sh"]

CMD ["ocserv", "--config", "/etc/ocserv/ocserv.conf", "--foreground"]

HEALTHCHECK --interval=5m --timeout=3s \
  CMD  pidof -q ocserv || exit 1
Click to expand and view more

Contents of ocserv.sh:

BASH
#!/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}"

# Ocserv vars (do not modify)
OCSERV_DIR="/etc/ocserv"
CERTS_DIR="${OCSERV_DIR}/certs"
SSL_DIR="${OCSERV_DIR}/ssl"
SECRETS_DIR="${OCSERV_DIR}/secrets"

# Start server if data files exist
if [[ -r "${OCSERV_DIR}"/ocserv.conf ]]; then
    echo "Starting OpenConnect Server"
    exec "$@" || { echo "Starting failed" >&2; exit 1; }
else
    echo "Running OpenConnect Server at first with new certs generation"
fi

# Create certs dirs
if [[ -d $OCSERV_DIR ]]; then
    for sub_dir in "${OCSERV_DIR}"/{"ssl/live/${SRV_CN}","certs","secrets"}; do
        mkdir -p "$sub_dir"
    done
    if [[ -r /usr/share/doc/ocserv/sample.config ]]; then
        cp /usr/share/doc/ocserv/sample.config "${OCSERV_DIR}"/
    fi
fi

# Create ocserv config file
cat << _EOF_ > "${OCSERV_DIR}"/ocserv.conf
auth = "certificate"
#auth = "plain[passwd=${OCSERV_DIR}/ocpasswd]"
#enable-auth = "certificate"
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 = ${OCSERV_DIR}/connect.sh
disconnect-script = ${OCSERV_DIR}/disconnect.sh
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
_EOF_

# Create template for CA SSL cert
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_

# Create template for users SSL certs
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_

# Create template for server self-signed SSL cert
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_

# Generate empty revoke file
cat << _EOF_ > "${CERTS_DIR}"/crl.tmpl
crl_next_update = 365
crl_number = 1
_EOF_

# Create connect script which runs for every user connection
cat << _EOF_ > "${OCSERV_DIR}"/connect.sh && chmod +x "${OCSERV_DIR}"/connect.sh
#!/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_

# Create disconnect script which runs for every user disconnection
cat << _EOF_ > "${OCSERV_DIR}"/disconnect.sh && chmod +x "${OCSERV_DIR}"/disconnect.sh
#!/bin/bash

set -Eeuo pipefail

echo "\$(date) User \${USERNAME} Disconnected - Bytes In: \${STATS_BYTES_IN} Bytes Out: \${STATS_BYTES_OUT} Duration:\${STATS_DURATION}"
_EOF_

# Create script to create new users
cat << _EOF_ > "${OCSERV_DIR}"/ocuser && chmod +x "${OCSERV_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_

# Add revoke script
cat << _EOF_ > "${OCSERV_DIR}"/ocrevoke && chmod +x "${OCSERV_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_

# 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

# Start ocserv service
echo "Starting OpenConnect Server"
exec "$@" || { echo "Starting failed" >&2; exit 1; }
Click to expand and view more

And we run the build with the command:

BASH
docker build -f Dockerfile ./ -t openconnect:v1.3
Click to expand and view more

Note that the build file name and image tag are different.

Done.

Let’s look at the list of images:

BASH
docker image ls
Click to expand and view more

The difference is obvious.

Running ocserv in a Docker container

A test run can be done like this:

BASH
docker run --rm --detach openconnect:v1.3

docker ps
Click to expand and view more

But I recommend using docker compose:

BASH
curl -fLO https://raw.githubusercontent.com/r4ven-me/openconnect/main/src/server/v1.3/docker-compose.yml

curl -fLO https://raw.githubusercontent.com/r4ven-me/openconnect/main/src/server/v1.3/.env
Click to expand and view more

Fill in the .env file as needed and run:

BASH
docker compose up -d

docker compose ps
Click to expand and view more

Or even better, check out the previously mentioned article: Setting up an OpenConnect SSL VPN server (ocserv) in Docker for internal projects.

Conclusion

I think this turned out to be a pretty illustrative case of what building software from source looks like. I’ll note that a lot in this situation depends on the documentation that the developers write. In the case of building ocserv, the documentation isn’t bad, but far from perfect. Some issues took me quite a lot of time. But on the other hand, this is open source software, and most often it’s distributed under licenses that don’t imply any warranty. This is when software is, as they say, “provided as is.”

In any case, this was an interesting and definitely useful experience for me.

If you still have questions or something to discuss, drop by our Telegram chat: @r4ven_me_chat. And of course subscribe to the main channel there too: @r4ven_me — links to all new posts arrive there on the day of publication. Linux quizzes are also held there 😉

Thanks for reading. I wish you error-free builds from source 😎

Materials used

Comments



Copyright Notice

Author: Ivan Cherniy

Link: https://r4ven.me/en/virtualization/sobiraem-openconnect-ocserv-versii-1-3-iz-ishodnikov-v-debian-12-docker-obraz/

License: CC BY-NC-SA 4.0

Blog materials may be used with attribution to the author and source, for non-commercial purposes, and under the same license.

Start searching

Enter keywords to search articles

↑↓
ESC
⌘K Shortcut