1
0
Fork 0
mirror of synced 2024-12-26 07:53:23 -05: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-ui
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
#RUN apk --no-cache add build-base git gcc
RUN cd /app && env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags='-extldflags "-static" -s -w' -o openvpn-admin
RUN cd /app && env CGO_ENABLED=./1 GOOS=linux GOARCH=amd64 go build -ldflags='-linkmode external -extldflags "-static" -s -w' -o openvpn-admin
FROM node:14.2-alpine3.11 AS frontend-builder
COPY frontend/ /app
RUN cd /app && npm install && npm run build
FROM alpine:3.11
FROM alpine:3.13
WORKDIR /app
COPY --from=backend-builder /app/openvpn-admin /app
COPY --from=frontend-builder /app/static /app/static
COPY client.conf.tpl /app/client.conf.tpl
COPY ccd.tpl /app/ccd.tpl
RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \
apk add --update bash easy-rsa && \
RUN apk add --update bash easy-rsa && \
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/*

View file

@ -1,7 +1,7 @@
FROM alpine:3.11
RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \
apk add --update bash openvpn easy-rsa && \
FROM alpine:3.13
RUN apk add --update bash openvpn easy-rsa && \
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/*
COPY .werffiles /etc/openvpn/setup
COPY setup/ /etc/openvpn/setup
RUN chmod +x /etc/openvpn/setup/configure.sh

View file

@ -1,3 +1,3 @@
#!/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 }}
remote {{ $server.Host }} {{ $server.Port }} tcp
remote {{ $server.Host }} {{ $server.Port }} {{ $server.Protocol }}
{{- end }}
verb 4
@ -11,10 +11,19 @@ key-direction 1
#redirect-gateway def1
tls-client
remote-cert-tls server
# for update resolv.conf on ubuntu
#script-security 2 system
# uncomment needed below lines for use with linux
#script-security 2
# if use use resolved
#up /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>

View file

@ -8,6 +8,7 @@ services:
image: openvpn:local
command: /etc/openvpn/setup/configure.sh
environment:
- OPVN_PASSWD_AUTH=true
- OPVN_ROLE=slave
cap_add:
- NET_ADMIN
@ -21,7 +22,7 @@ services:
build:
context: .
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:
- OPVN_SLAVE=1
network_mode: service:openvpn

View file

@ -7,6 +7,8 @@ services:
dockerfile: Dockerfile.openvpn
image: openvpn:local
command: /etc/openvpn/setup/configure.sh
environment:
- OPVN_PASSWD_AUTH=true
cap_add:
- NET_ADMIN
ports:
@ -19,7 +21,7 @@ services:
build:
context: .
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
volumes:
- ./easyrsa_master:/mnt/easyrsa

View file

@ -7838,6 +7838,11 @@
"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": {
"version": "4.1.2",
"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-clipboard2": "^0.2.1",
"vue-cookies": "^1.7.4",
"vue-good-table": "^2.21.1"
"vue-good-table": "^2.21.1",
"vue-notification": "^1.3.20"
},
"browserslist": [
"> 1%",

View file

@ -3,12 +3,14 @@ import axios from 'axios';
import VueCookies from 'vue-cookies'
import VueClipboard from 'vue-clipboard2'
import VueGoodTablePlugin from 'vue-good-table'
import Notifications from 'vue-notification'
import 'vue-good-table/dist/vue-good-table.css'
Vue.use(VueClipboard)
Vue.use(VueGoodTablePlugin)
Vue.use(VueCookies)
Vue.use(Notifications)
var axios_cfg = function(url, data='', type='form') {
if (data == '') {
@ -54,6 +56,11 @@ new Vue({
field: 'AccountStatus',
filterable: true,
},
{
label: 'Connection Server',
field: 'ConnectionServer',
filterable: true,
},
{
label: 'Expiration Date',
field: 'ExpirationDate',
@ -84,39 +91,52 @@ new Vue({
],
rows: [],
actions: [
{
name: 'u-change-password',
label: 'Change password',
class: 'btn-warning',
showWhenStatus: 'Active',
showForServerRole: ['master']
},
{
name: 'u-revoke',
label: 'Revoke',
class: 'btn-warning',
showWhenStatus: 'Active',
showForServerRole: ['master']
},
{
name: 'u-unrevoke',
label: 'Unrevoke',
class: 'btn-primary',
showWhenStatus: 'Revoked',
showForServerRole: ['master']
},
{
name: 'u-show-config',
label: 'Show config',
showWhenStatus: 'Active',
showForServerRole: ['master', 'slave']
},
// {
// name: 'u-show-config',
// label: 'Show config',
// class: 'btn-primary',
// showWhenStatus: 'Active',
// showForServerRole: ['master', 'slave']
// },
{
name: 'u-download-config',
label: 'Download config',
class: 'btn-info',
showWhenStatus: 'Active',
showForServerRole: ['master', 'slave']
},
{
name: 'u-edit-ccd',
label: 'Edit routes',
class: 'btn-primary',
showWhenStatus: 'Active',
showForServerRole: ['master']
},
{
name: 'u-edit-ccd',
label: 'Show routes',
class: 'btn-primary',
showWhenStatus: 'Active',
showForServerRole: ['slave']
}
@ -128,11 +148,15 @@ new Vue({
lastSync: "unknown",
u: {
newUserName: '',
// newUserPassword: 'nopass',
newUserPassword: '',
newUserCreateError: '',
newPassword: '',
passwordChangeStatus: '',
passwordChangeMessage: '',
modalNewUserVisible: false,
modalShowConfigVisible: false,
modalShowCcdVisible: false,
modalChangePasswordVisible: false,
openvpnConfig: '',
ccd: {
Name: '',
@ -160,6 +184,7 @@ new Vue({
axios.request(axios_cfg('api/user/revoke', data, 'form'))
.then(function(response) {
_this.getUserData();
_this.$notify({title: 'User ' + _this.username + ' revoked!', type: 'warn'})
});
})
_this.$root.$on('u-unrevoke', function () {
@ -168,6 +193,7 @@ new Vue({
axios.request(axios_cfg('api/user/unrevoke', data, 'form'))
.then(function(response) {
_this.getUserData();
_this.$notify({title: 'User ' + _this.username + ' unrevoked!', type: 'success'})
});
})
_this.$root.$on('u-show-config', function () {
@ -210,6 +236,11 @@ new Vue({
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: {
customAddressDisabled: function () {
@ -218,6 +249,9 @@ new Vue({
ccdApplyStatusCssClass: function () {
return this.u.ccdApplyStatus == 200 ? "alert-success" : "alert-danger"
},
passwordChangeStatusCssClass: function () {
return this.u.passwordChangeStatus == 200 ? "alert-success" : "alert-danger"
},
modalNewUserDisplay: function () {
return this.u.modalNewUserVisible ? {display: 'flex'} : {}
},
@ -227,6 +261,9 @@ new Vue({
modalShowCcdDisplay: function () {
return this.u.modalShowCcdVisible ? {display: 'flex'} : {}
},
modalChangePasswordDisplay: function () {
return this.u.modalChangePasswordVisible ? {display: 'flex'} : {}
},
revokeFilterText: function() {
return this.filters.hideRevoked ? "Show revoked" : "Hide revoked"
},
@ -256,6 +293,15 @@ new Vue({
_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() {
var _this = this;
axios.request(axios_cfg('api/server/role'))
@ -269,6 +315,7 @@ new Vue({
}
});
},
createUser: function() {
var _this = this;
@ -276,19 +323,23 @@ new Vue({
var data = new URLSearchParams();
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'))
.then(function(response) {
_this.getUserData();
_this.u.modalNewUserVisible = false;
_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) {
_this.u.newUserCreateError = error.response.data;
_this.$notify({title: 'New user ' + _this.username + ' creation failed.', type: 'error'})
});
},
ccdApply: function() {
var _this = this;
@ -299,11 +350,38 @@ new Vue({
.then(function(response) {
_this.u.ccdApplyStatus = 200;
_this.u.ccdApplyStatusMessage = response.data;
_this.$notify({title: 'Ccd for user ' + _this.username + ' applied', type: 'success'})
})
.catch(function(error) {
_this.u.ccdApplyStatus = error.response.status;
_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">
<span v-if="props.column.field == 'actions'">
<button
class="btn btn-sm btn-success el-square modal-el-margin"
class="btn btn-sm el-square modal-el-margin"
type="button"
:title="action.label"
:data-username="props.row.Identity"
@ -36,6 +36,7 @@
:data-text="action.label"
@click.left.stop="rowActionFn"
v-for="action in actions"
v-bind:class="action.class"
v-if="action.showWhenStatus == props.row.AccountStatus && action.showForServerRole.includes(serverRole)">
{{ action.label }}
</button>
@ -51,11 +52,11 @@
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4>Add new user </h4>
<h4>Add new user</h4>
</div>
<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="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 class="modal-footer justify-content-center" v-if="u.newUserCreateError.length > 0">
@ -64,18 +65,41 @@
</div>
</div>
<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>
</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-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4>ovpn config for {{ username }}</h4>
<h4>ovpn config for: <strong>{{ username }}</strong></h4>
</div>
<div class="modal-body">
<div class="d-flex">
@ -96,16 +120,18 @@
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<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">
<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-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>
<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 class="modal-body">
<div class="d-flex ">
@ -121,22 +147,22 @@
<tbody>
<tr v-for="(customRoute, index) in u.ccd.CustomRoutes">
<td>
<div v-if = "serverRole == 'slave'">
<div v-if="serverRole == 'slave'">
{{ customRoute.Address }}
</div>
<input v-if = "serverRole == 'master'" v-model = "customRoute.Address">
<input v-if="serverRole == 'master'" v-model="customRoute.Address">
</td>
<td>
<div v-if = "serverRole == 'slave'">
<div v-if="serverRole == 'slave'">
{{ customRoute.Mask }}
</div>
<input v-if = "serverRole == 'master'" v-model = "customRoute.Mask">
<input v-if="serverRole == 'master'" v-model="customRoute.Mask">
</td>
<td>
<div v-if = "serverRole == 'slave'">
<div v-if="serverRole == 'slave'">
{{ customRoute.Description }}
</div>
<input v-if = "serverRole == 'master'" v-model = "customRoute.Description">
<input v-if="serverRole == 'master'" v-model="customRoute.Description">
</td>
<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>
@ -167,6 +193,7 @@
</div>
</div>
<notifications position="bottom left" :speed="900" />
</div>
<script src="dist/build.js"></script>
</body>

225
main.go
View file

@ -20,7 +20,9 @@ import (
)
const (
usernameRegexp = `^([a-zA-Z0-9_.-])+$`
usernameRegexp = `^([a-zA-Z0-9_.-@])+$`
passwordRegexp = `^([a-zA-Z0-9_.-@])+$`
passwordMinLength = 6
downloadCertsApiUrl = "/api/data/certs/download"
downloadCcdApiUrl = "/api/data/ccd/download"
certsArchiveFileName = "certs.tar.gz"
@ -28,10 +30,10 @@ const (
indexTxtDateLayout = "060102150405Z"
stringDateFormat = "2006-01-02 15:04:05"
ovpnStatusDateLayout = "Mon Jan 2 15:04:05 2006"
version = "1.5.0"
)
var (
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()
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()
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()
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()
mgmtListenHost = kingpin.Flag("mgmt.host","host for openvpn server mgmt interface").Default("127.0.0.1").String()
mgmtListenPort = kingpin.Flag("mgmt.port","port for openvpn server mgmt interface").Default("8989").String()
mgmtAddress = kingpin.Flag("mgmt","comma separated (alias=address) for openvpn servers mgmt interfaces").Default("main=127.0.0.1:8989").Strings()
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()
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()
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()
debug = kingpin.Flag("debug", "Enable debug mode.").Default("false").Bool()
verbose = kingpin.Flag("verbose", "Enable verbose mode.").Default("false").Bool()
certsArchivePath = "/tmp/" + certsArchiveFileName
ccdArchivePath = "/tmp/" + ccdArchiveFileName
@ -103,7 +107,7 @@ var (
ovpnClientConnectionInfo = prometheus.NewGaugeVec(prometheus.GaugeOpts{
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"},
)
@ -140,11 +144,14 @@ type OpenvpnAdmin struct {
clients []OpenvpnClient
activeClients []clientStatus
promRegistry *prometheus.Registry
mgmtInterfaces map[string]string
}
type OpenvpnServer struct {
Host string
Port string
Protocol string
}
type openvpnClientConfig struct {
@ -153,6 +160,7 @@ type openvpnClientConfig struct {
Cert string
Key string
TLS string
PasswdAuth bool
}
type OpenvpnClient struct {
@ -161,6 +169,7 @@ type OpenvpnClient struct {
ExpirationDate string `json:"ExpirationDate"`
RevocationDate string `json:"RevocationDate"`
ConnectionStatus string `json:"ConnectionStatus"`
ConnectionServer string `json:"ConnectionServer"`
}
type ccdRoute struct {
@ -186,15 +195,16 @@ type indexTxtLine struct {
}
type clientStatus struct {
CommonName string
RealAddress string
BytesReceived string
BytesSent string
ConnectedSince string
VirtualAddress string
LastRef string
CommonName string
RealAddress string
BytesReceived string
BytesSent string
ConnectedSince string
VirtualAddress string
LastRef string
ConnectedSinceFormatted string
LastRefFormatted string
ConnectedTo string
}
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
}
r.ParseForm()
userCreated, userCreateStatus := oAdmin.userCreate(r.FormValue("username"))
userCreated, userCreateStatus := oAdmin.userCreate(r.FormValue("username"), r.FormValue("password"))
if userCreated {
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")))
}
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) {
r.ParseForm()
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)
}
func (oAdmin *OpenvpnAdmin) downloadCddHandler(w http.ResponseWriter, r *http.Request) {
func (oAdmin *OpenvpnAdmin) downloadCcdHandler(w http.ResponseWriter, r *http.Request) {
if oAdmin.role == "slave" {
http.Error(w, `{"status":"error"}`, http.StatusLocked)
return
@ -337,7 +366,9 @@ func (oAdmin *OpenvpnAdmin) downloadCddHandler(w http.ResponseWriter, r *http.Re
}
func main() {
kingpin.Parse()
kingpin.Version(version)
kingpin.Parse()
ovpnAdmin := new(OpenvpnAdmin)
ovpnAdmin.lastSyncTime = "unknown"
@ -346,6 +377,13 @@ func main() {
ovpnAdmin.masterSyncToken = *masterSyncToken
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.setState()
@ -368,6 +406,7 @@ func main() {
http.HandleFunc("/api/server/role", ovpnAdmin.serverRoleHandler)
http.HandleFunc("/api/users/list", ovpnAdmin.userListHandler)
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/unrevoke", ovpnAdmin.userUnrevokeHandler)
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/successful", ovpnAdmin.lastSuccessfulSyncTimeHandler)
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.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
@ -415,7 +454,7 @@ func (oAdmin *OpenvpnAdmin) setState() {
oAdmin.activeClients = oAdmin.mgmtGetActiveClients()
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() {
@ -469,8 +508,8 @@ func (oAdmin *OpenvpnAdmin) renderClientConfig(username string) string {
var hosts []OpenvpnServer
for _, server := range *openvpnServer {
parts := strings.SplitN(server, ":",2)
hosts = append(hosts, OpenvpnServer{Host: parts[0], Port: parts[1]})
parts := strings.SplitN(server, ":",3)
hosts = append(hosts, OpenvpnServer{Host: parts[0], Port: parts[1], Protocol: parts[2]})
}
conf := openvpnClientConfig{}
@ -479,6 +518,7 @@ func (oAdmin *OpenvpnAdmin) renderClientConfig(username string) string {
conf.Cert = fRead(*easyrsaDirPath + "/pki/issued/" + username + ".crt")
conf.Key = fRead(*easyrsaDirPath + "/pki/private/" + username + ".key")
conf.TLS = fRead(*easyrsaDirPath + "/pki/ta.key")
conf.PasswdAuth = *authByPassword
t, _ := template.ParseFiles("client.conf.tpl")
var tmp bytes.Buffer
@ -624,6 +664,14 @@ func validateUsername(username string) bool {
return validUsername.MatchString(username)
}
func validatePassword(password string) bool {
if len(password) < passwordMinLength {
return false
} else {
return true
}
}
func checkUserExist(username string) bool {
for _, u := range indexTxtParser(fRead(*indexTxtPath)) {
if u.DistinguishedName == ("/CN=" + username) {
@ -663,8 +711,12 @@ func (oAdmin *OpenvpnAdmin) usersList() []OpenvpnClient {
expiredCerts += 1
}
if isUserConnected(line.Identity, oAdmin.activeClients) {
ovpnClient.ConnectionServer = ""
userConnected, userConnectedTo := isUserConnected(line.Identity, oAdmin.activeClients)
if userConnected {
ovpnClient.ConnectionStatus = "Connected"
ovpnClient.ConnectionServer = userConnectedTo
connectedUsers += 1
}
@ -689,32 +741,82 @@ func (oAdmin *OpenvpnAdmin) usersList() []OpenvpnClient {
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)
// 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) {
ucErr = fmt.Sprintf("User \"%s\" already exists\n", username)
if *debug {
log.Printf("ERROR: userCreate: %s", ucErr)
}
if *debug {
log.Printf("ERROR: userCreate: %s", 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))
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)
}
oAdmin.clients = oAdmin.usersList()
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 {
for _, u := range oAdmin.activeClients {
if u.CommonName == username {
@ -728,6 +830,10 @@ func (oAdmin *OpenvpnAdmin) userRevoke(username string) string {
if checkUserExist(username) {
// 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))
if *authByPassword {
o = runBash(fmt.Sprintf("openvpn-user revoke --db-path %s --user %s", *authDatabase, username))
//fmt.Println(o)
}
crlFix()
oAdmin.clients = oAdmin.usersList()
return fmt.Sprintln(o)
@ -757,6 +863,10 @@ func (oAdmin *OpenvpnAdmin) userUnrevoke(username string) string {
//fmt.Print(renderIndexTxt(usersFromIndexTxt))
o = runBash(fmt.Sprintf("cd %s && easyrsa gen-crl", *easyrsaDirPath))
//fmt.Println(o)
if *authByPassword {
o = runBash(fmt.Sprintf("openvpn-user restore --db-path %s --user %s", *authDatabase, username))
//fmt.Println(o)
}
crlFix()
o = ""
fmt.Println(o)
@ -773,11 +883,6 @@ func (oAdmin *OpenvpnAdmin) userUnrevoke(username string) string {
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 {
buf := make([]byte, 32768)
@ -786,7 +891,7 @@ func (oAdmin *OpenvpnAdmin) mgmtRead(conn net.Conn) string {
return s
}
func (oAdmin *OpenvpnAdmin) mgmtConnectedUsersParser(text string) []clientStatus {
func (oAdmin *OpenvpnAdmin) mgmtConnectedUsersParser(text, serverName string) []clientStatus {
var u []clientStatus
isClientList := false
isRouteTable := false
@ -814,14 +919,14 @@ func (oAdmin *OpenvpnAdmin) mgmtConnectedUsersParser(text string) []clientStatus
userName := user[0]
userAddress := user[1]
userBytesRecieved:= user[2]
userBytesReceived:= user[2]
userBytesSent:= user[3]
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)
bytesSent, _ := strconv.Atoi(userBytesSent)
bytesReceive, _ := strconv.Atoi(userBytesRecieved)
bytesReceive, _ := strconv.Atoi(userBytesReceived)
ovpnClientConnectionFrom.WithLabelValues(userName, userAddress).Set(float64(parseDateToUnix(ovpnStatusDateLayout, userConnectedSince)))
ovpnClientBytesSent.WithLabelValues(userName).Set(float64(bytesSent))
ovpnClientBytesReceived.WithLabelValues(userName).Set(float64(bytesReceive))
@ -841,10 +946,10 @@ func (oAdmin *OpenvpnAdmin) mgmtConnectedUsersParser(text string) []clientStatus
return u
}
func (oAdmin *OpenvpnAdmin) mgmtKillUserConnection(username string) {
conn, err := net.Dial("tcp", *mgmtListenHost+":"+*mgmtListenPort)
func (oAdmin *OpenvpnAdmin) mgmtKillUserConnection(username, serverName string) {
conn, err := net.Dial("tcp", oAdmin.mgmtInterfaces[serverName])
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
}
oAdmin.mgmtRead(conn) // read welcome message
@ -854,25 +959,29 @@ func (oAdmin *OpenvpnAdmin) mgmtKillUserConnection(username string) {
}
func (oAdmin *OpenvpnAdmin) mgmtGetActiveClients() []clientStatus {
conn, err := net.Dial("tcp", *mgmtListenHost+":"+*mgmtListenPort)
if err != nil {
log.Println("ERROR: openvpn mgmt interface is not reachable")
return []clientStatus{}
var activeClients []clientStatus
for srv, addr := range oAdmin.mgmtInterfaces {
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
}
func isUserConnected(username string, connectedUsers []clientStatus) bool {
func isUserConnected(username string, connectedUsers []clientStatus) (bool, string) {
for _, connectedUser := range connectedUsers {
if connectedUser.CommonName == username {
return true
return true, connectedUser.ConnectedTo
}
}
return false
return false, ""
}
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"
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
set -x
set -ex
EASY_RSA_LOC="/etc/openvpn/easyrsa"
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
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
[ -f $EASY_RSA_LOC/pki/crl.pem ] && chmod 644 $EASY_RSA_LOC/pki/crl.pem
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
tls-auth /etc/openvpn/easyrsa/pki/ta.key
key-direction 0
duplicate-cn
cipher AES-128-CBC
management 127.0.0.1 8989
#management 127.0.0.1 8989
keepalive 10 60
persist-key
persist-tun
topology subnet
proto tcp
port 1194
#proto tcp
#port 1194
dev tun0
status /tmp/openvpn-status.log
user nobody
group nogroup
push "topology subnet"
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