Add passwd auth feature; Fixes; Some layout changes

This commit is contained in:
Ilya Sosnovsky 2021-02-15 09:03:38 +03:00
parent 0af5fc3622
commit bec0e738d1
12 changed files with 326 additions and 88 deletions

View File

@ -16,3 +16,6 @@ frontend/node_modules
openvpn-web-ui openvpn-web-ui
openvpn-ui openvpn-ui
openvpn-admin openvpn-admin
docker-compose.yaml
docker-compose-slave.yaml

16
.werffiles/auth.sh Normal file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env sh
PATH=$PATH:/usr/local/bin
set -e
env
auth_usr=$(head -1 $1)
auth_passwd=$(tail -1 $1)
if [ $common_name = $username ]; then
openvpn-user auth --db.path /etc/openvpn/easyrsa/pki/users.db --user ${auth_usr} --password ${auth_passwd}
else
echo "Authorization failed"
exit 1
fi

View File

@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -x set -ex
EASY_RSA_LOC="/etc/openvpn/easyrsa" EASY_RSA_LOC="/etc/openvpn/easyrsa"
SERVER_CERT="${EASY_RSA_LOC}/pki/issued/server.crt" SERVER_CERT="${EASY_RSA_LOC}/pki/issued/server.crt"
@ -34,10 +34,20 @@ fi
cp -f /etc/openvpn/setup/openvpn.conf /etc/openvpn/openvpn.conf cp -f /etc/openvpn/setup/openvpn.conf /etc/openvpn/openvpn.conf
if [ ${OPVN_PASSWD_AUTH} = "true" ]; then
mkdir -p /etc/openvpn/scripts/
cp -f /etc/openvpn/setup/auth.sh /etc/openvpn/scripts/auth.sh
chmod +x /etc/openvpn/scripts/auth.sh
echo "auth-user-pass-verify /etc/openvpn/scripts/auth.sh via-file" | tee -a /etc/openvpn/openvpn.conf
echo "script-security 2" | tee -a /etc/openvpn/openvpn.conf
echo "verify-client-cert require" | tee -a /etc/openvpn/openvpn.conf
openvpn-user db-init --db.path=$EASY_RSA_LOC/pki/users.db
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
mkdir -p /etc/openvpn/ccd mkdir -p /etc/openvpn/ccd
openvpn --config /etc/openvpn/openvpn.conf --client-config-dir /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

View File

@ -8,15 +8,14 @@ dh /etc/openvpn/easyrsa/pki/dh.pem
crl-verify /etc/openvpn/easyrsa/pki/crl.pem crl-verify /etc/openvpn/easyrsa/pki/crl.pem
tls-auth /etc/openvpn/easyrsa/pki/ta.key tls-auth /etc/openvpn/easyrsa/pki/ta.key
key-direction 0 key-direction 0
duplicate-cn
cipher AES-128-CBC cipher AES-128-CBC
management 127.0.0.1 8989 #management 127.0.0.1 8989
keepalive 10 60 keepalive 10 60
persist-key persist-key
persist-tun persist-tun
topology subnet topology subnet
proto tcp #proto tcp
port 1194 #port 1194
dev tun0 dev tun0
status /tmp/openvpn-status.log status /tmp/openvpn-status.log
user nobody user nobody

View File

