1
0
Fork 0
mirror of synced 2024-09-07 15:06:26 -04:00

Merge pull request #1 from pashcovich/draft-password-auth

Additional password auth
Multiple mgmt interface usgae
Layout changes
Small fixes
This commit is contained in:
Ilya Sosnovsky 2021-02-20 15:51:28 +03:00 committed by GitHub
commit 3db757659a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 367 additions and 228 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

View file

@ -1,19 +1,18 @@
FROM golang:1.14.2-alpine3.11 AS backend-builder FROM golang:1.14.2-buster AS backend-builder
COPY . /app COPY . /app
#RUN apk --no-cache add build-base git gcc RUN cd /app && env CGO_ENABLED=./1 GOOS=linux GOARCH=amd64 go build -ldflags='-linkmode external -extldflags "-static" -s -w' -o openvpn-admin
RUN cd /app && env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags='-extldflags "-static" -s -w' -o openvpn-admin
FROM node:14.2-alpine3.11 AS frontend-builder 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 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=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 && \
wget https://github.com/pashcovich/openvpn-user/releases/download/v1.0.3-rc.1/openvpn-user-linux-amd64.tar.gz -O - | tar xz -C /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,7 +1,7 @@
FROM alpine:3.11 FROM alpine:3.13
RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \ RUN apk add --update bash openvpn easy-rsa && \
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 && \
wget https://github.com/pashcovich/openvpn-user/releases/download/v1.0.3-rc.1/openvpn-user-linux-amd64.tar.gz -O - | tar xz -C /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 setup/ /etc/openvpn/setup
RUN chmod +x /etc/openvpn/setup/configure.sh RUN chmod +x /etc/openvpn/setup/configure.sh

View file

@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags='-extldflags "-static" -s -w' -o openvpn-admin CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags "-linkmode external -extldflags -static -s -w" -o openvpn-admin

View file

@ -1,5 +1,5 @@
{{- range $server := .Hosts }} {{- range $server := .Hosts }}
remote {{ $server.Host }} {{ $server.Port }} tcp remote {{ $server.Host }} {{ $server.Port }} {{ $server.Protocol }}
{{- end }} {{- end }}
verb 4 verb 4
@ -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.server="127.0.0.1:7744" --ovpn.server="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

