I don't need this tool anymore.
LD_PRELOAD
hook for the OpenVPN client to replace the auth-user-pass
password on the fly.
This crate provides an OpenVPN client LD_PRELOAD
hook that replaces, on the
fly, the user password defined in the configured auth-user-pass
file.
The hook is designed to be used in combination with the auth-user-pass
OpenVPN
configuration directive (or --auth-user-pass
command line argument). This
directive specifies the path to a file that contains credentials for client
authentication. The auth-user-pass
file is a plain text file that typically
contains two lines: the first line is the username, and the second line is the
password.
When OpenVPN reads the contents of the auth-user-pass
file, the hook replaces
the password transparently. OpenVPN only ever sees the replacement password. The
actual password stored in the file is never really used when the hook is
enabled, but a password must be there nonetheless. It's value can be considered
a decoy.
It works by intercepting the fopen
, fgets
and fclose
functions, which
happen to be used by OpenVPN to read the auth-user-pass
. An additional
AUTH_FILE_PATH
environment variable passed at runtime allows the hook to
identify which file to track and replace the password in, while ignoring other
files.
The replacement password is stored in the dynamic library binary itself, in encrypted form. This means that the legitimate password must be provided at build time. It also means that in order to use the hook for two different client connections, two different binaries must be built (assuming said connections use different passwords).
The encryption key applied to the password is derived from the machine-id. Given that the password must be encrypted at build time and decrypted at runtime, it follows that the hook must be built on the same machine where it will be used.
The auth-user-pass
file is a plain text file that contains the user password.
Even if it is only readable by the user running the OpenVPN client when
properly configured, it is still a plain text password that is stored in the
filesystem. This is a security risk.
This felt like a good opportunity to learn about LD_PRELOAD
hooks and dynamic
libraries in Rust and overengineer a solution to a problem that doesn't seem to
matter.
In that spirit, this hook aims to mitigate this risk by replacing the password on the fly. The password is stored in encrypted form in the dynamic library binary itself. The encryption key is derived from the machine-id, which means that the password can only be decrypted on the same machine where the hook was built.
Despite using encryption at some point, this hook just provides obfuscation, not real security. Getting a hold of the binary and the machine-id is enough to reverse engineer the password. Both factors are stored in the same machine, so it is not a stretch to assume that an attacker that has access to one of them will also have access to the other.
Still, this is probably better than storing the password in plain text in the
auth-user-pass
file.
This hook has been tested with OpenVPN 2.6.5 on Linux.
It is likely to work with other versions of OpenVPN, as long as they use the
same auth-user-pass
file format and the same fopen
, fgets
and fclose
calls to read the file. That's unlikely to change any time soon.
In its current form, it is unlikely to work on other operating systems, as it
uses the Linux-specific /etc/machine-id
. Maybe it could also work on Mac by
manually writing that file. Unclear.
Normally we would only need to run cargo tests
. However, the tests require
that the dynamic library is available, which is not the case if we haven't built
it yet. This is a chicken and egg problem that has been solved within cargo for
executables (see CARGO_BIN_EXE_<name>
), but not for dynamic
libraries (see these issues).
We can work around this limitation by building first and then running the tests:
cargo build && cargo test
Additionally, cargo test
requires gcc
to build a small C application as part of our integration tests.
The installation is a three step process:
- Build the hook.
- Install the hook in a location where it can be found by the OpenVPN client.
- Configure the OpenVPN client to use the hook.
As discussed before, it must be built on the same machine where it will be used.
The hook dynamic library can be built with Cargo, providing the password as a
build argument in the BUILD_ARG_PASSWORD
environment variable. But being
careful not to leak the password in the shell history. For example, by using the
provided build script, which prompts for the password:
$ ./build.sh
Password:
Compiling openvpn-auth-hook v0.1.0 (/home/manu/private/repos/openvpn-auth-hook)
Finished release [optimized] target(s) in 3.22s
The dynamic library has been written to target/release/libopenvpn_auth_hook.so
We can also quickly verify that the hook works by running the test application,
which was automatically built during cargo test
:
$ echo "user\npw" > /tmp/test_file
# WARNING: Prints the password in the terminal!
$ LD_PRELOAD=target/release/libopenvpn_auth_hook.so AUTH_FILE_PATH=/tmp/test_file tests/test_app /tmp/test_file 4096
user
<the_password_entered_during_build_sh>
It just needs to be copied somewhere where the OpenVPN client can use it and
with appropriate permissions. But its permissions need to be restricted as much
as possible, exactly like it is done for the auth-user-pass
file. For example:
# As root
# Assuming OpenVPN client is run as user `openvpn`
install -Dm400 -o openvpn target/release/libopenvpn_auth_hook.so /usr/local/lib/libopenvpn_auth_hook.so
The auth-user-pass
file must be defined with the real username (first line)
and a dummy or decoy password (second line).
The OpenVPN client must be configured to use the hook dynamic library. This can
be done by setting the LD_PRELOAD
environment variable to the path where the
hook dynamic library was installed.
Additionally, the AUTH_FILE_PATH
variable must be set to the path of the
auth-user-pass
file that the hook should intercept. It has to be the same path
that the OpenVPN client is configured to use in the auth-user-pass
directive.
But note that the path has to be completely identical; the hook does not perform
any kind of path normalization or expansion, it just does a byte-by-byte
comparison of the path provided in the AUTH_FILE_PATH
environment variable
with the path provided to fopen
.
Let's assume we have an OpenVPN client configuration named office
, located at
/etc/openvpn/client/office.conf
.
The Systemd service unit for this connection is therefore named
[email protected]
.
The office.conf
file contains an auth-user-pass
directive that points to the
relative office/auth-user-pass.txt
file, which corresponds to
/etc/openvpn/client/office/auth-user-pass.txt
in absolute terms. That file
contains:
my_username
my_decoy_password_that_is_not_used_but_must_be_here
Then the Systemd Environment
directive can be used to set both the
LD_PRELOAD
and AUTH_FILE_PATH
environment variables for this connection
with:
# As root
# Configure the hook as a Systemd drop-in for the service unit
mkdir -p /etc/systemd/system/[email protected]
echo -e '[Service]\nEnvironment=LD_PRELOAD=/usr/local/lib/libopenvpn_auth_hook.so AUTH_FILE_PATH=office/auth-user-pass.txt' > /etc/systemd/system/[email protected]/openvpn-auth-hook.conf
# Apply the changes and restart the service
systemctl daemon-reload
systemctl restart [email protected]
# Check the logs and make sure there are no errors and authentication is successful
journalctl -u [email protected]