@ -7,13 +7,16 @@ FROM node:14.2-alpine3.11 AS frontend-builder
COPY frontend/ /app COPY frontend/ /app
RUN cd /app && npm install && npm run build RUN cd /app && npm install && npm run build
FROM alpine:3.11 FROM golang:1.14.2-buster AS user-builder
RUN git clone https://github.com/pashcovich/openvpn-user /app && cd /app && env CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags='-linkmode external -extldflags "-static" -s -w' -o openvpn-user
FROM alpine:3.13
WORKDIR /app WORKDIR /app
COPY --from=backend-builder /app/openvpn-admin /app COPY --from=backend-builder /app/openvpn-admin /app
COPY --from=user-builder /app/openvpn-user /usr/local/bin
COPY --from=frontend-builder /app/static /app/static COPY --from=frontend-builder /app/static /app/static
COPY client.conf.tpl /app/client.conf.tpl COPY client.conf.tpl /app/client.conf.tpl
COPY ccd.tpl /app/ccd.tpl COPY ccd.tpl /app/ccd.tpl
RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \ RUN apk add --update bash easy-rsa && \
apk add --update bash easy-rsa && \
ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \ ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \
rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/* rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*

View File

@ -1,6 +1,9 @@
FROM alpine:3.11 FROM golang:1.14.2-buster AS user-builder
RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \ RUN git clone https://github.com/pashcovich/openvpn-user /app && cd /app && env CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags='-linkmode external -extldflags "-static" -s -w' -o openvpn-user
apk add --update bash openvpn easy-rsa && \
FROM alpine:3.13
COPY --from=user-builder /app/openvpn-user /usr/local/bin
RUN apk add --update bash openvpn easy-rsa && \
ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \ ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \
rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/* rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*
COPY .werffiles /etc/openvpn/setup COPY .werffiles /etc/openvpn/setup

View File

@ -11,10 +11,19 @@ key-direction 1
#redirect-gateway def1 #redirect-gateway def1
tls-client tls-client
remote-cert-tls server remote-cert-tls server
# for update resolv.conf on ubuntu # uncomment needed below lines for use with linux
#script-security 2 system #script-security 2
# if use use resolved
#up /etc/openvpn/update-resolv-conf #up /etc/openvpn/update-resolv-conf
#down /etc/openvpn/update-resolv-conf #down /etc/openvpn/update-resolv-conf
# if you use systemd-resolved first install and openvpn-systemd-resolved package
#up /etc/openvpn/update-systemd-resolved
#down /etc/openvpn/update-systemd-resolved
{{- if .PasswdAuth }}
auth-user-pass
{{- end }}
<cert> <cert>
{{ .Cert -}} {{ .Cert -}}
</cert> </cert>

View File

@ -8,6 +8,7 @@ services:
image: openvpn:local image: openvpn:local
command: /etc/openvpn/setup/configure.sh command: /etc/openvpn/setup/configure.sh
environment: environment:
- OPVN_PASSWD_AUTH=true
- OPVN_ROLE=slave - OPVN_ROLE=slave
cap_add: cap_add:
- NET_ADMIN - NET_ADMIN
@ -21,7 +22,7 @@ services:
build: build:
context: . context: .
image: openvpn-admin:local image: openvpn-admin:local
command: /app/openvpn-admin --debug --ovpn.network="172.16.100.0/22" --master.sync-token="TOKEN" --master.host="http://172.20.0.1:8080" --role="slave" --ovpn.host="127.0.0.1:7744" --ovpn.host="127.0.0.1:7778" command: /app/openvpn-admin --debug --ovpn.network="172.16.100.0/22" --master.sync-token="TOKEN" --master.host="http://172.20.0.1:8080" --role="slave" --ovpn.host="127.0.0.1:7744" --ovpn.host="127.0.0.1:7778" --auth.password
environment: environment:
- OPVN_SLAVE=1 - OPVN_SLAVE=1
network_mode: service:openvpn network_mode: service:openvpn

View File

@ -7,6 +7,8 @@ services:
dockerfile: Dockerfile.openvpn dockerfile: Dockerfile.openvpn
image: openvpn:local image: openvpn:local
command: /etc/openvpn/setup/configure.sh command: /etc/openvpn/setup/configure.sh
environment:
- OPVN_PASSWD_AUTH=true
cap_add: cap_add:
- NET_ADMIN - NET_ADMIN
ports: ports:
@ -19,7 +21,7 @@ services:
build: build:
context: . context: .
image: openvpn-admin:local image: openvpn-admin:local
command: /app/openvpn-admin --debug --ovpn.network="172.16.100.0/22" --master.sync-token="TOKEN" command: /app/openvpn-admin --debug --ovpn.network="172.16.100.0/22" --master.sync-token="TOKEN" --auth.password
network_mode: service:openvpn network_mode: service:openvpn
volumes: volumes:
- ./easyrsa_master:/mnt/easyrsa - ./easyrsa_master:/mnt/easyrsa

View File

@ -54,6 +54,11 @@ new Vue({
field: 'AccountStatus', field: 'AccountStatus',
filterable: true, filterable: true,
}, },
{
label: 'Connection Server',
field: 'ConnectionServer',
filterable: true,
},
{ {
label: 'Expiration Date', label: 'Expiration Date',
field: 'ExpirationDate', field: 'ExpirationDate',
@ -84,39 +89,52 @@ new Vue({
], ],
rows: [], rows: [],
actions: [ actions: [
{
name: 'u-change-password',
label: 'Change password',
class: 'btn-warning',
showWhenStatus: 'Active',
showForServerRole: ['master']
},
{ {
name: 'u-revoke', name: 'u-revoke',
label: 'Revoke', label: 'Revoke',
class: 'btn-warning',
showWhenStatus: 'Active', showWhenStatus: 'Active',
showForServerRole: ['master'] showForServerRole: ['master']
}, },
{ {
name: 'u-unrevoke', name: 'u-unrevoke',
label: 'Unrevoke', label: 'Unrevoke',
class: 'btn-primary',
showWhenStatus: 'Revoked', showWhenStatus: 'Revoked',
showForServerRole: ['master'] showForServerRole: ['master']
}, },
{ // {
name: 'u-show-config', // name: 'u-show-config',
label: 'Show config', // label: 'Show config',
showWhenStatus: 'Active', // class: 'btn-primary',
showForServerRole: ['master', 'slave'] // showWhenStatus: 'Active',
}, // showForServerRole: ['master', 'slave']
// },
{ {
name: 'u-download-config', name: 'u-download-config',
label: 'Download config', label: 'Download config',
class: 'btn-info',
showWhenStatus: 'Active', showWhenStatus: 'Active',
showForServerRole: ['master', 'slave'] showForServerRole: ['master', 'slave']
}, },
{ {
name: 'u-edit-ccd', name: 'u-edit-ccd',
label: 'Edit routes', label: 'Edit routes',
class: 'btn-primary',
showWhenStatus: 'Active', showWhenStatus: 'Active',
showForServerRole: ['master'] showForServerRole: ['master']
}, },
{ {
name: 'u-edit-ccd', name: 'u-edit-ccd',
label: 'Show routes', label: 'Show routes',
class: 'btn-primary',
showWhenStatus: 'Active', showWhenStatus: 'Active',
showForServerRole: ['slave'] showForServerRole: ['slave']
} }
@ -128,11 +146,15 @@ new Vue({
lastSync: "unknown", lastSync: "unknown",
u: { u: {
newUserName: '', newUserName: '',
// newUserPassword: 'nopass', newUserPassword: '',
newUserCreateError: '', newUserCreateError: '',
newPassword: '',
passwordChangeStatus: '',
passwordChangeMessage: '',
modalNewUserVisible: false, modalNewUserVisible: false,
modalShowConfigVisible: false, modalShowConfigVisible: false,
modalShowCcdVisible: false, modalShowCcdVisible: false,
modalChangePasswordVisible: false,
openvpnConfig: '', openvpnConfig: '',
ccd: { ccd: {
Name: '', Name: '',
@ -210,6 +232,11 @@ new Vue({
console.log(response.data); console.log(response.data);
}); });
}) })
_this.$root.$on('u-change-password', function () {
_this.u.modalChangePasswordVisible = true;
var data = new URLSearchParams();
data.append('username', _this.username);
})
}, },
computed: { computed: {
customAddressDisabled: function () { customAddressDisabled: function () {
@ -218,6 +245,9 @@ new Vue({
ccdApplyStatusCssClass: function () { ccdApplyStatusCssClass: function () {
return this.u.ccdApplyStatus == 200 ? "alert-success" : "alert-danger" return this.u.ccdApplyStatus == 200 ? "alert-success" : "alert-danger"
}, },
passwordChangeStatusCssClass: function () {
return this.u.passwordChangeStatus == 200 ? "alert-success" : "alert-danger"
},
modalNewUserDisplay: function () { modalNewUserDisplay: function () {
return this.u.modalNewUserVisible ? {display: 'flex'} : {} return this.u.modalNewUserVisible ? {display: 'flex'} : {}
}, },
@ -227,6 +257,9 @@ new Vue({
modalShowCcdDisplay: function () { modalShowCcdDisplay: function () {
return this.u.modalShowCcdVisible ? {display: 'flex'} : {} return this.u.modalShowCcdVisible ? {display: 'flex'} : {}
}, },
modalChangePasswordDisplay: function () {
return this.u.modalChangePasswordVisible ? {display: 'flex'} : {}
},
revokeFilterText: function() { revokeFilterText: function() {
return this.filters.hideRevoked ? "Show revoked" : "Hide revoked" return this.filters.hideRevoked ? "Show revoked" : "Hide revoked"
}, },
@ -269,6 +302,7 @@ new Vue({
} }
}); });
}, },
createUser: function() { createUser: function() {
var _this = this; var _this = this;
@ -276,19 +310,20 @@ new Vue({
var data = new URLSearchParams(); var data = new URLSearchParams();
data.append('username', _this.u.newUserName); data.append('username', _this.u.newUserName);
// data.append('password', this.u.newUserPassword); data.append('password', _this.u.newUserPassword);
axios.request(axios_cfg('api/user/create', data, 'form')) axios.request(axios_cfg('api/user/create', data, 'form'))
.then(function(response) { .then(function(response) {
_this.getUserData();
_this.u.modalNewUserVisible = false; _this.u.modalNewUserVisible = false;
_this.u.newUserName = ''; _this.u.newUserName = '';
// _this.u.newUserPassword = 'nopass'; _this.u.newUserPassword = '';
_this.getUserData();
}) })
.catch(function(error) { .catch(function(error) {
_this.u.newUserCreateError = error.response.data; _this.u.newUserCreateError = error.response.data;
}); });
}, },
ccdApply: function() { ccdApply: function() {
var _this = this; var _this = this;
@ -304,6 +339,30 @@ new Vue({
_this.u.ccdApplyStatus = error.response.status; _this.u.ccdApplyStatus = error.response.status;
_this.u.ccdApplyStatusMessage = error.response.data; _this.u.ccdApplyStatusMessage = error.response.data;
}); });
} },
}
changeUserPassword: function(user) {
var _this = this;
_this.u.passwordChangeMessage = "";
var data = new URLSearchParams();
data.append('username', user);
data.append('password', _this.u.newPassword);
axios.request(axios_cfg('api/user/change-password', data, 'form'))
.then(function(response) {
_this.u.passwordChangeStatus = 200;
_this.u.newPassword = '';
_this.u.passwordChangeMessage = response.data.message;
_this.getUserData();
_this.u.modalChangePasswordVisible = false;
})
.catch(function(error) {
_this.u.passwordChangeStatus = error.response.status;
_this.u.passwordChangeMessage = error.response.data.message;
});
},
}
}) })

View File

@ -28,7 +28,7 @@
<template slot="table-row" slot-scope="props"> <template slot="table-row" slot-scope="props">
<span v-if="props.column.field == 'actions'"> <span v-if="props.column.field == 'actions'">
<button <button
class="btn btn-sm btn-success el-square modal-el-margin" class="btn btn-sm el-square modal-el-margin"
type="button" type="button"
:title="action.label" :title="action.label"
:data-username="props.row.Identity" :data-username="props.row.Identity"
@ -36,6 +36,7 @@
:data-text="action.label" :data-text="action.label"
@click.left.stop="rowActionFn" @click.left.stop="rowActionFn"
v-for="action in actions" v-for="action in actions"
v-bind:class="action.class"
v-if="action.showWhenStatus == props.row.AccountStatus && action.showForServerRole.includes(serverRole)"> v-if="action.showWhenStatus == props.row.AccountStatus && action.showForServerRole.includes(serverRole)">
{{ action.label }} {{ action.label }}
</button> </button>
@ -55,7 +56,7 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<input type="text" class="form-control el-square modal-el-margin" placeholder="Username [_a-zA-Z0-9\.-]" v-model="u.newUserName"> <input type="text" class="form-control el-square modal-el-margin" placeholder="Username [_a-zA-Z0-9\.-]" v-model="u.newUserName">
<!-- <input type="password" class="form-control el-square modal-el-margin" minlength="6" autocomplete="off" placeholder="Password [_a-zA-Z0-9\.-]" v-model="u.newUserPassword">--> <input type="password" class="form-control el-square modal-el-margin" minlength="6" autocomplete="off" placeholder="Password [_a-zA-Z0-9\.-]" v-model="u.newUserPassword">
</div> </div>
<div class="modal-footer justify-content-center" v-if="u.newUserCreateError.length > 0"> <div class="modal-footer justify-content-center" v-if="u.newUserCreateError.length > 0">
@ -64,18 +65,41 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="createUser();">Create</button> <button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="createUser()">Create</button>
<button type="button" class="btn btn-primary el-square d-flex justify-content-sm-end modal-el-margin" v-on:click.stop="u.newUserName='';u.newUserPassword='nopass';u.modalNewUserVisible=false">Close</button> <button type="button" class="btn btn-primary el-square d-flex justify-content-sm-end modal-el-margin" v-on:click.stop="u.newUserName='';u.newUserPassword='nopass';u.modalNewUserVisible=false">Close</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="modal-wrapper" v-if="u.modalChangePasswordVisible" v-bind:style="modalChangePasswordDisplay">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4>Change password for: <strong>{{ username }}</strong></h4>
</div>
<div class="modal-body">
<input type="password" class="form-control el-square modal-el-margin" minlength="6" autocomplete="off" placeholder="Password [_a-zA-Z0-9\.-]" v-model="u.newPassword">
</div>
<div class="modal-footer justify-content-center" v-if="u.passwordChangeMessage.length > 0">
<div class="alert" v-bind:class="passwordChangeStatusCssClass" role="alert" >
{{ u.passwordChangeMessage }}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="changeUserPassword(username)">Change password</button>
<button type="button" class="btn btn-primary el-square d-flex justify-content-sm-end modal-el-margin" v-on:click.stop="u.newPassword='';u.modalChangePasswordVisible=false">Close</button>
</div>
</div>
</div>
</div>
<div class="modal-wrapper" v-if="u.modalShowConfigVisible" v-bind:style="modalShowConfigDisplay"> <div class="modal-wrapper" v-if="u.modalShowConfigVisible" v-bind:style="modalShowConfigDisplay">
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h4>ovpn config for {{ username }}</h4> <h4>ovpn config for: <strong>{{ username }}</strong></h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="d-flex"> <div class="d-flex">
@ -96,16 +120,18 @@
<div class="modal-dialog modal-lg modal-dialog-scrollable"> <div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h3 class="static-address-label ">Routes table for: <strong>{{ username }}</strong></h3>
</div>
<div class="modal-body">
<div class="input-group"> <div class="input-group">
<h4 class="static-address-label ">Client "{{ username }}" static address</h4> <h5 class="static-address-label ">Static address:</h5>
<div class="input-group-prepend"> <div class="input-group-prepend">
<div class="input-group-text"> <div class="input-group-text">
<input id="enable-static" type="checkbox" onchange="document.getElementById('staticAddress').disabled=!this.checked;" v-if="serverRole == 'master'" v-bind:checked="customAddressDisabled"> <input id="enable-static" type="checkbox" onchange="document.getElementById('staticAddress').disabled=!this.checked;" v-if="serverRole == 'master'" v-bind:checked="!customAddressDisabled">
</div> </div>
</div> </div>
<input id="staticAddress" type="text" class="form-control" v-model="u.ccd.ClientAddress" placeholder="127.0.0.1" v-bind:disabled="customAddressDisabled"> <input id="staticAddress" type="text" class="form-control" v-model="u.ccd.ClientAddress" placeholder="127.0.0.1" v-bind:disabled="customAddressDisabled">
</div> </div>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="d-flex "> <div class="d-flex ">

183
main.go
View File

@ -20,7 +20,9 @@ import (
) )
const ( const (
usernameRegexp = `^([a-zA-Z0-9_.-])+$` usernameRegexp = `^([a-zA-Z0-9_.-@])+$`
passwordRegexp = `^([a-zA-Z0-9_.-@])+$`
passwordMinLength = 6
downloadCertsApiUrl = "/api/data/certs/download" downloadCertsApiUrl = "/api/data/certs/download"
downloadCcdApiUrl = "/api/data/ccd/download" downloadCcdApiUrl = "/api/data/ccd/download"
certsArchiveFileName = "certs.tar.gz" certsArchiveFileName = "certs.tar.gz"
@ -42,14 +44,17 @@ var (
masterSyncToken = kingpin.Flag("master.sync-token", "master host data sync security token").Default("justasimpleword").PlaceHolder("TOKEN").String() masterSyncToken = kingpin.Flag("master.sync-token", "master host data sync security token").Default("justasimpleword").PlaceHolder("TOKEN").String()
openvpnServer = kingpin.Flag("ovpn.host","host(s) for openvpn server").Default("127.0.0.1:7777").PlaceHolder("HOST:PORT").Strings() openvpnServer = kingpin.Flag("ovpn.host","host(s) for openvpn server").Default("127.0.0.1:7777").PlaceHolder("HOST:PORT").Strings()
openvpnNetwork = kingpin.Flag("ovpn.network","network for openvpn server").Default("172.16.100.0/24").String() openvpnNetwork = kingpin.Flag("ovpn.network","network for openvpn server").Default("172.16.100.0/24").String()
mgmtListenHost = kingpin.Flag("mgmt.host","host for openvpn server mgmt interface").Default("127.0.0.1").String() mgmtAddress = kingpin.Flag("mgmt","comma separated (alias=addresses) for openvpn servers mgmt interfaces").Default("main=127.0.0.1:8989").Strings()
mgmtListenPort = kingpin.Flag("mgmt.port","port for openvpn server mgmt interface").Default("8989").String() //mgmtListenPort = kingpin.Flag("mgmt.port","port for openvpn server mgmt interface").Default("8989").String()
metricsPath = kingpin.Flag("metrics.path", "URL path for surfacing collected metrics").Default("/metrics").String() metricsPath = kingpin.Flag("metrics.path", "URL path for surfacing collected metrics").Default("/metrics").String()
easyrsaDirPath = kingpin.Flag("easyrsa.path", "path to easyrsa dir").Default("/mnt/easyrsa").String() easyrsaDirPath = kingpin.Flag("easyrsa.path", "path to easyrsa dir").Default("/mnt/easyrsa").String()
indexTxtPath = kingpin.Flag("easyrsa.index-path", "path to easyrsa index file.").Default("/mnt/easyrsa/pki/index.txt").String() indexTxtPath = kingpin.Flag("easyrsa.index-path", "path to easyrsa index file.").Default("/mnt/easyrsa/pki/index.txt").String()
ccdDir = kingpin.Flag("ccd.path", "path to client-config-dir").Default("/mnt/ccd").String() ccdDir = kingpin.Flag("ccd.path", "path to client-config-dir").Default("/mnt/ccd").String()
authByPassword = kingpin.Flag("auth.password", "Enable additional password authorization.").Default("false").Bool()
authDatabase = kingpin.Flag("auth.db", "Database path fort password authorization.").Default("/mnt/easyrsa/pki/users.db").String()
staticPath = kingpin.Flag("static.path", "path to static dir").Default("./static").String() staticPath = kingpin.Flag("static.path", "path to static dir").Default("./static").String()
debug = kingpin.Flag("debug", "Enable debug mode.").Default("false").Bool() debug = kingpin.Flag("debug", "Enable debug mode.").Default("false").Bool()
verbose = kingpin.Flag("verbose", "Enable verbose mode.").Default("false").Bool()
certsArchivePath = "/tmp/" + certsArchiveFileName certsArchivePath = "/tmp/" + certsArchiveFileName
ccdArchivePath = "/tmp/" + ccdArchiveFileName ccdArchivePath = "/tmp/" + ccdArchiveFileName
@ -103,7 +108,7 @@ var (
ovpnClientConnectionInfo = prometheus.NewGaugeVec(prometheus.GaugeOpts{ ovpnClientConnectionInfo = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "ovpn_client_connection_info", Name: "ovpn_client_connection_info",
Help: "openvpn user connection info. ip - assigned address from opvn network. value - last time when connection was refreshed in unix format", Help: "openvpn user connection info. ip - assigned address from ovpn network. value - last time when connection was refreshed in unix format",
}, },
[]string{"client", "ip"}, []string{"client", "ip"},
) )
@ -140,8 +145,10 @@ type OpenvpnAdmin struct {
clients []OpenvpnClient clients []OpenvpnClient
activeClients []clientStatus activeClients []clientStatus
promRegistry *prometheus.Registry promRegistry *prometheus.Registry
mgmtInterfaces map[string]string
} }
type OpenvpnServer struct { type OpenvpnServer struct {
Host string Host string
Port string Port string
@ -153,6 +160,7 @@ type openvpnClientConfig struct {
Cert string Cert string
Key string Key string
TLS string TLS string
PasswdAuth bool
} }
type OpenvpnClient struct { type OpenvpnClient struct {
@ -161,6 +169,7 @@ type OpenvpnClient struct {
ExpirationDate string `json:"ExpirationDate"` ExpirationDate string `json:"ExpirationDate"`
RevocationDate string `json:"RevocationDate"` RevocationDate string `json:"RevocationDate"`
ConnectionStatus string `json:"ConnectionStatus"` ConnectionStatus string `json:"ConnectionStatus"`
ConnectionServer string `json:"ConnectionServer"`
} }
type ccdRoute struct { type ccdRoute struct {
@ -195,6 +204,7 @@ type clientStatus struct {
LastRef string LastRef string
ConnectedSinceFormatted string ConnectedSinceFormatted string
LastRefFormatted string LastRefFormatted string
ConnectedTo string
} }
func (oAdmin *OpenvpnAdmin) userListHandler(w http.ResponseWriter, r *http.Request) { func (oAdmin *OpenvpnAdmin) userListHandler(w http.ResponseWriter, r *http.Request) {
@ -214,7 +224,7 @@ func (oAdmin *OpenvpnAdmin) userCreateHandler(w http.ResponseWriter, r *http.Req
return return
} }
r.ParseForm() r.ParseForm()
userCreated, userCreateStatus := oAdmin.userCreate(r.FormValue("username")) userCreated, userCreateStatus := oAdmin.userCreate(r.FormValue("username"), r.FormValue("password"))
if userCreated { if userCreated {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
@ -244,6 +254,25 @@ func (oAdmin *OpenvpnAdmin) userUnrevokeHandler(w http.ResponseWriter, r *http.R
fmt.Fprintf(w, "%s", oAdmin.userUnrevoke(r.FormValue("username"))) fmt.Fprintf(w, "%s", oAdmin.userUnrevoke(r.FormValue("username")))
} }
func (oAdmin *OpenvpnAdmin) userChangePasswordHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
if *authByPassword {
passwordChanged, passwordChangeMessage := oAdmin.userChangePassword(r.FormValue("username"), r.FormValue("password"))
if passwordChanged {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{"status":"ok", "message": "%s"}`, passwordChangeMessage)
return
} else {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, `{"status":"error", "message": "%s"}`, passwordChangeMessage)
return
}
} else {
http.Error(w, `{"status":"error"}`, http.StatusNotImplemented )
}
}
func (oAdmin *OpenvpnAdmin) userShowConfigHandler(w http.ResponseWriter, r *http.Request) { func (oAdmin *OpenvpnAdmin) userShowConfigHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm() r.ParseForm()
fmt.Fprintf(w, "%s", oAdmin.renderClientConfig(r.FormValue("username"))) fmt.Fprintf(w, "%s", oAdmin.renderClientConfig(r.FormValue("username")))
@ -318,7 +347,7 @@ func (oAdmin *OpenvpnAdmin) downloadCertsHandler(w http.ResponseWriter, r *http.
http.ServeFile(w,r, certsArchivePath) http.ServeFile(w,r, certsArchivePath)
} }
func (oAdmin *OpenvpnAdmin) downloadCddHandler(w http.ResponseWriter, r *http.Request) { func (oAdmin *OpenvpnAdmin) downloadCcdHandler(w http.ResponseWriter, r *http.Request) {
if oAdmin.role == "slave" { if oAdmin.role == "slave" {
http.Error(w, `{"status":"error"}`, http.StatusLocked) http.Error(w, `{"status":"error"}`, http.StatusLocked)
return return
@ -346,6 +375,13 @@ func main() {
ovpnAdmin.masterSyncToken = *masterSyncToken ovpnAdmin.masterSyncToken = *masterSyncToken
ovpnAdmin.promRegistry = prometheus.NewRegistry() ovpnAdmin.promRegistry = prometheus.NewRegistry()
ovpnAdmin.mgmtInterfaces = make(map[string]string)
for _, mgmtInterface := range *mgmtAddress {
parts := strings.SplitN(mgmtInterface, "=",2)
ovpnAdmin.mgmtInterfaces[parts[0]] = parts[len(parts)-1]
}
ovpnAdmin.registerMetrics() ovpnAdmin.registerMetrics()
ovpnAdmin.setState() ovpnAdmin.setState()
@ -368,6 +404,7 @@ func main() {
http.HandleFunc("/api/server/role", ovpnAdmin.serverRoleHandler) http.HandleFunc("/api/server/role", ovpnAdmin.serverRoleHandler)
http.HandleFunc("/api/users/list", ovpnAdmin.userListHandler) http.HandleFunc("/api/users/list", ovpnAdmin.userListHandler)
http.HandleFunc("/api/user/create", ovpnAdmin.userCreateHandler) http.HandleFunc("/api/user/create", ovpnAdmin.userCreateHandler)
http.HandleFunc("/api/user/change-password", ovpnAdmin.userChangePasswordHandler)
http.HandleFunc("/api/user/revoke", ovpnAdmin.userRevokeHandler) http.HandleFunc("/api/user/revoke", ovpnAdmin.userRevokeHandler)
http.HandleFunc("/api/user/unrevoke", ovpnAdmin.userUnrevokeHandler) http.HandleFunc("/api/user/unrevoke", ovpnAdmin.userUnrevokeHandler)
http.HandleFunc("/api/user/config/show", ovpnAdmin.userShowConfigHandler) http.HandleFunc("/api/user/config/show", ovpnAdmin.userShowConfigHandler)
@ -379,7 +416,7 @@ func main() {
http.HandleFunc("/api/sync/last/try", ovpnAdmin.lastSyncTimeHandler) http.HandleFunc("/api/sync/last/try", ovpnAdmin.lastSyncTimeHandler)
http.HandleFunc("/api/sync/last/successful", ovpnAdmin.lastSuccessfulSyncTimeHandler) http.HandleFunc("/api/sync/last/successful", ovpnAdmin.lastSuccessfulSyncTimeHandler)
http.HandleFunc(downloadCertsApiUrl, ovpnAdmin.downloadCertsHandler) http.HandleFunc(downloadCertsApiUrl, ovpnAdmin.downloadCertsHandler)
http.HandleFunc(downloadCcdApiUrl, ovpnAdmin.downloadCddHandler) http.HandleFunc(downloadCcdApiUrl, ovpnAdmin.downloadCcdHandler)
http.Handle(*metricsPath, promhttp.HandlerFor(ovpnAdmin.promRegistry, promhttp.HandlerOpts{})) http.Handle(*metricsPath, promhttp.HandlerFor(ovpnAdmin.promRegistry, promhttp.HandlerOpts{}))
http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
@ -415,7 +452,7 @@ func (oAdmin *OpenvpnAdmin) setState() {
oAdmin.activeClients = oAdmin.mgmtGetActiveClients() oAdmin.activeClients = oAdmin.mgmtGetActiveClients()
oAdmin.clients = oAdmin.usersList() oAdmin.clients = oAdmin.usersList()
ovpnServerCaCertExpire.Set(float64((getOpvnCaCertExpireDate().Unix() - time.Now().Unix()) / 3600 / 24)) ovpnServerCaCertExpire.Set(float64((getOvpnCaCertExpireDate().Unix() - time.Now().Unix()) / 3600 / 24))
} }
func (oAdmin *OpenvpnAdmin) updateState() { func (oAdmin *OpenvpnAdmin) updateState() {
@ -479,6 +516,7 @@ func (oAdmin *OpenvpnAdmin) renderClientConfig(username string) string {
conf.Cert = fRead(*easyrsaDirPath + "/pki/issued/" + username + ".crt") conf.Cert = fRead(*easyrsaDirPath + "/pki/issued/" + username + ".crt")
conf.Key = fRead(*easyrsaDirPath + "/pki/private/" + username + ".key") conf.Key = fRead(*easyrsaDirPath + "/pki/private/" + username + ".key")
conf.TLS = fRead(*easyrsaDirPath + "/pki/ta.key") conf.TLS = fRead(*easyrsaDirPath + "/pki/ta.key")
conf.PasswdAuth = *authByPassword
t, _ := template.ParseFiles("client.conf.tpl") t, _ := template.ParseFiles("client.conf.tpl")
var tmp bytes.Buffer var tmp bytes.Buffer
@ -624,6 +662,14 @@ func validateUsername(username string) bool {
return validUsername.MatchString(username) return validUsername.MatchString(username)
} }
func validatePassword(password string) bool {
if len(password) < passwordMinLength {
return false
} else {
return true
}
}
func checkUserExist(username string) bool { func checkUserExist(username string) bool {
for _, u := range indexTxtParser(fRead(*indexTxtPath)) { for _, u := range indexTxtParser(fRead(*indexTxtPath)) {
if u.DistinguishedName == ("/CN=" + username) { if u.DistinguishedName == ("/CN=" + username) {
@ -663,8 +709,12 @@ func (oAdmin *OpenvpnAdmin) usersList() []OpenvpnClient {
expiredCerts += 1 expiredCerts += 1
} }
if isUserConnected(line.Identity, oAdmin.activeClients) { ovpnClient.ConnectionServer = ""
userConnected, userConnectedTo := isUserConnected(line.Identity, oAdmin.activeClients)
if userConnected {
ovpnClient.ConnectionStatus = "Connected" ovpnClient.ConnectionStatus = "Connected"
ovpnClient.ConnectionServer = userConnectedTo
connectedUsers += 1 connectedUsers += 1
} }
@ -689,16 +739,9 @@ func (oAdmin *OpenvpnAdmin) usersList() []OpenvpnClient {
return users return users
} }
func (oAdmin *OpenvpnAdmin) userCreate(username string) (bool, string) { func (oAdmin *OpenvpnAdmin) userCreate(username, password string) (bool, string) {
ucErr := fmt.Sprintf("User \"%s\" created", username) ucErr := fmt.Sprintf("User \"%s\" created", username)
// TODO: add password for user cert . priority=low
if validateUsername(username) == false {
ucErr = fmt.Sprintf("Username \"%s\" incorrect, you can use only %s\n", username, usernameRegexp)
if *debug {
log.Printf("ERROR: userCreate: %s", ucErr)
}
return false, ucErr
}
if checkUserExist(username) { if checkUserExist(username) {
ucErr = fmt.Sprintf("User \"%s\" already exists\n", username) ucErr = fmt.Sprintf("User \"%s\" already exists\n", username)
if *debug { if *debug {
@ -706,15 +749,72 @@ func (oAdmin *OpenvpnAdmin) userCreate(username string) (bool, string) {
} }
return false, ucErr return false, ucErr
} }
if ! validateUsername(username) {
ucErr = fmt.Sprintf("Username \"%s\" incorrect, you can use only %s\n", username, usernameRegexp)
if *debug {
log.Printf("ERROR: userCreate: %s", ucErr)
}
return false, ucErr
}
if ! validatePassword(password) {
ucErr = fmt.Sprintf("Password too short, password length must be greater or equal %d", passwordMinLength)
if *debug {
log.Printf("ERROR: userCreate: %s\n", ucErr)
}
return false, ucErr
}
o := runBash(fmt.Sprintf("date +%%Y-%%m-%%d\\ %%H:%%M:%%S && cd %s && easyrsa build-client-full %s nopass", *easyrsaDirPath, username)) o := runBash(fmt.Sprintf("date +%%Y-%%m-%%d\\ %%H:%%M:%%S && cd %s && easyrsa build-client-full %s nopass", *easyrsaDirPath, username))
fmt.Println(o) fmt.Println(o)
if *debug {
if *authByPassword {
o = runBash(fmt.Sprintf("openvpn-user create --db.path %s --user %s --password %s", *authDatabase, username, password))
fmt.Println(o)
}
if *verbose {
log.Printf("INFO: user created: %s", username) log.Printf("INFO: user created: %s", username)
} }
oAdmin.clients = oAdmin.usersList() oAdmin.clients = oAdmin.usersList()
return true, ucErr return true, ucErr
} }
func (oAdmin *OpenvpnAdmin) userChangePassword(username, password string) (bool, string) {
if checkUserExist(username) {
o := runBash(fmt.Sprintf("openvpn-user check --db.path %s --user %s | grep %s | wc -l", *authDatabase, username, username))
fmt.Println(o)
if ! validatePassword(password) {
ucpErr := fmt.Sprintf("Password for too short, password length must be greater or equal %d", passwordMinLength)
if *debug {
log.Printf("ERROR: userChangePassword: %s\n", ucpErr)
}
return false, ucpErr
}
if strings.TrimSpace(o) == "0" {
fmt.Println("Creating user in users.db")
o = runBash(fmt.Sprintf("openvpn-user create --db.path %s --user %s --password %s", *authDatabase, username, password))
fmt.Println(o)
}
o = runBash(fmt.Sprintf("openvpn-user change-password --db.path %s --user %s --password %s", *authDatabase, username, password))
fmt.Println(o)
if *verbose {
log.Printf("INFO: password for user %s was changed", username)
}
return true, "Password changed"
}
return false, "User does not exist"
}
func (oAdmin *OpenvpnAdmin) getUserStatistic(username string) clientStatus { func (oAdmin *OpenvpnAdmin) getUserStatistic(username string) clientStatus {
for _, u := range oAdmin.activeClients { for _, u := range oAdmin.activeClients {
if u.CommonName == username { if u.CommonName == username {
@ -728,6 +828,10 @@ func (oAdmin *OpenvpnAdmin) userRevoke(username string) string {
if checkUserExist(username) { if checkUserExist(username) {
// check certificate valid flag 'V' // check certificate valid flag 'V'
o := runBash(fmt.Sprintf("date +%%Y-%%m-%%d\\ %%H:%%M:%%S && cd %s && echo yes | easyrsa revoke %s && easyrsa gen-crl", *easyrsaDirPath, username)) o := runBash(fmt.Sprintf("date +%%Y-%%m-%%d\\ %%H:%%M:%%S && cd %s && echo yes | easyrsa revoke %s && easyrsa gen-crl", *easyrsaDirPath, username))
if *authByPassword {
o = runBash(fmt.Sprintf("openvpn-user revoke --db-path %s --user %s", *authDatabase, username))
//fmt.Println(o)
}
crlFix() crlFix()
oAdmin.clients = oAdmin.usersList() oAdmin.clients = oAdmin.usersList()
return fmt.Sprintln(o) return fmt.Sprintln(o)
@ -757,6 +861,10 @@ func (oAdmin *OpenvpnAdmin) userUnrevoke(username string) string {
//fmt.Print(renderIndexTxt(usersFromIndexTxt)) //fmt.Print(renderIndexTxt(usersFromIndexTxt))
o = runBash(fmt.Sprintf("cd %s && easyrsa gen-crl", *easyrsaDirPath)) o = runBash(fmt.Sprintf("cd %s && easyrsa gen-crl", *easyrsaDirPath))
//fmt.Println(o) //fmt.Println(o)
if *authByPassword {
o = runBash(fmt.Sprintf("openvpn-user restore --db-path %s --user %s", *authDatabase, username))
//fmt.Println(o)
}
crlFix() crlFix()
o = "" o = ""
fmt.Println(o) fmt.Println(o)
@ -773,11 +881,6 @@ func (oAdmin *OpenvpnAdmin) userUnrevoke(username string) string {
return fmt.Sprintf("{\"msg\":\"User \"%s\" not found\"}", username) return fmt.Sprintf("{\"msg\":\"User \"%s\" not found\"}", username)
} }
// TODO: add ability to change password for user cert . priority=low
func userChangePassword(username string, newPassword string) bool {
return false
}
func (oAdmin *OpenvpnAdmin) mgmtRead(conn net.Conn) string { func (oAdmin *OpenvpnAdmin) mgmtRead(conn net.Conn) string {
buf := make([]byte, 32768) buf := make([]byte, 32768)
@ -786,7 +889,7 @@ func (oAdmin *OpenvpnAdmin) mgmtRead(conn net.Conn) string {
return s return s
} }
func (oAdmin *OpenvpnAdmin) mgmtConnectedUsersParser(text string) []clientStatus { func (oAdmin *OpenvpnAdmin) mgmtConnectedUsersParser(text, serverName string) []clientStatus {
var u []clientStatus var u []clientStatus
isClientList := false isClientList := false
isRouteTable := false isRouteTable := false
@ -814,14 +917,14 @@ func (oAdmin *OpenvpnAdmin) mgmtConnectedUsersParser(text string) []clientStatus
userName := user[0] userName := user[0]
userAddress := user[1] userAddress := user[1]
userBytesRecieved:= user[2] userBytesReceived:= user[2]
userBytesSent:= user[3] userBytesSent:= user[3]
userConnectedSince := user[4] userConnectedSince := user[4]
userStatus := clientStatus{CommonName: userName, RealAddress: userAddress, BytesReceived: userBytesRecieved, BytesSent: userBytesSent, ConnectedSince: userConnectedSince} userStatus := clientStatus{CommonName: userName, RealAddress: userAddress, BytesReceived: userBytesReceived, BytesSent: userBytesSent, ConnectedSince: userConnectedSince, ConnectedTo: serverName}
u = append(u, userStatus) u = append(u, userStatus)
bytesSent, _ := strconv.Atoi(userBytesSent) bytesSent, _ := strconv.Atoi(userBytesSent)
bytesReceive, _ := strconv.Atoi(userBytesRecieved) bytesReceive, _ := strconv.Atoi(userBytesReceived)
ovpnClientConnectionFrom.WithLabelValues(userName, userAddress).Set(float64(parseDateToUnix(ovpnStatusDateLayout, userConnectedSince))) ovpnClientConnectionFrom.WithLabelValues(userName, userAddress).Set(float64(parseDateToUnix(ovpnStatusDateLayout, userConnectedSince)))
ovpnClientBytesSent.WithLabelValues(userName).Set(float64(bytesSent)) ovpnClientBytesSent.WithLabelValues(userName).Set(float64(bytesSent))
ovpnClientBytesReceived.WithLabelValues(userName).Set(float64(bytesReceive)) ovpnClientBytesReceived.WithLabelValues(userName).Set(float64(bytesReceive))
@ -841,8 +944,8 @@ func (oAdmin *OpenvpnAdmin) mgmtConnectedUsersParser(text string) []clientStatus
return u return u
} }
func (oAdmin *OpenvpnAdmin) mgmtKillUserConnection(username string) { func (oAdmin *OpenvpnAdmin) mgmtKillUserConnection(username, serverName string) {
conn, err := net.Dial("tcp", *mgmtListenHost+":"+*mgmtListenPort) conn, err := net.Dial("tcp", oAdmin.mgmtInterfaces[serverName])
if err != nil { if err != nil {
log.Println("ERROR: openvpn mgmt interface is not reachable") log.Println("ERROR: openvpn mgmt interface is not reachable")
return return
@ -854,25 +957,29 @@ func (oAdmin *OpenvpnAdmin) mgmtKillUserConnection(username string) {
} }
func (oAdmin *OpenvpnAdmin) mgmtGetActiveClients() []clientStatus { func (oAdmin *OpenvpnAdmin) mgmtGetActiveClients() []clientStatus {
conn, err := net.Dial("tcp", *mgmtListenHost+":"+*mgmtListenPort) var activeClients []clientStatus
for srv, addr := range oAdmin.mgmtInterfaces {
conn, err := net.Dial("tcp", addr)
if err != nil { if err != nil {
log.Println("ERROR: openvpn mgmt interface is not reachable") log.Printf("ERROR: openvpn mgmt interface for %s is not reachable by addr %s\n", srv, addr)
return []clientStatus{} //return []clientStatus{}
} }
oAdmin.mgmtRead(conn) // read welcome message oAdmin.mgmtRead(conn) // read welcome message
conn.Write([]byte("status\n")) conn.Write([]byte("status\n"))
activeClients := oAdmin.mgmtConnectedUsersParser(oAdmin.mgmtRead(conn)) activeClients = append(activeClients, oAdmin.mgmtConnectedUsersParser(oAdmin.mgmtRead(conn), srv)...)
conn.Close() conn.Close()
}
return activeClients return activeClients
} }
func isUserConnected(username string, connectedUsers []clientStatus) bool { func isUserConnected(username string, connectedUsers []clientStatus) (bool, string) {
for _, connectedUser := range connectedUsers { for _, connectedUser := range connectedUsers {
if connectedUser.CommonName == username { if connectedUser.CommonName == username {
return true return true, connectedUser.ConnectedTo
} }
} }
return false return false, ""
} }
func (oAdmin *OpenvpnAdmin) downloadCerts() bool { func (oAdmin *OpenvpnAdmin) downloadCerts() bool {
@ -968,7 +1075,7 @@ func (oAdmin *OpenvpnAdmin) syncWithMaster() {
} }
} }
func getOpvnCaCertExpireDate() time.Time { func getOvpnCaCertExpireDate() time.Time {
caCertPath := *easyrsaDirPath + "/pki/ca.crt" caCertPath := *easyrsaDirPath + "/pki/ca.crt"
caCertExpireDate := runBash(fmt.Sprintf("openssl x509 -in %s -noout -enddate | awk -F \"=\" {'print $2'}", caCertPath)) caCertExpireDate := runBash(fmt.Sprintf("openssl x509 -in %s -noout -enddate | awk -F \"=\" {'print $2'}", caCertPath))