@ -7838,6 +7838,11 @@
"vue-style-loader": "^4.1.0" "vue-style-loader": "^4.1.0"
} }
}, },
"vue-notification": {
"version": "1.3.20",
"resolved": "https://registry.npmjs.org/vue-notification/-/vue-notification-1.3.20.tgz",
"integrity": "sha512-vPj67Ah72p8xvtyVE8emfadqVWguOScAjt6OJDEUdcW5hW189NsqvfkOrctxHUUO9UYl9cTbIkzAEcPnHu+zBQ=="
},
"vue-style-loader": { "vue-style-loader": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz", "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz",

View file

@ -14,7 +14,8 @@
"vue": "^2.6.12", "vue": "^2.6.12",
"vue-clipboard2": "^0.2.1", "vue-clipboard2": "^0.2.1",
"vue-cookies": "^1.7.4", "vue-cookies": "^1.7.4",
"vue-good-table": "^2.21.1" "vue-good-table": "^2.21.1",
"vue-notification": "^1.3.20"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",

View file

@ -3,12 +3,14 @@ import axios from 'axios';
import VueCookies from 'vue-cookies' import VueCookies from 'vue-cookies'
import VueClipboard from 'vue-clipboard2' import VueClipboard from 'vue-clipboard2'
import VueGoodTablePlugin from 'vue-good-table' import VueGoodTablePlugin from 'vue-good-table'
import Notifications from 'vue-notification'
import 'vue-good-table/dist/vue-good-table.css' import 'vue-good-table/dist/vue-good-table.css'
Vue.use(VueClipboard) Vue.use(VueClipboard)
Vue.use(VueGoodTablePlugin) Vue.use(VueGoodTablePlugin)
Vue.use(VueCookies) Vue.use(VueCookies)
Vue.use(Notifications)
var axios_cfg = function(url, data='', type='form') { var axios_cfg = function(url, data='', type='form') {
if (data == '') { if (data == '') {
@ -54,6 +56,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 +91,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 +148,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: '',
@ -160,6 +184,7 @@ new Vue({
axios.request(axios_cfg('api/user/revoke', data, 'form')) axios.request(axios_cfg('api/user/revoke', data, 'form'))
.then(function(response) { .then(function(response) {
_this.getUserData(); _this.getUserData();
_this.$notify({title: 'User ' + _this.username + ' revoked!', type: 'warn'})
}); });
}) })
_this.$root.$on('u-unrevoke', function () { _this.$root.$on('u-unrevoke', function () {
@ -168,6 +193,7 @@ new Vue({
axios.request(axios_cfg('api/user/unrevoke', data, 'form')) axios.request(axios_cfg('api/user/unrevoke', data, 'form'))
.then(function(response) { .then(function(response) {
_this.getUserData(); _this.getUserData();
_this.$notify({title: 'User ' + _this.username + ' unrevoked!', type: 'success'})
}); });
}) })
_this.$root.$on('u-show-config', function () { _this.$root.$on('u-show-config', function () {
@ -210,6 +236,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 +249,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 +261,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"
}, },
@ -256,6 +293,15 @@ new Vue({
_this.rows = response.data; _this.rows = response.data;
}); });
}, },
staticAddrCheckboxOnChange: function() {
var staticAddrInput = document.getElementById('static-address');
var staticAddrEnable = document.getElementById('enable-static');
staticAddrInput.disabled = !staticAddrEnable.checked;
staticAddrInput.value == "dynamic" ? staticAddrInput.value = "" : staticAddrInput.value = "dynamic";
},
getServerRole: function() { getServerRole: function() {
var _this = this; var _this = this;
axios.request(axios_cfg('api/server/role')) axios.request(axios_cfg('api/server/role'))
@ -269,6 +315,7 @@ new Vue({
} }
}); });
}, },
createUser: function() { createUser: function() {
var _this = this; var _this = this;
@ -276,19 +323,23 @@ 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();
_this.$notify({title: 'New user ' + _this.username + ' created', type: 'success'})
}) })
.catch(function(error) { .catch(function(error) {
_this.u.newUserCreateError = error.response.data; _this.u.newUserCreateError = error.response.data;
_this.$notify({title: 'New user ' + _this.username + ' creation failed.', type: 'error'})
}); });
}, },
ccdApply: function() { ccdApply: function() {
var _this = this; var _this = this;
@ -299,11 +350,38 @@ new Vue({
.then(function(response) { .then(function(response) {
_this.u.ccdApplyStatus = 200; _this.u.ccdApplyStatus = 200;
_this.u.ccdApplyStatusMessage = response.data; _this.u.ccdApplyStatusMessage = response.data;
_this.$notify({title: 'Ccd for user ' + _this.username + ' applied', type: 'success'})
}) })
.catch(function(error) { .catch(function(error) {
_this.u.ccdApplyStatus = error.response.status; _this.u.ccdApplyStatus = error.response.status;
_this.u.ccdApplyStatusMessage = error.response.data; _this.u.ccdApplyStatusMessage = error.response.data;
_this.$notify({title: 'Ccd for user ' + _this.username + ' apply failed ', type: 'error'})
}); });
} },
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.getUserData();
_this.u.modalChangePasswordVisible = false;
_this.$notify({title: 'Password for user ' + _this.username + ' changed!', type: 'success'})
})
.catch(function(error) {
_this.u.passwordChangeStatus = error.response.status;
_this.u.passwordChangeMessage = error.response.data.message;
_this.$notify({title: 'Changing password for user ' + _this.username + ' failed!', type: 'error'})
});
},
} }
}) })

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.passwordChangeMessage='';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" @change="staticAddrCheckboxOnChange()" 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="static-address" 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 ">
@ -121,22 +147,22 @@
<tbody> <tbody>
<tr v-for="(customRoute, index) in u.ccd.CustomRoutes"> <tr v-for="(customRoute, index) in u.ccd.CustomRoutes">
<td> <td>
<div v-if = "serverRole == 'slave'"> <div v-if="serverRole == 'slave'">
{{ customRoute.Address }} {{ customRoute.Address }}
</div> </div>
<input v-if = "serverRole == 'master'" v-model = "customRoute.Address"> <input v-if="serverRole == 'master'" v-model="customRoute.Address">
</td> </td>
<td> <td>
<div v-if = "serverRole == 'slave'"> <div v-if="serverRole == 'slave'">
{{ customRoute.Mask }} {{ customRoute.Mask }}
</div> </div>
<input v-if = "serverRole == 'master'" v-model = "customRoute.Mask"> <input v-if="serverRole == 'master'" v-model="customRoute.Mask">
</td> </td>
<td> <td>
<div v-if = "serverRole == 'slave'"> <div v-if="serverRole == 'slave'">
{{ customRoute.Description }} {{ customRoute.Description }}
</div> </div>
<input v-if = "serverRole == 'master'" v-model = "customRoute.Description"> <input v-if="serverRole == 'master'" v-model="customRoute.Description">
</td> </td>
<td class="text-right" v-if="serverRole == 'master'"> <td class="text-right" v-if="serverRole == 'master'">
<button type="button" class="btn btn-danger btn-sm el-square modal-el-margin" v-if="serverRole == 'master'" v-on:click.stop="u.ccd.CustomRoutes.splice(index, 1)">Delete</button> <button type="button" class="btn btn-danger btn-sm el-square modal-el-margin" v-if="serverRole == 'master'" v-on:click.stop="u.ccd.CustomRoutes.splice(index, 1)">Delete</button>
@ -167,6 +193,7 @@
</div> </div>
</div> </div>
<notifications position="bottom left" :speed="900" />
</div> </div>
<script src="dist/build.js"></script> <script src="dist/build.js"></script>
</body> </body>

