diff --git a/README.md b/README.md index acc4987..0883a78 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. diff --git a/frontend/src/main.js b/frontend/src/main.js index 6f3088b..d91690d 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -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', @@ -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(); diff --git a/main.go b/main.go index 145a942..8d1bc5d 100644 --- a/main.go +++ b/main.go @@ -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 @@ -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() @@ -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) { @@ -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 @@ -985,7 +1003,7 @@ 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) } @@ -993,10 +1011,14 @@ func (oAdmin *OvpnAdmin) userCreate(username, password string) (bool, string) { 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 } diff --git a/setup/configure.sh b/setup/configure.sh index a9299a5..acb81bd 100644 --- a/setup/configure.sh +++ b/setup/configure.sh @@ -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 @@ -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/ @@ -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 < /etc/openvpn/google-auth.sh <