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,19 +8,18 @@ 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
group nogroup group nogroup
push "topology subnet" push "topology subnet"
push "route-metric 9999" push "route-metric 9999"
push "dhcp-option DNS 1.1.1.1" push "dhcp-option DNS 1.1.1.1"

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>
@ -51,11 +52,11 @@
<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>Add new user </h4> <h4>Add new user</h4>
</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 ">

211
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 {
@ -186,15 +195,16 @@ type indexTxtLine struct {
} }
type clientStatus struct { type clientStatus struct {
CommonName string CommonName string
RealAddress string RealAddress string
BytesReceived string BytesReceived string
BytesSent string BytesSent string
ConnectedSince string ConnectedSince string
VirtualAddress string VirtualAddress string
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,32 +739,82 @@ 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 {
log.Printf("ERROR: userCreate: %s", ucErr) log.Printf("ERROR: userCreate: %s", ucErr)
} }
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
if err != nil {
log.Println("ERROR: openvpn mgmt interface is not reachable") for srv, addr := range oAdmin.mgmtInterfaces {
return []clientStatus{} conn, err := net.Dial("tcp", addr)
if err != nil {
log.Printf("ERROR: openvpn mgmt interface for %s is not reachable by addr %s\n", srv, addr)
//return []clientStatus{}
}
oAdmin.mgmtRead(conn) // read welcome message
conn.Write([]byte("status\n"))
activeClients = append(activeClients, oAdmin.mgmtConnectedUsersParser(oAdmin.mgmtRead(conn), srv)...)
conn.Close()
} }
oAdmin.mgmtRead(conn) // read welcome message
conn.Write([]byte("status\n"))
activeClients := oAdmin.mgmtConnectedUsersParser(oAdmin.mgmtRead(conn))
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))