225
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"
@ -28,10 +30,10 @@ const (
indexTxtDateLayout = "060102150405Z" indexTxtDateLayout = "060102150405Z"
stringDateFormat = "2006-01-02 15:04:05" stringDateFormat = "2006-01-02 15:04:05"
ovpnStatusDateLayout = "Mon Jan 2 15:04:05 2006" ovpnStatusDateLayout = "Mon Jan 2 15:04:05 2006"
version = "1.5.0"
) )
var ( var (
listenHost = kingpin.Flag("listen.host","host for openvpn-admin").Default("0.0.0.0").String() listenHost = kingpin.Flag("listen.host","host for openvpn-admin").Default("0.0.0.0").String()
listenPort = kingpin.Flag("listen.port","port for openvpn-admin").Default("8080").String() listenPort = kingpin.Flag("listen.port","port for openvpn-admin").Default("8080").String()
serverRole = kingpin.Flag("role","server role master or slave").Default("master").HintOptions("master", "slave").String() serverRole = kingpin.Flag("role","server role master or slave").Default("master").HintOptions("master", "slave").String()
@ -40,16 +42,18 @@ var (
masterBasicAuthPassword = kingpin.Flag("master.basic-auth.password","password for basic auth on master server url").Default("").String() masterBasicAuthPassword = kingpin.Flag("master.basic-auth.password","password for basic auth on master server url").Default("").String()
masterSyncFrequency = kingpin.Flag("master.sync-frequency", "master host data sync frequency in seconds.").Default("600").Int() masterSyncFrequency = kingpin.Flag("master.sync-frequency", "master host data sync frequency in seconds.").Default("600").Int()
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.server","comma separated addresses for openvpn servers").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=address) 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()
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 +107,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,11 +144,14 @@ 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
Protocol string
} }
type openvpnClientConfig struct { type openvpnClientConfig struct {
@ -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
@ -337,7 +366,9 @@ func (oAdmin *OpenvpnAdmin) downloadCddHandler(w http.ResponseWriter, r *http.Re
} }
func main() { func main() {
kingpin.Parse() kingpin.Version(version)
kingpin.Parse()
ovpnAdmin := new(OpenvpnAdmin) ovpnAdmin := new(OpenvpnAdmin)
ovpnAdmin.lastSyncTime = "unknown" ovpnAdmin.lastSyncTime = "unknown"
@ -346,6 +377,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 +406,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 +418,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 +454,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() {
@ -469,8 +508,8 @@ func (oAdmin *OpenvpnAdmin) renderClientConfig(username string) string {
var hosts []OpenvpnServer var hosts []OpenvpnServer
for _, server := range *openvpnServer { for _, server := range *openvpnServer {
parts := strings.SplitN(server, ":",2) parts := strings.SplitN(server, ":",3)
hosts = append(hosts, OpenvpnServer{Host: parts[0], Port: parts[1]}) hosts = append(hosts, OpenvpnServer{Host: parts[0], Port: parts[1], Protocol: parts[2]})
} }
conf := openvpnClientConfig{} conf := openvpnClientConfig{}
@ -479,6 +518,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 +664,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 +711,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 +741,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 +830,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 +863,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 +883,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 +891,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 +919,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,10 +946,10 @@ 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.Printf("WARNING: openvpn mgmt interface for %s is not reachable by addr %s\n", serverName, oAdmin.mgmtInterfaces[serverName])
return return
} }
oAdmin.mgmtRead(conn) // read welcome message oAdmin.mgmtRead(conn) // read welcome message
@ -854,25 +959,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("WARNING: openvpn mgmt interface for %s is not reachable by addr %s\n", srv, addr)
break
}
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 +1077,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))

16
setup/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"

120
werf.yaml
View file

@ -1,120 +0,0 @@
project: openvpn-admin
configVersion: 1
deploy:
helmRelease: "[[ project ]]-[[ env ]]"
namespace: "[[ project ]]-[[ env ]]"
---
artifact: backend-builder
from: golang:1.14.2-alpine3.11
git:
- add: /
to: /app
stageDependencies:
install:
- "*.go"
excludePaths:
- .helm
- .werf
- frontend
- werf.yaml
- Dockerfile
ansible:
install:
- name: Install packages
apk:
name:
- build-base
- gcc
- name: Build backend
command: go build -ldflags='-extldflags "-static" -s -w' -o openvpn-admin
environment:
CGO_ENABLED: 0
GOOS: linux
GOARCH: amd64
args:
chdir: /app
---
artifact: frontend-builder
from: node:14.2-alpine3.11
git:
- add: /frontend
to: /app
stageDependencies:
install:
- "**/*"
excludePaths:
- Dockerfile
- build.sh
- werf.yaml
ansible:
setup:
- name: install deps
command: npm install
args:
chdir: /app
- name: Build app
command: npm run build
args:
chdir: /app
---
image: openvpn-admin
from: alpine:3.11
import:
- artifact: backend-builder
add: /app/openvpn-admin
to: /usr/bin/openvpn-admin
before: setup
- artifact: frontend-builder
add: /app/static
to: /app/static
before: setup
git:
- add: /client.conf.tpl
to: /app/client.conf.tpl
stageDependencies:
setup:
- "*"
- add: /ccd.tpl
to: /app/ccd.tpl
stageDependencies:
setup:
- "*"
ansible:
install:
- name: Install packages
apk:
name:
- easy-rsa
- bash
- name: Create symbolic link for easy-rsa
file:
src: "/usr/share/easy-rsa/easyrsa"
dest: "/usr/local/bin/easyrsa"
state: link
---
image: openvpn
from: alpine:3.11
git:
- add: /.werffiles/
to: /etc/openvpn/setup/
stageDependencies:
install:
- "*"
ansible:
install:
- name: Install packages
apk:
name:
- openvpn
- easy-rsa
- name: Create symbolic link for easy-rsa
file:
src: "/usr/share/easy-rsa/easyrsa"
dest: "/usr/local/bin/easyrsa"
state: link