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 testAdditionally, 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.soWe 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.soThe 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
openvpn-client@office.service.
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/openvpn-client@office.service.d
echo -e '[Service]\nEnvironment=LD_PRELOAD=/usr/local/lib/libopenvpn_auth_hook.so AUTH_FILE_PATH=office/auth-user-pass.txt' > /etc/systemd/system/openvpn-client@office.service.d/openvpn-auth-hook.conf
# Apply the changes and restart the service
systemctl daemon-reload
systemctl restart openvpn-client@office.service
# Check the logs and make sure there are no errors and authentication is successful
journalctl -u openvpn-client@office.service