mirror of
https://github.com/flant/ovpn-admin.git
synced 2026-02-04 01:10:22 -08:00
Merge 65e09ec42ffd8bef3de61ecca4a7b8fc6d156425 into 39f95e3d2c6e1f0e0fa425d9c45104d607c0e3d9
This commit is contained in:
commit
d4dc784891
47
README.md
47
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/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) 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) Storing certificates and other files in Kubernetes Secrets (**Attention, this feature is experimental!**).
|
||||||
|
* (optionally) Enabling Google Auth 2FA for each user.
|
||||||
|
|
||||||
### Screenshots
|
### Screenshots
|
||||||
|
|
||||||
@ -63,7 +64,51 @@ cd ovpn-admin
|
|||||||
|
|
||||||
(Please don't forget to configure all needed params in advance.)
|
(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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@ -163,6 +163,14 @@ new Vue({
|
|||||||
showForServerRole: ['master', 'slave'],
|
showForServerRole: ['master', 'slave'],
|
||||||
showForModule: ["core"],
|
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',
|
name: 'u-edit-ccd',
|
||||||
label: 'Edit routes',
|
label: 'Edit routes',
|
||||||
@ -272,6 +280,22 @@ new Vue({
|
|||||||
URL.revokeObjectURL(link.href)
|
URL.revokeObjectURL(link.href)
|
||||||
}).catch(console.error);
|
}).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.$root.$on('u-edit-ccd', function () {
|
||||||
_this.u.modalShowCcdVisible = true;
|
_this.u.modalShowCcdVisible = true;
|
||||||
var data = new URLSearchParams();
|
var data = new URLSearchParams();
|
||||||
|
|||||||
26
main.go
26
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()
|
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()
|
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()
|
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
|
certsArchivePath = "/tmp/" + certsArchiveFileName
|
||||||
ccdArchivePath = "/tmp/" + ccdArchiveFileName
|
ccdArchivePath = "/tmp/" + ccdArchiveFileName
|
||||||
@ -547,6 +549,10 @@ func main() {
|
|||||||
ovpnAdmin.modules = append(ovpnAdmin.modules, "ccd")
|
ovpnAdmin.modules = append(ovpnAdmin.modules, "ccd")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *googleAuth2FAEnabled {
|
||||||
|
ovpnAdmin.modules = append(ovpnAdmin.modules, "google-auth-2fa")
|
||||||
|
}
|
||||||
|
|
||||||
if ovpnAdmin.role == "slave" {
|
if ovpnAdmin.role == "slave" {
|
||||||
ovpnAdmin.syncDataFromMaster()
|
ovpnAdmin.syncDataFromMaster()
|
||||||
go ovpnAdmin.syncWithMaster()
|
go ovpnAdmin.syncWithMaster()
|
||||||
@ -576,6 +582,7 @@ func main() {
|
|||||||
http.HandleFunc(*listenBaseUrl + "api/sync/last/successful", ovpnAdmin.lastSuccessfulSyncTimeHandler)
|
http.HandleFunc(*listenBaseUrl + "api/sync/last/successful", ovpnAdmin.lastSuccessfulSyncTimeHandler)
|
||||||
http.HandleFunc(*listenBaseUrl + downloadCertsApiUrl, ovpnAdmin.downloadCertsHandler)
|
http.HandleFunc(*listenBaseUrl + downloadCertsApiUrl, ovpnAdmin.downloadCertsHandler)
|
||||||
http.HandleFunc(*listenBaseUrl + downloadCcdApiUrl, ovpnAdmin.downloadCcdHandler)
|
http.HandleFunc(*listenBaseUrl + downloadCcdApiUrl, ovpnAdmin.downloadCcdHandler)
|
||||||
|
http.HandleFunc(*listenBaseUrl + "api/qr-code/", downloadHandler)
|
||||||
|
|
||||||
http.Handle(*metricsPath, promhttp.HandlerFor(ovpnAdmin.promRegistry, promhttp.HandlerOpts{}))
|
http.Handle(*metricsPath, promhttp.HandlerFor(ovpnAdmin.promRegistry, promhttp.HandlerOpts{}))
|
||||||
http.HandleFunc(*listenBaseUrl + "ping", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc(*listenBaseUrl + "ping", func(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -586,6 +593,17 @@ func main() {
|
|||||||
log.Fatal(http.ListenAndServe(*listenHost+":"+*listenPort, nil))
|
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 {
|
func CacheControlWrapper(h http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Cache-Control", "max-age=2592000") // 30 days
|
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)
|
log.Error(err)
|
||||||
}
|
}
|
||||||
} else {
|
} 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)
|
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))
|
o := runBash(fmt.Sprintf("openvpn-user create --db.path %s --user %s --password %s", *authDatabase, username, password))
|
||||||
log.Debug(o)
|
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)
|
log.Infof("Certificate for user %s issued", username)
|
||||||
|
|
||||||
//oAdmin.clients = oAdmin.usersList()
|
oAdmin.clients = oAdmin.usersList()
|
||||||
|
|
||||||
return true, ucErr
|
return true, ucErr
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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_NET=${OVPN_SERVER_NET:-172.16.100.0}
|
||||||
OVPN_SRV_MASK=${OVPN_SERVER_MASK:-255.255.255.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
|
cd $EASY_RSA_LOC
|
||||||
|
|
||||||
@ -39,7 +65,11 @@ if [ ! -c /dev/net/tun ]; then
|
|||||||
mknod /dev/net/tun c 10 200
|
mknod /dev/net/tun c 10 200
|
||||||
fi
|
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
|
if [ ${OVPN_PASSWD_AUTH} = "true" ]; then
|
||||||
mkdir -p /etc/openvpn/scripts/
|
mkdir -p /etc/openvpn/scripts/
|
||||||
@ -54,6 +84,61 @@ fi
|
|||||||
[ -d $EASY_RSA_LOC/pki ] && chmod 755 $EASY_RSA_LOC/pki
|
[ -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
|
[ -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
|
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}
|
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}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user