Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Simple web UI to manage OpenVPN users, their certificates & routes in Linux. Whi
* (optionally) Specifying/changing password for additional authorization in OpenVPN;
* (optionally) Specifying the Kubernetes LoadBalancer if it's used in front of the OpenVPN server (to get an automatically defined `remote` in the `client.conf.tpl` template).
* (optionally) Storing certificates and other files in Kubernetes Secrets (**Attention, this feature is experimental!**).
* (optionally) Enabling Google Auth 2FA for each user.

### Screenshots

Expand Down Expand Up @@ -63,7 +64,51 @@ cd ovpn-admin

(Please don't forget to configure all needed params in advance.)

### 3. Prebuilt binary
### 3. Building from source (for enabling google-auth 2FA only)

***Note: This configuration is for enabling 2FA with the admin portal and must be run on the host machine. It will not work in the Docker environment due to compatibility issues with the Google Auth 2FA setup in Docker.***

Requirements. You need Linux with the following components installed:
- [golang](https://golang.org/doc/install)
- [packr2](https://github.com/gobuffalo/packr#installation)
- [nodejs/npm](https://nodejs.org/en/download/package-manager/)

Commands to execute:

```bash
git clone https://github.com/palark/ovpn-admin.git
cd ovpn-admin
./bootstrap.sh
./build.sh
./ovpn-admin

./setup/configure.sh
```

To enable the necessary authentication features, follow these steps:

1. Add the following lines in `templates/client.conf.tpl`:
- auth-user-pass
- auth-nocache
- reneg-sec 0

2. Add the following line in `setup/openvpn.conf`:
- plugin /usr/lib/openvpn/openvpn-plugin-auth-pam.so openvpn

3. Set the following varibales with the speicified values in `main.go`
```
easyrsaDirPath = /etc/openvpn/easyrsa
indexTxtPath = /etc/openvpn/easyrsa/pki/index.txt
authDatabase = /etc/openvpn/easyrsa/pki/users.db
ccdDir = /etc/openvpn/ccd (if ccdEnabled set to true)
```

4. Set the following varibales with the speicified values in `setup/configure.sh`
```
OVPN_2FA=true
```

### 4. Prebuilt binary

You can also download and use prebuilt binaries from the [releases](https://github.com/palark/ovpn-admin/releases/latest) page — just choose a relevant tar.gz file.

Expand Down
26 changes: 25 additions & 1 deletion frontend/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,14 @@ new Vue({
showForServerRole: ['master', 'slave'],
showForModule: ["core"],
},
{
name: 'u-download-qrcode',
label: 'Download QR Code',
class: 'btn-info',
showWhenStatus: 'Active',
showForServerRole: ['master', 'slave'],
showForModule: ["google-auth-2fa"],
},
{
name: 'u-edit-ccd',
label: 'Edit routes',
Expand Down Expand Up @@ -271,7 +279,23 @@ new Vue({
link.click()
URL.revokeObjectURL(link.href)
}).catch(console.error);
})
})
_this.$root.$on('u-download-qrcode', function () {
const url = `/api/qr-code/${_this.username}`;

axios.get(url, { responseType: 'blob' })
.then(function (response) {
const blob = new Blob([response.data], { type: 'image/png' });

const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = _this.username + ".png";
link.click();

URL.revokeObjectURL(link.href);
})
.catch(console.error);
});
_this.$root.$on('u-edit-ccd', function () {
_this.u.modalShowCcdVisible = true;
var data = new URLSearchParams();
Expand Down
26 changes: 24 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ var (
logLevel = kingpin.Flag("log.level", "set log level: trace, debug, info, warn, error (default info)").Default("info").Envar("LOG_LEVEL").String()
logFormat = kingpin.Flag("log.format", "set log format: text, json (default text)").Default("text").Envar("LOG_FORMAT").String()
storageBackend = kingpin.Flag("storage.backend", "storage backend: filesystem, kubernetes.secrets (default filesystem)").Default("filesystem").Envar("STORAGE_BACKEND").String()
googleAuth2FAEnabled = kingpin.Flag("auth.mfa", "enable 2FA authentication").Default("false").Envar("OVPN_2FA").Bool()
googleAuthDir = kingpin.Flag("google-auth.path", "path to store qr-code and secret keys of users").Default("/etc/google-auth").Envar("GOOGLE_2FA_AUTH_DIR").String()

certsArchivePath = "/tmp/" + certsArchiveFileName
ccdArchivePath = "/tmp/" + ccdArchiveFileName
Expand Down Expand Up @@ -547,6 +549,10 @@ func main() {
ovpnAdmin.modules = append(ovpnAdmin.modules, "ccd")
}

if *googleAuth2FAEnabled {
ovpnAdmin.modules = append(ovpnAdmin.modules, "google-auth-2fa")
}

if ovpnAdmin.role == "slave" {
ovpnAdmin.syncDataFromMaster()
go ovpnAdmin.syncWithMaster()
Expand Down Expand Up @@ -576,6 +582,7 @@ func main() {
http.HandleFunc(*listenBaseUrl + "api/sync/last/successful", ovpnAdmin.lastSuccessfulSyncTimeHandler)
http.HandleFunc(*listenBaseUrl + downloadCertsApiUrl, ovpnAdmin.downloadCertsHandler)
http.HandleFunc(*listenBaseUrl + downloadCcdApiUrl, ovpnAdmin.downloadCcdHandler)
http.HandleFunc(*listenBaseUrl + "api/qr-code/", downloadHandler)

http.Handle(*metricsPath, promhttp.HandlerFor(ovpnAdmin.promRegistry, promhttp.HandlerOpts{}))
http.HandleFunc(*listenBaseUrl + "ping", func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -586,6 +593,17 @@ func main() {
log.Fatal(http.ListenAndServe(*listenHost+":"+*listenPort, nil))
}

func downloadHandler(w http.ResponseWriter, r *http.Request) {
username := strings.TrimPrefix(r.URL.Path, "/api/download/")
imagePath := fmt.Sprintf("%s/%s.png", *googleAuthDir, username)

if _, err := os.Stat(imagePath); os.IsNotExist(err) {
http.Error(w, "Image not found", http.StatusNotFound)
return
}
http.ServeFile(w, r, imagePath)
}

func CacheControlWrapper(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "max-age=2592000") // 30 days
Expand Down Expand Up @@ -985,18 +1003,22 @@ func (oAdmin *OvpnAdmin) userCreate(username, password string) (bool, string) {
log.Error(err)
}
} else {
o := runBash(fmt.Sprintf("cd %s && %s build-client-full %s nopass 1>/dev/null", *easyrsaDirPath, *easyrsaBinPath, username))
o := runBash(fmt.Sprintf("cd %s && echo 'yes' | sudo %s build-client-full %s nopass 1>/dev/null", *easyrsaDirPath, *easyrsaBinPath, username))
log.Debug(o)
}

if *authByPassword {
o := runBash(fmt.Sprintf("openvpn-user create --db.path %s --user %s --password %s", *authDatabase, username, password))
log.Debug(o)
}
if *googleAuth2FAEnabled {
mfa_auth := runBash(fmt.Sprintf("sudo /etc/openvpn/google-auth.sh %s", username))
log.Debug(mfa_auth)
}

log.Infof("Certificate for user %s issued", username)

//oAdmin.clients = oAdmin.usersList()
oAdmin.clients = oAdmin.usersList()

return true, ucErr
}
Expand Down
87 changes: 86 additions & 1 deletion setup/configure.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@ SERVER_CERT="${EASY_RSA_LOC}/pki/issued/server.crt"
OVPN_SRV_NET=${OVPN_SERVER_NET:-172.16.100.0}
OVPN_SRV_MASK=${OVPN_SERVER_MASK:-255.255.255.0}

OVPN_PASSWD_AUTH=${OVPN_PASSWD_AUTH:-false}
OVPN_2FA=false
GOOGLE_2FA_AUTH_DIR="/etc/google-auth"

if [ ${OVPN_2FA} = "true" ]; then
TARGETARCH=$(dpkg --print-architecture)

mkdir -p $EASY_RSA_LOC
sudo apt update -y

sudo apt install -y openvpn iptables

if [ ! -f "/usr/local/bin/easyrsa" ]; then
sudo apt install easy-rsa
sudo ln -sf /usr/share/easy-rsa/easyrsa /usr/local/bin/easyrsa
fi

if [ ! -f "/usr/local/bin/openvpn-user" ]; then
cd /tmp
wget "https://github.com/pashcovich/openvpn-user/releases/download/v1.0.4/openvpn-user-linux-${TARGETARCH}.tar.gz" -O - | sudo tar xz -C /usr/local/bin
fi

if [ -f "/usr/local/bin/openvpn-user-${TARGETARCH}" ]; then
sudo ln -sf /usr/local/bin/openvpn-user-${TARGETARCH} /usr/local/bin/openvpn-user
fi
fi

cd $EASY_RSA_LOC

Expand Down Expand Up @@ -39,7 +65,11 @@ if [ ! -c /dev/net/tun ]; then
mknod /dev/net/tun c 10 200
fi

cp -f /etc/openvpn/setup/openvpn.conf /etc/openvpn/openvpn.conf
if [ ${OVPN_2FA} = "true" ]; then
cp -f /home/ubuntu/ovpn-admin/setup/openvpn.conf /etc/openvpn/openvpn.conf
else
cp -f /etc/openvpn/setup/openvpn.conf /etc/openvpn/openvpn.conf
fi

if [ ${OVPN_PASSWD_AUTH} = "true" ]; then
mkdir -p /etc/openvpn/scripts/
Expand All @@ -54,6 +84,61 @@ fi
[ -d $EASY_RSA_LOC/pki ] && chmod 755 $EASY_RSA_LOC/pki
[ -f $EASY_RSA_LOC/pki/crl.pem ] && chmod 644 $EASY_RSA_LOC/pki/crl.pem

if [ ${OVPN_2FA} = "true" ]; then
if [ ! -f "/usr/local/lib/security/pam_google_authenticator.so" ]; then
apt update && apt install -y \
build-essential \
linux-headers-$(uname -r) \
autoconf \
automake \
libtool \
cmake \
make \
git \
libpam0g-dev \
libpam-google-authenticator \
qrencode

cd /tmp
rm -rf google-authenticator-libpam
git clone https://github.com/google/google-authenticator-libpam
cd google-authenticator-libpam/
./bootstrap.sh
./configure
make
make install
rm -rf google-authenticator-libpam
fi

if [ ! -f "/etc/pam.d/openvpn" ]; then
bash -c 'cat > /etc/pam.d/openvpn <<EOF
auth requisite /usr/local/lib/security/pam_google_authenticator.so secret=/etc/google-auth/\${USER} user=root
account required pam_permit.so'
fi

if [ ! -f "/etc/openvpn/google-auth.sh" ]; then
sudo mkdir -p /etc/google-auth
sudo chown -R root /etc/google-auth

sudo bash -c 'cat > /etc/openvpn/google-auth.sh <<EOF
#!/bin/bash

CLIENT=\$1
HOST=\$(hostname)
R="\e[0;91m"
G="\e[0;92m"
W="\e[0;97m"
B="\e[1m"
C="\e[0m"

google-authenticator -t -d -f -r 3 -R 30 -W -C -s "\${GOOGLE_2FA_AUTH_DIR}/\${CLIENT}" || { echo -e "\${R}\${B}error generating QR code\${C}"; exit 1; }
secret=\$(head -n 1 "\${GOOGLE_2FA_AUTH_DIR}/\${CLIENT}")
qrencode -t PNG -o "\${GOOGLE_2FA_AUTH_DIR}/\${CLIENT}.png" "otpauth://totp/\${CLIENT}@\${HOST}?secret=\${secret}&issuer=openvpn" || { echo -e "\${R}\${B}Error generating PNG\${C}"; exit 1; }'

sudo chmod +x $GOOGLE_2FA_AUTH_DIR/google-auth.sh
fi
fi

mkdir -p /etc/openvpn/ccd

openvpn --config /etc/openvpn/openvpn.conf --client-config-dir /etc/openvpn/ccd --port 1194 --proto tcp --management 127.0.0.1 8989 --dev tun0 --server ${OVPN_SRV_NET} ${OVPN_SRV_MASK}