Merge pull request #132 from flant/rc/2.0.0

Release 2.0.0
This commit is contained in:
Ilya Sosnovsky 2022-08-29 12:20:48 +03:00 committed by GitHub
commit ca53605554
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1859 additions and 10225 deletions

View File

@ -3,7 +3,7 @@
*.iml *.iml
out out
gen gen
.github
easyrsa easyrsa
easyrsa_master easyrsa_master
@ -13,6 +13,7 @@ ccd_master
ccd_slave ccd_slave
werf.yaml werf.yaml
frontend/node_modules frontend/node_modules
frontend/static/dist
openvpn-web-ui openvpn-web-ui
openvpn-ui openvpn-ui
openvpn-admin openvpn-admin
@ -20,4 +21,6 @@ ovpn-admin
docker-compose.yaml docker-compose.yaml
docker-compose-slave.yaml docker-compose-slave.yaml
img img
dashboard
.helm

View File

@ -5,7 +5,7 @@ on:
jobs: jobs:
build: build:
name: build latest images for relase name: build latest images for release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code

View File

@ -17,7 +17,7 @@ jobs:
- name: checkout code - name: checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: build binaries - name: build binaries
uses: wangyoucao577/go-release-action@v1.22 uses: wangyoucao577/go-release-action@v1.28
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
goversion: 1.17 goversion: 1.17

View File

@ -17,7 +17,7 @@ jobs:
- name: checkout code - name: checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: build binaries - name: build binaries
uses: wangyoucao577/go-release-action@v1.22 uses: wangyoucao577/go-release-action@v1.28
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
goversion: 1.17 goversion: 1.17

View File

@ -1,4 +1,4 @@
FROM node:16.13.0-alpine3.12 AS frontend-builder FROM node:16-alpine3.15 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
@ -8,10 +8,10 @@ COPY --from=frontend-builder /app/static /app/frontend/static
COPY . /app COPY . /app
RUN cd /app && packr2 && env CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -a -tags netgo -ldflags '-linkmode external -extldflags -static -s -w' -o ovpn-admin && packr2 clean RUN cd /app && packr2 && env CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -a -tags netgo -ldflags '-linkmode external -extldflags -static -s -w' -o ovpn-admin && packr2 clean
FROM alpine:3.14 FROM alpine:3.16
WORKDIR /app WORKDIR /app
COPY --from=backend-builder /app/ovpn-admin /app COPY --from=backend-builder /app/ovpn-admin /app
RUN apk add --update bash easy-rsa openssl openvpn && \ RUN apk add --update bash easy-rsa openssl openvpn coreutils && \
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/openvpn-user-linux-amd64.tar.gz -O - | tar xz -C /usr/local/bin && \ wget https://github.com/pashcovich/openvpn-user/releases/download/v1.0.4/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.14 FROM alpine:3.16
RUN apk add --update bash openvpn easy-rsa && \ RUN apk add --update bash openvpn easy-rsa iptables && \
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/openvpn-user-linux-amd64.tar.gz -O - | tar xz -C /usr/local/bin && \ wget https://github.com/pashcovich/openvpn-user/releases/download/v1.0.4/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 setup/ /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

@ -8,8 +8,8 @@ Originally created in [Flant](https://flant.com/) for internal needs & used for
## Features ## Features
* Adding OpenVPN users (generating certificates for them); * Adding, deleting OpenVPN users (generating certificates for them);
* Revoking/restoring users certificates; * Revoking/restoring/rotating users certificates;
* Generating ready-to-user config files; * Generating ready-to-user config files;
* Providing metrics for Prometheus, including certificates expiration date, number of (connected/total) users, information about connected users; * Providing metrics for Prometheus, including certificates expiration date, number of (connected/total) users, information about connected users;
* (optionally) Specifying CCD (`client-config-dir`) for each user; * (optionally) Specifying CCD (`client-config-dir`) for each user;
@ -28,15 +28,12 @@ An example of dashboard made using ovpn-admin metrics:
## Installation ## Installation
### Disclaimer
This tool uses external calls for `bash`, `coreutils` and `easy-rsa`, thus **Linux systems only are supported** at the moment.
### 1. Docker ### 1. Docker
There is a ready-to-use [docker-compose.yaml](https://github.com/flant/ovpn-admin/blob/master/docker-compose.yaml), so you can just change/add values you need and start it with [start.sh](https://github.com/flant/ovpn-admin/blob/master/start.sh). There is a ready-to-use [docker-compose.yaml](https://github.com/flant/ovpn-admin/blob/master/docker-compose.yaml), so you can just change/add values you need and start it with [start.sh](https://github.com/flant/ovpn-admin/blob/master/start.sh).
Requirements. You need [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/) installed. Requirements:
You need [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/) installed.
Commands to execute: Commands to execute:
@ -45,6 +42,9 @@ git clone https://github.com/flant/ovpn-admin.git
cd ovpn-admin cd ovpn-admin
./start.sh ./start.sh
``` ```
#### 1.1
Ready docker images available on [Docker Hub](https://hub.docker.com/r/flant/ovpn-admin/tags)
. Tags are simple: `$VERSION` or `latest` for ovpn-admin and `openvpn-$VERSION` or `openvpn-latest` for openvpn-server
### 2. Building from source ### 2. Building from source
@ -65,11 +65,19 @@ cd ovpn-admin
(Please don't forget to configure all needed params in advance.) (Please don't forget to configure all needed params in advance.)
### 3. Prebuilt binary (WIP) ### 3. Prebuilt binary
You can also download and use prebuilt binaries from the [releases](https://github.com/flant/ovpn-admin/releases) page — just choose a relevant tar.gz file. You can also download and use prebuilt binaries from the [releases](https://github.com/flant/ovpn-admin/releases/latest) page — just choose a relevant tar.gz file.
To use password authentication (the `--auth` flag) you have to install [openvpn-user](https://github.com/pashcovich/openvpn-user/releases). This tool should be available in your `$PATH` and its binary should be executable (`+x`).
## Notes
* this tool uses external calls for `bash`, `coreutils` and `easy-rsa`, thus **Linux systems only are supported** at the moment.
* to enable additional password authentication provide `--auth` and `--auth.db="/etc/easyrsa/pki/users.db`" flags and install [openvpn-user](https://github.com/pashcovich/openvpn-user/releases/latest). This tool should be available in your `$PATH` and its binary should be executable (`+x`).
* master-replica synchronization does not work with `--storage.backend=kubernetes.secrets` - **WIP**
* additional password authentication does not work with `--storage.backend=kubernetes.secrets` - **WIP**
* if you use `--ccd` and `--ccd.path="/etc/openvpn/ccd"` abd plan to use static address setup for users do not forget to provide `--ovpn.network="172.16.100.0/24"` with valid openvpn-server network
* tested only with Openvpn-server versions 2.4 and 2.
* status of users connections update every 28 second(*no need to ask why =)*)
## Usage ## Usage
@ -80,88 +88,82 @@ Flags:
--help show context-sensitive help (try also --help-long and --help-man) --help show context-sensitive help (try also --help-long and --help-man)
--listen.host="0.0.0.0" host for ovpn-admin --listen.host="0.0.0.0" host for ovpn-admin
(or $OVPN_LISTEN_HOST) (or OVPN_LISTEN_HOST)
--listen.port="8080" port for ovpn-admin --listen.port="8080" port for ovpn-admin
(or $OVPN_LISTEN_PORT) (or OVPN_LISTEN_PORT)
--role="master" server role, master or slave --role="master" server role, master or slave
(or $OVPN_ROLE) (or OVPN_ROLE)
--master.host="http://127.0.0.1" --master.host="http://127.0.0.1"
(or $OVPN_MASTER_HOST) URL for the master server (or OVPN_MASTER_HOST) URL for the master server
--master.basic-auth.user="" user for master server's Basic Auth --master.basic-auth.user="" user for master server's Basic Auth
(or $OVPN_MASTER_USER) (or OVPN_MASTER_USER)
--master.basic-auth.password="" --master.basic-auth.password=""
(or $OVPN_MASTER_PASSWORD) password for master server's Basic Auth (or OVPN_MASTER_PASSWORD) password for master server's Basic Auth
--master.sync-frequency=600 master host data sync frequency in seconds --master.sync-frequency=600 master host data sync frequency in seconds
(or $OVPN_MASTER_SYNC_FREQUENCY) (or OVPN_MASTER_SYNC_FREQUENCY)
--master.sync-token=TOKEN master host data sync security token --master.sync-token=TOKEN master host data sync security token
(or $OVPN_MASTER_TOKEN) (or OVPN_MASTER_TOKEN)
--ovpn.network="172.16.100.0/24" --ovpn.network="172.16.100.0/24"
(or $OVPN_NETWORK) NETWORK/MASK_PREFIX for OpenVPN server (or OVPN_NETWORK) NETWORK/MASK_PREFIX for OpenVPN server
--ovpn.server=HOST:PORT:PROTOCOL ... --ovpn.server=HOST:PORT:PROTOCOL ...
(or $OVPN_SERVER) HOST:PORT:PROTOCOL for OpenVPN server (or OVPN_SERVER) HOST:PORT:PROTOCOL for OpenVPN server
can have multiple values can have multiple values
--ovpn.server.behindLB enable if your OpenVPN server is behind Kubernetes --ovpn.server.behindLB enable if your OpenVPN server is behind Kubernetes
(or $OVPN_LB) Service having the LoadBalancer type (or OVPN_LB) Service having the LoadBalancer type
--ovpn.service="openvpn-external" --ovpn.service="openvpn-external"
(or $OVPN_LB_SERVICE) the name of Kubernetes Service having the LoadBalancer (or OVPN_LB_SERVICE) the name of Kubernetes Service having the LoadBalancer
type if your OpenVPN server is behind it type if your OpenVPN server is behind it
--mgmt=main=127.0.0.1:8989 ... --mgmt=main=127.0.0.1:8989 ...
(or $OVPN_MGMT) ALIAS=HOST:PORT for OpenVPN server mgmt interface; (or OVPN_MGMT) ALIAS=HOST:PORT for OpenVPN server mgmt interface;
can have multiple values can have multiple values
--metrics.path="/metrics" URL path for exposing collected metrics --metrics.path="/metrics" URL path for exposing collected metrics
(or $OVPN_METRICS_PATH) (or OVPN_METRICS_PATH)
--easyrsa.path="./easyrsa/" path to easyrsa dir --easyrsa.path="./easyrsa/" path to easyrsa dir
(or $EASYRSA_PATH) (or EASYRSA_PATH)
--easyrsa.index-path="./easyrsa/pki/index.txt" --easyrsa.index-path="./easyrsa/pki/index.txt"
(or $OVPN_INDEX_PATH) path to easyrsa index file (or OVPN_INDEX_PATH) path to easyrsa index file
--ccd enable client-config-dir --ccd enable client-config-dir
(or $OVPN_CCD) (or OVPN_CCD)
--ccd.path="./ccd" path to client-config-dir --ccd.path="./ccd" path to client-config-dir
(or $OVPN_CCD_PATH) (or OVPN_CCD_PATH)
--templates.clientconfig-path="" --templates.clientconfig-path=""
(or $OVPN_TEMPLATES_CC_PATH) path to custom client.conf.tpl (or OVPN_TEMPLATES_CC_PATH) path to custom client.conf.tpl
--templates.ccd-path="" path to custom ccd.tpl --templates.ccd-path="" path to custom ccd.tpl
(or $OVPN_TEMPLATES_CCD_PATH) (or OVPN_TEMPLATES_CCD_PATH)
--auth.password enable additional password authorization --auth.password enable additional password authorization
(or $OVPN_AUTH) (or OVPN_AUTH)
--auth.db="./easyrsa/pki/users.db" --auth.db="./easyrsa/pki/users.db"
(or $OVPN_AUTH_DB_PATH) database path for password authorization (or OVPN_AUTH_DB_PATH) database path for password authorization
--debug enable debug mode
(or $OVPN_DEBUG)
--verbose enable verbose mode
(or $OVPN_VERBOSE)
--log.level set log level: trace, debug, info, warn, error (default info) --log.level set log level: trace, debug, info, warn, error (default info)
(or $LOG_LEVEL) (or LOG_LEVEL)
--log.format set log format: text, json (default text) --log.format set log format: text, json (default text)
(or $LOG_FORMAT) (or LOG_FORMAT)
--storage.backend storage backend: filesystem, kubernetes.secrets (default filesystem) --storage.backend storage backend: filesystem, kubernetes.secrets (default filesystem)
(or $STORAGE_BACKEND) (or STORAGE_BACKEND)
--version show application version --version show application version
``` ```

View File

@ -10,6 +10,7 @@ services:
environment: environment:
OVPN_SERVER_NET: "192.168.100.0" OVPN_SERVER_NET: "192.168.100.0"
OVPN_SERVER_MASK: "255.255.255.0" OVPN_SERVER_MASK: "255.255.255.0"
OVPN_PASSWD_AUTH: "true"
cap_add: cap_add:
- NET_ADMIN - NET_ADMIN
ports: ports:
@ -24,12 +25,17 @@ services:
image: ovpn-admin:local image: ovpn-admin:local
command: /app/ovpn-admin command: /app/ovpn-admin
environment: environment:
OVPN_DEBUG: "True" OVPN_DEBUG: "true"
OVPN_VERBOSE: "True" OVPN_VERBOSE: "true"
OVPN_NETWORK: "192.168.100.0/24" OVPN_NETWORK: "192.168.100.0/24"
OVPN_CCD: "true"
OVPN_CCD_PATH: "/mnt/ccd"
EASYRSA_PATH: "/mnt/easyrsa" EASYRSA_PATH: "/mnt/easyrsa"
OVPN_SERVER: "127.0.0.1:7777:tcp" OVPN_SERVER: "127.0.0.1:7777:tcp"
OVPN_INDEX_PATH: "/mnt/easyrsa/pki/index.txt" OVPN_INDEX_PATH: "/mnt/easyrsa/pki/index.txt"
OVPN_AUTH: "true"
OVPN_AUTH_DB_PATH: "/mnt/easyrsa/pki/users.db"
LOG_LEVEL: "debug"
network_mode: service:openvpn network_mode: service:openvpn
volumes: volumes:
- ./easyrsa_master:/mnt/easyrsa - ./easyrsa_master:/mnt/easyrsa

10828
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,14 +10,15 @@
"build": "cross-env NODE_ENV=production webpack --progress" "build": "cross-env NODE_ENV=production webpack --progress"
}, },
"dependencies": { "dependencies": {
"axios": "^0.24.0", "axios": "^0.27.1",
"bootstrap-vue": "^2.21.2", "bootstrap-vue": "^2.22.0",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"vue": "^2.6.14", "vue": "^2.6.14",
"vue-clipboard2": "^0.3.3", "vue-clipboard2": "^0.3.3",
"vue-cookies": "^1.7.4", "vue-cookies": "^1.7.4",
"vue-good-table": "^2.21.11", "vue-good-table": "^2.21.11",
"vue-notification": "^1.3.20" "vue-notification": "^1.3.20",
"vue-style-loader": "^4.1.3"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
@ -38,7 +39,7 @@
"node-sass": "^7.0.1", "node-sass": "^7.0.1",
"sass-loader": "^12.4.0", "sass-loader": "^12.4.0",
"terser-webpack-plugin": "^5.3.0", "terser-webpack-plugin": "^5.3.0",
"vue-loader": "^15.9.8", "vue-loader": "^17.0.0",
"vue-template-compiler": "^2.6.14", "vue-template-compiler": "^2.6.14",
"webpack": "^5.65.0", "webpack": "^5.65.0",
"webpack-cli": "^4.9.1", "webpack-cli": "^4.9.1",

View File

@ -57,8 +57,8 @@ new Vue({
filterable: true, filterable: true,
}, },
{ {
label: 'Connection Server', label: 'Active Connections',
field: 'ConnectionServer', field: 'Connections',
filterable: true, filterable: true,
}, },
{ {
@ -107,6 +107,38 @@ new Vue({
showForServerRole: ['master'], showForServerRole: ['master'],
showForModule: ["core"], showForModule: ["core"],
}, },
{
name: 'u-delete',
label: 'Delete',
class: 'btn-danger',
showWhenStatus: 'Revoked',
showForServerRole: ['master'],
showForModule: ["core"],
},
{
name: 'u-delete',
label: 'Delete',
class: 'btn-danger',
showWhenStatus: 'Expired',
showForServerRole: ['master'],
showForModule: ["core"],
},
{
name: 'u-rotate',
label: 'Rotate',
class: 'btn-warning',
showWhenStatus: 'Revoked',
showForServerRole: ['master'],
showForModule: ["core"],
},
{
name: 'u-rotate',
label: 'Rotate',
class: 'btn-warning',
showWhenStatus: 'Expired',
showForServerRole: ['master'],
showForModule: ["core"],
},
{ {
name: 'u-unrevoke', name: 'u-unrevoke',
label: 'Unrevoke', label: 'Unrevoke',
@ -161,10 +193,14 @@ new Vue({
newPassword: '', newPassword: '',
passwordChangeStatus: '', passwordChangeStatus: '',
passwordChangeMessage: '', passwordChangeMessage: '',
rotateUserMessage: '',
deleteUserMessage: '',
modalNewUserVisible: false, modalNewUserVisible: false,
modalShowConfigVisible: false, modalShowConfigVisible: false,
modalShowCcdVisible: false, modalShowCcdVisible: false,
modalChangePasswordVisible: false, modalChangePasswordVisible: false,
modalRotateUserVisible: false,
modalDeleteUserVisible: false,
openvpnConfig: '', openvpnConfig: '',
ccd: { ccd: {
Name: '', Name: '',
@ -204,6 +240,16 @@ new Vue({
_this.$notify({title: 'User ' + _this.username + ' unrevoked!', type: 'success'}) _this.$notify({title: 'User ' + _this.username + ' unrevoked!', type: 'success'})
}); });
}) })
_this.$root.$on('u-rotate', function () {
_this.u.modalRotateUserVisible = true;
var data = new URLSearchParams();
data.append('username', _this.username);
})
_this.$root.$on('u-delete', function () {
_this.u.modalDeleteUserVisible = true;
var data = new URLSearchParams();
data.append('username', _this.username);
})
_this.$root.$on('u-show-config', function () { _this.$root.$on('u-show-config', function () {
_this.u.modalShowConfigVisible = true; _this.u.modalShowConfigVisible = true;
var data = new URLSearchParams(); var data = new URLSearchParams();
@ -251,8 +297,8 @@ new Vue({
}) })
}, },
computed: { computed: {
customAddressDisabled: function () { customAddressDynamic: function () {
return this.serverRole == "master" ? this.u.ccd.ClientAddress == "dynamic" : true return this.u.ccd.ClientAddress == "dynamic"
}, },
ccdApplyStatusCssClass: function () { ccdApplyStatusCssClass: function () {
return this.u.ccdApplyStatus == 200 ? "alert-success" : "alert-danger" return this.u.ccdApplyStatus == 200 ? "alert-success" : "alert-danger"
@ -260,6 +306,12 @@ new Vue({
passwordChangeStatusCssClass: function () { passwordChangeStatusCssClass: function () {
return this.u.passwordChangeStatus == 200 ? "alert-success" : "alert-danger" return this.u.passwordChangeStatus == 200 ? "alert-success" : "alert-danger"
}, },
userRotateStatusCssClass: function () {
return this.u.roatateUserStatus == 200 ? "alert-success" : "alert-danger"
},
deleteUserStatusCssClass: function () {
return this.u.deleteUserStatus == 200 ? "alert-success" : "alert-danger"
},
modalNewUserDisplay: function () { modalNewUserDisplay: function () {
return this.u.modalNewUserVisible ? {display: 'flex'} : {} return this.u.modalNewUserVisible ? {display: 'flex'} : {}
}, },
@ -272,6 +324,12 @@ new Vue({
modalChangePasswordDisplay: function () { modalChangePasswordDisplay: function () {
return this.u.modalChangePasswordVisible ? {display: 'flex'} : {} return this.u.modalChangePasswordVisible ? {display: 'flex'} : {}
}, },
modalRotateUserDisplay: function () {
return this.u.modalRotateUserVisible ? {display: 'flex'} : {}
},
modalDeleteUserDisplay: function () {
return this.u.modalDeleteUserVisible ? {display: 'flex'} : {}
},
revokeFilterText: function() { revokeFilterText: function() {
return this.filters.hideRevoked ? "Show revoked" : "Hide revoked" return this.filters.hideRevoked ? "Show revoked" : "Hide revoked"
}, },
@ -288,7 +346,16 @@ new Vue({
}, },
methods: { methods: {
rowStyleClassFn: function(row) { rowStyleClassFn: function(row) {
return row.ConnectionStatus == 'Connected' ? 'connected-user' : '' if (row.ConnectionStatus == 'Connected') {
return 'connected-user'
}
if (row.AccountStatus == 'Revoked') {
return 'revoked-user'
}
if (row.AccountStatus == 'Expired') {
return 'expired-user'
}
return ''
}, },
rowActionFn: function(e) { rowActionFn: function(e) {
this.username = e.target.dataset.username; this.username = e.target.dataset.username;
@ -302,14 +369,6 @@ new Vue({
}); });
}, },
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";
},
getServerSetting: function() { getServerSetting: function() {
var _this = this; var _this = this;
axios.request(axios_cfg('api/server/settings')) axios.request(axios_cfg('api/server/settings'))
@ -394,6 +453,52 @@ new Vue({
_this.$notify({title: 'Changing password for user ' + _this.username + ' failed!', type: 'error'}) _this.$notify({title: 'Changing password for user ' + _this.username + ' failed!', type: 'error'})
}); });
}, },
rotateUser: function(user) {
var _this = this;
_this.u.rotateUserMessage = "";
var data = new URLSearchParams();
data.append('username', user);
data.append('password', _this.u.newPassword);
axios.request(axios_cfg('api/user/rotate', data, 'form'))
.then(function(response) {
_this.u.roatateUserStatus = 200;
_this.u.newPassword = '';
_this.getUserData();
_this.u.modalRotateUserVisible = false;
_this.$notify({title: 'Certificates for user ' + _this.username + ' rotated!', type: 'success'})
})
.catch(function(error) {
_this.u.roatateUserStatus = error.response.status;
_this.u.rotateUserMessage = error.response.data.message;
_this.$notify({title: 'Rotate certificates for user ' + _this.username + ' failed!', type: 'error'})
})
},
deleteUser: function(user) {
var _this = this;
_this.u.deleteUserMessage = "";
var data = new URLSearchParams();
data.append('username', user);
axios.request(axios_cfg('api/user/delete', data, 'form'))
.then(function(response) {
_this.u.deleteUserStatus = 200;
_this.u.newPassword = '';
_this.getUserData();
_this.u.modalDeleteUserVisible = false;
_this.$notify({title: 'User ' + _this.username + ' deleted!', type: 'success'})
})
.catch(function(error) {
_this.u.deleteUserStatus = error.response.status;
_this.u.deleteUserMessage = error.response.data.message;
_this.$notify({title: 'Deleting user ' + _this.username + ' failed!', type: 'error'})
})
},
} }
}) })

View File

@ -48,6 +48,14 @@ body {
background-color: rgba(162, 245, 169, 0.5); background-color: rgba(162, 245, 169, 0.5);
} }
.revoked-user {
background-color: rgba(198, 186, 186, 0.5);
}
.expired-user {
background-color: rgba(255, 220, 127, 0.5);
}
.new-user-btn { .new-user-btn {
margin-right: 2rem; margin-right: 2rem;
} }

View File

@ -42,10 +42,6 @@
</template> </template>
</vue-good-table> </vue-good-table>
<!-- <div class="d-flex justify-content-md-end">-->
<!-- <button type="button" class="btn btn-sm btn-success el-square new-user-btn" v-on:click.stop="u.ctxVisible=false;u.modalNewUserVisible=true">Add user</button>-->
<!-- </div>-->
<div class="modal-wrapper" v-if="u.modalNewUserVisible" v-bind:style="modalNewUserDisplay"> <div class="modal-wrapper" v-if="u.modalNewUserVisible" v-bind:style="modalNewUserDisplay">
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">
@ -123,12 +119,10 @@
<div class="modal-body"> <div class="modal-body">
<div class="input-group"> <div class="input-group">
<h5 class="static-address-label ">Static address:</h5> <h5 class="static-address-label ">Static address:</h5>
<div class="input-group-prepend"> <input id="static-address" type="text" class="form-control" v-model="u.ccd.ClientAddress" placeholder="127.0.0.1">
<div class="input-group-text"> <div class="input-group-append">
<input id="enable-static" type="checkbox" @change="staticAddrCheckboxOnChange()" v-if="serverRole == 'master'" v-bind:checked="!customAddressDisabled"> <button id="static-address-clear" class="btn btn-warning" type="button" v-on:click="u.ccd.ClientAddress = 'dynamic'" v-if="serverRole == 'master'" v-bind:disabled="customAddressDynamic">Clear</button>
</div>
</div> </div>
<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">
@ -191,6 +185,50 @@
</div> </div>
</div> </div>
<div class="modal-wrapper" v-if="u.modalRotateUserVisible" v-bind:style="modalRotateUserDisplay">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4>Confirm rotating certificates for user: <strong>{{ username }}</strong></h4>
</div>
<div class="modal-body" v-if="modulesEnabled.includes('passwdAuth')">
<h4>Enter new password:</h4>
<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.rotateUserMessage.length > 0">
<div class="alert" v-bind:class="userRotateStatusCssClass" role="alert" >
{{ u.rotateUserMessage }}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger el-square modal-el-margin" v-on:click.stop="rotateUser(username)">Rotate</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.rotateUserMessage='';u.modalRotateUserVisible=false">Close</button>
</div>
</div>
</div>
</div>
<div class="modal-wrapper" v-if="u.modalDeleteUserVisible" v-bind:style="modalDeleteUserDisplay">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4>Confirm deleting user: <strong>{{ username }}</strong></h4>
</div>
<div class="modal-footer justify-content-center" v-if="u.deleteUserMessage.length > 0">
<div class="alert" v-bind:class="deleteUserStatusCssClass" role="alert" >
{{ u.deleteUserMessage }}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger el-square modal-el-margin" v-on:click.stop="deleteUser(username)">Delete</button>
<button type="button" class="btn btn-primary el-square d-flex justify-content-sm-end modal-el-margin" v-on:click.stop="u.deleteUserMessage='';u.modalDeleteUserVisible=false">Close</button>
</div>
</div>
</div>
</div>
<notifications position="bottom left" :speed="900" /> <notifications position="bottom left" :speed="900" />
</div> </div>
<script src="dist/bundle.min.js"></script> <script src="dist/bundle.min.js"></script>

View File

@ -1,55 +1,50 @@
// Generated using webpack-cli https://github.com/webpack/webpack-cli
const path = require('path'); const path = require('path');
//const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const isProduction = process.env.NODE_ENV == 'production'; module.exports = {
mode: 'production',
const config = {
entry: { entry: {
bundle: [ bundle: [
'./src/main.js', './src/main.js',
], ],
style: [ style: [
'./src/style.js', './src/style.js',
] ]
}, },
output: { output: {
path: path.resolve(__dirname, './static/dist'), path: path.resolve(__dirname, './static/dist'),
publicPath: '/dist/', publicPath: '/dist/',
filename: '[name].min.js' filename: '[name].min.js'
}, },
plugins: [ plugins: [
//new BundleAnalyzerPlugin(),
], ],
module: { module: {
rules: [ rules: [
{ {
test: /\.css$/, test: /\.css$/,
use: [ use: [
'vue-style-loader', 'vue-style-loader',
'css-loader' 'css-loader'
], ],
}, },
{ {
test: /\.js$/, test: /\.js$/,
exclude: /node_modules/, //exclude: /node_modules\/(?!bootstrap-vue\/src\/)/,
loader: 'babel-loader', exclude: /node_modules/,
options: { loader: 'babel-loader',
presets: ['@babel/preset-env'] options: {
} presets: ['@babel/preset-env']
}, }
], },
],
}, },
resolve: { resolve: {
alias: { alias: {
'vue$': 'vue/dist/vue.esm.js' 'vue$': 'vue/dist/vue.esm.js',
//'bootstrap-vue$': 'bootstrap-vue/src/index.js'
}, },
extensions: ['*', '.js', '.vue', '.json'] extensions: ['*', '.js', '.vue', '.json']
}, },
}; }
module.exports = () => {
config.mode = 'production';
return config;
};

4
go.mod
View File

@ -8,6 +8,9 @@ require (
gopkg.in/alecthomas/kingpin.v2 v2.2.6 gopkg.in/alecthomas/kingpin.v2 v2.2.6
k8s.io/apimachinery v0.23.1 k8s.io/apimachinery v0.23.1
k8s.io/client-go v0.23.1 k8s.io/client-go v0.23.1
)
require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
@ -22,6 +25,7 @@ require (
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect github.com/google/go-cmp v0.5.6 // indirect
github.com/google/gofuzz v1.2.0 // indirect github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect github.com/googleapis/gnostic v0.5.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/karrick/godirwalk v1.16.1 // indirect github.com/karrick/godirwalk v1.16.1 // indirect

2
go.sum
View File

@ -263,6 +263,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=

View File

@ -1,17 +1,22 @@
package main package main
import ( import (
"archive/tar"
"compress/gzip"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"strings"
"time" "time"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
func parseDate(layout,datetime string) time.Time { func parseDate(layout, datetime string) time.Time {
t, err := time.Parse(layout, datetime) t, err := time.Parse(layout, datetime)
if err != nil { if err != nil {
log.Errorln(err) log.Errorln(err)
@ -19,11 +24,11 @@ func parseDate(layout,datetime string) time.Time {
return t return t
} }
func parseDateToString(layout,datetime,format string) string { func parseDateToString(layout, datetime, format string) string {
return parseDate(layout, datetime).Format(format) return parseDate(layout, datetime).Format(format)
} }
func parseDateToUnix(layout,datetime string) int64 { func parseDateToUnix(layout, datetime string) int64 {
return parseDate(layout, datetime).Unix() return parseDate(layout, datetime).Unix()
} }
@ -32,7 +37,7 @@ func runBash(script string) string {
cmd := exec.Command("bash", "-c", script) cmd := exec.Command("bash", "-c", script)
stdout, err := cmd.CombinedOutput() stdout, err := cmd.CombinedOutput()
if err != nil { if err != nil {
return (fmt.Sprint(err) + " : " + string(stdout)) return fmt.Sprint(err) + " : " + string(stdout)
} }
return string(stdout) return string(stdout)
} }
@ -43,7 +48,7 @@ func fExist(path string) bool {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return false return false
} else if err != nil { } else if err != nil {
log.Fatal(err) log.Fatalf("fExist: %s", err)
return false return false
} }
@ -53,37 +58,102 @@ func fExist(path string) bool {
func fRead(path string) string { func fRead(path string) string {
content, err := ioutil.ReadFile(path) content, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
log.Fatal(err) log.Warning(err)
return ""
} }
return string(content) return string(content)
} }
func fCreate(path string) bool { func fCreate(path string) error {
var _, err = os.Stat(path) var _, err = os.Stat(path)
if os.IsNotExist(err) { if os.IsNotExist(err) {
var file, err = os.Create(path) var file, err = os.Create(path)
if err != nil { if err != nil {
log.Errorln(err) log.Errorln(err)
return false return err
} }
defer file.Close() defer file.Close()
} }
return true return nil
} }
func fWrite(path, content string) { func fWrite(path, content string) error {
err := ioutil.WriteFile(path, []byte(content), 0644) err := ioutil.WriteFile(path, []byte(content), 0644)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
return nil
} }
func fDelete(path string) { func fDelete(path string) error {
err := os.Remove(path) err := os.Remove(path)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
return nil
}
func fCopy(src, dst string) error {
sfi, err := os.Stat(src)
if err != nil {
return err
}
if !sfi.Mode().IsRegular() {
// cannot copy non-regular files (e.g., directories, symlinks, devices, etc.)
return fmt.Errorf("fCopy: non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String())
}
dfi, err := os.Stat(dst)
if err != nil {
if !os.IsNotExist(err) {
return err
}
} else {
if !(dfi.Mode().IsRegular()) {
return fmt.Errorf("fCopy: non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String())
}
if os.SameFile(sfi, dfi) {
return err
}
}
if err = os.Link(src, dst); err == nil {
return err
}
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer func() {
cerr := out.Close()
if err == nil {
err = cerr
}
}()
if _, err = io.Copy(out, in); err != nil {
return err
}
err = out.Sync()
return err
}
func fMove(src, dst string) error {
err := fCopy(src, dst)
if err != nil {
log.Warn(err)
return err
}
err = fDelete(src)
if err != nil {
log.Warn(err)
return err
}
return nil
} }
func fDownload(path, url string, basicAuth bool) error { func fDownload(path, url string, basicAuth bool) error {
@ -99,7 +169,7 @@ func fDownload(path, url string, basicAuth bool) error {
} }
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
log.Warnf("WARNING: Download file operation for url %s finished with status code %d\n", url, resp.StatusCode ) log.Warnf("WARNING: Download file operation for url %s finished with status code %d\n", url, resp.StatusCode)
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -113,3 +183,124 @@ func fDownload(path, url string, basicAuth bool) error {
return nil return nil
} }
func createArchiveFromDir(dir, path string) error {
var files []string
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Warn(err)
return err
}
if !info.IsDir() {
files = append(files, path)
}
return nil
})
if err != nil {
log.Warn(err)
}
out, err := os.Create(path)
if err != nil {
log.Errorf("Error writing archive %s: %s", path, err)
return err
}
defer out.Close()
gw := gzip.NewWriter(out)
defer gw.Close()
tw := tar.NewWriter(gw)
defer tw.Close()
// Iterate over files and add them to the tar archive
for _, filePath := range files {
file, err := os.Open(filePath)
if err != nil {
log.Warnf("Error writing archive %s: %s", path, err)
return err
}
// Get FileInfo about our file providing file size, mode, etc.
info, err := file.Stat()
if err != nil {
file.Close()
return err
}
// Create a tar Header from the FileInfo data
header, err := tar.FileInfoHeader(info, info.Name())
if err != nil {
file.Close()
return err
}
header.Name = strings.Replace(filePath, dir+"/", "", 1)
// Write file header to the tar archive
err = tw.WriteHeader(header)
if err != nil {
file.Close()
return err
}
// Copy file content to tar archive
_, err = io.Copy(tw, file)
if err != nil {
file.Close()
return err
}
file.Close()
}
return nil
}
func extractFromArchive(archive, path string) error {
// Open the file which will be written into the archive
file, err := os.Open(archive)
if err != nil {
return err
}
defer file.Close()
// Write file header to the tar archive
uncompressedStream, err := gzip.NewReader(file)
if err != nil {
log.Fatal("extractFromArchive(): NewReader failed")
}
tarReader := tar.NewReader(uncompressedStream)
for true {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("extractFromArchive: Next() failed: %s", err.Error())
}
switch header.Typeflag {
case tar.TypeDir:
if err := os.Mkdir(path+"/"+header.Name, 0755); err != nil {
log.Fatalf("extractFromArchive: Mkdir() failed: %s", err.Error())
}
case tar.TypeReg:
outFile, err := os.Create(path + "/" + header.Name)
if err != nil {
log.Fatalf("extractFromArchive: Create() failed: %s", err.Error())
}
if _, err := io.Copy(outFile, tarReader); err != nil {
log.Fatalf("extractFromArchive: Copy() failed: %s", err.Error())
}
outFile.Close()
default:
log.Fatalf(
"extractFromArchive: uknown type: %s in %s", header.Typeflag, header.Name)
}
}
return nil
}

View File

@ -7,9 +7,11 @@ import (
"crypto/x509" "crypto/x509"
"errors" "errors"
"fmt" "fmt"
"github.com/google/uuid"
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec" "os/exec"
"strings"
"time" "time"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -22,7 +24,7 @@ import (
const ( const (
secretCA = "openvpn-pki-ca" secretCA = "openvpn-pki-ca"
secretServer = "openvpn-pki-server" secretServer = "openvpn-pki-server"
secretClientTmpl = "openvpn-pki-%s" secretClientTmpl = "openvpn-pki-%d"
secretCRL = "openvpn-pki-crl" secretCRL = "openvpn-pki-crl"
secretIndexTxt = "openvpn-pki-index-txt" secretIndexTxt = "openvpn-pki-index-txt"
secretDHandTA = "openvpn-pki-dh-and-ta" secretDHandTA = "openvpn-pki-dh-and-ta"
@ -228,11 +230,11 @@ func (openVPNPKI *OpenVPNPKI) indexTxtUpdate() (err error) {
log.Trace(cert.Subject.CommonName) log.Trace(cert.Subject.CommonName)
if secret.Annotations["revokedAt"] == "" { if secret.Annotations["revokedAt"] == "" {
indexTxt += fmt.Sprintf("%s\t%s\t\t%s\t%s\t%s\n", "V", cert.NotAfter.Format(indexTxtDateFormat), cert.SerialNumber.String(), "unknown", "/CN="+cert.Subject.CommonName) indexTxt += fmt.Sprintf("%s\t%s\t\t%s\t%s\t%s\n", "V", cert.NotAfter.Format(indexTxtDateFormat), fmt.Sprintf("%d", cert.SerialNumber), "unknown", "/CN="+secret.Annotations["name"])
} else if cert.NotAfter.Before(time.Now()) { } else if cert.NotAfter.Before(time.Now()) {
indexTxt += fmt.Sprintf("%s\t%s\t\t%s\t%s\t%s\n", "E", cert.NotAfter.Format(indexTxtDateFormat), cert.SerialNumber.String(), "unknown", "/CN="+cert.Subject.CommonName) indexTxt += fmt.Sprintf("%s\t%s\t\t%s\t%s\t%s\n", "E", cert.NotAfter.Format(indexTxtDateFormat), fmt.Sprintf("%d", cert.SerialNumber), "unknown", "/CN="+secret.Annotations["name"])
} else { } else {
indexTxt += fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%s\n", "R", cert.NotAfter.Format(indexTxtDateFormat), secret.Annotations["revokedAt"], cert.SerialNumber.String(), "unknown", "/CN="+cert.Subject.CommonName) indexTxt += fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%s\n", "R", cert.NotAfter.Format(indexTxtDateFormat), secret.Annotations["revokedAt"], fmt.Sprintf("%d", cert.SerialNumber), "unknown", "/CN="+secret.Annotations["name"])
} }
} }
@ -336,7 +338,7 @@ func (openVPNPKI *OpenVPNPKI) easyrsaBuildClient(commonName string) (err error)
"notBefore": clientCert.NotBefore.Format(indexTxtDateFormat), "notBefore": clientCert.NotBefore.Format(indexTxtDateFormat),
"notAfter": clientCert.NotAfter.Format(indexTxtDateFormat), "notAfter": clientCert.NotAfter.Format(indexTxtDateFormat),
"revokedAt": "", "revokedAt": "",
"serialNumber": clientCert.SerialNumber.String(), "serialNumber": fmt.Sprintf("%d", clientCert.SerialNumber),
}, },
} }
@ -447,6 +449,78 @@ func (openVPNPKI *OpenVPNPKI) easyrsaUnrevoke(commonName string) (err error) {
return return
} }
func (openVPNPKI *OpenVPNPKI) easyrsaRotate(commonName, newPassword string) (err error) {
secret, err := openVPNPKI.secretGetByLabels("name=" + commonName)
if err != nil {
log.Error(err)
}
uniqHash := strings.Replace(uuid.New().String(), "-", "", -1)
secret.Annotations["commonName"] = "REVOKED-" + commonName + "-" + uniqHash
secret.Labels["name"] = "REVOKED" + commonName
secret.Labels["revokedForever"] = "true"
_, err = openVPNPKI.KubeClient.CoreV1().Secrets(namespace).Update(context.TODO(), secret, metav1.UpdateOptions{})
if err != nil {
return
}
err = openVPNPKI.easyrsaBuildClient(commonName)
if err != nil {
return
}
err = openVPNPKI.indexTxtUpdate()
if err != nil {
return
}
err = openVPNPKI.updateIndexTxtOnDisk()
if err != nil {
return
}
err = openVPNPKI.easyrsaGenCRL()
if err != nil {
log.Error(err)
}
err = openVPNPKI.updateCRLOnDisk()
return
}
func (openVPNPKI *OpenVPNPKI) easyrsaDelete(commonName string) (err error) {
secret, err := openVPNPKI.secretGetByLabels("name=" + commonName)
if err != nil {
log.Error(err)
}
uniqHash := strings.Replace(uuid.New().String(), "-", "", -1)
secret.Annotations["commonName"] = "REVOKED-" + commonName + "-" + uniqHash
secret.Labels["name"] = "REVOKED-" + commonName + "-" + uniqHash
secret.Labels["revokedForever"] = "true"
_, err = openVPNPKI.KubeClient.CoreV1().Secrets(namespace).Update(context.TODO(), secret, metav1.UpdateOptions{})
if err != nil {
return
}
err = openVPNPKI.indexTxtUpdate()
if err != nil {
return
}
err = openVPNPKI.updateIndexTxtOnDisk()
if err != nil {
return
}
err = openVPNPKI.easyrsaGenCRL()
if err != nil {
log.Error(err)
}
err = openVPNPKI.updateCRLOnDisk()
return
}
func (openVPNPKI *OpenVPNPKI) secretGetClientCert(name string) (cert ClientCert, err error) { func (openVPNPKI *OpenVPNPKI) secretGetClientCert(name string) (cert ClientCert, err error) {
secret, err := openVPNPKI.secretGetByName(name) secret, err := openVPNPKI.secretGetByName(name)
if err != nil { if err != nil {

558
main.go
View File

@ -7,20 +7,23 @@ import (
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"encoding/pem" "encoding/pem"
"errors"
"fmt" "fmt"
"github.com/google/uuid"
"io/ioutil" "io/ioutil"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"net" "net"
"net/http" "net/http"
"os" "os"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"sync"
"text/template" "text/template"
"time" "time"
"unicode/utf8"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"github.com/gobuffalo/packr/v2" "github.com/gobuffalo/packr/v2"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
@ -73,7 +76,7 @@ var (
certsArchivePath = "/tmp/" + certsArchiveFileName certsArchivePath = "/tmp/" + certsArchiveFileName
ccdArchivePath = "/tmp/" + ccdArchiveFileName ccdArchivePath = "/tmp/" + ccdArchiveFileName
version = "1.7.5" version = "2.0.0"
) )
var logLevels = map[string]log.Level{ var logLevels = map[string]log.Level{
@ -122,7 +125,13 @@ var (
ovpnClientsConnected = prometheus.NewGauge(prometheus.GaugeOpts{ ovpnClientsConnected = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "ovpn_clients_connected", Name: "ovpn_clients_connected",
Help: "connected openvpn users", Help: "total connected openvpn clients",
},
)
ovpnUniqClientsConnected = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "ovpn_uniq_clients_connected",
Help: "uniq connected openvpn clients",
}, },
) )
@ -175,6 +184,7 @@ type OvpnAdmin struct {
templates *packr.Box templates *packr.Box
modules []string modules []string
mgmtStatusTimeFormat string mgmtStatusTimeFormat string
createUserMutex *sync.Mutex
} }
type OpenvpnServer struct { type OpenvpnServer struct {
@ -198,7 +208,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"` Connections int `json:"Connections"`
} }
type ccdRoute struct { type ccdRoute struct {
@ -237,20 +247,20 @@ type clientStatus struct {
} }
func (oAdmin *OvpnAdmin) userListHandler(w http.ResponseWriter, r *http.Request) { func (oAdmin *OvpnAdmin) userListHandler(w http.ResponseWriter, r *http.Request) {
log.Info(r.RemoteAddr, r.RequestURI) log.Info(r.RemoteAddr, " ", r.RequestURI)
usersList, _ := json.Marshal(oAdmin.clients) usersList, _ := json.Marshal(oAdmin.clients)
fmt.Fprintf(w, "%s", usersList) fmt.Fprintf(w, "%s", usersList)
} }
func (oAdmin *OvpnAdmin) userStatisticHandler(w http.ResponseWriter, r *http.Request) { func (oAdmin *OvpnAdmin) userStatisticHandler(w http.ResponseWriter, r *http.Request) {
log.Info(r.RemoteAddr, r.RequestURI) log.Info(r.RemoteAddr, " ", r.RequestURI)
_ = r.ParseForm() _ = r.ParseForm()
userStatistic, _ := json.Marshal(oAdmin.getUserStatistic(r.FormValue("username"))) userStatistic, _ := json.Marshal(oAdmin.getUserStatistic(r.FormValue("username")))
fmt.Fprintf(w, "%s", userStatistic) fmt.Fprintf(w, "%s", userStatistic)
} }
func (oAdmin *OvpnAdmin) userCreateHandler(w http.ResponseWriter, r *http.Request) { func (oAdmin *OvpnAdmin) userCreateHandler(w http.ResponseWriter, r *http.Request) {
log.Info(r.RemoteAddr, r.RequestURI) log.Info(r.RemoteAddr, " ", r.RequestURI)
if oAdmin.role == "slave" { if oAdmin.role == "slave" {
http.Error(w, `{"status":"error"}`, http.StatusLocked) http.Error(w, `{"status":"error"}`, http.StatusLocked)
return return
@ -259,6 +269,7 @@ func (oAdmin *OvpnAdmin) userCreateHandler(w http.ResponseWriter, r *http.Reques
userCreated, userCreateStatus := oAdmin.userCreate(r.FormValue("username"), r.FormValue("password")) userCreated, userCreateStatus := oAdmin.userCreate(r.FormValue("username"), r.FormValue("password"))
if userCreated { if userCreated {
oAdmin.clients = oAdmin.usersList()
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, userCreateStatus) fmt.Fprintf(w, userCreateStatus)
return return
@ -266,41 +277,82 @@ func (oAdmin *OvpnAdmin) userCreateHandler(w http.ResponseWriter, r *http.Reques
http.Error(w, userCreateStatus, http.StatusUnprocessableEntity) http.Error(w, userCreateStatus, http.StatusUnprocessableEntity)
} }
} }
func (oAdmin *OvpnAdmin) userRotateHandler(w http.ResponseWriter, r *http.Request) {
func (oAdmin *OvpnAdmin) userRevokeHandler(w http.ResponseWriter, r *http.Request) { log.Info(r.RemoteAddr, " ", r.RequestURI)
log.Info(r.RemoteAddr, r.RequestURI)
if oAdmin.role == "slave" { if oAdmin.role == "slave" {
http.Error(w, `{"status":"error"}`, http.StatusLocked) http.Error(w, `{"status":"error"}`, http.StatusLocked)
return return
} }
_ = r.ParseForm() _ = r.ParseForm()
fmt.Fprintf(w, "%s", oAdmin.userRevoke(r.FormValue("username"))) err, msg := oAdmin.userRotate(r.FormValue("username"), r.FormValue("password"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
} else {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, msg)
}
}
func (oAdmin *OvpnAdmin) userDeleteHandler(w http.ResponseWriter, r *http.Request) {
log.Info(r.RemoteAddr, " ", r.RequestURI)
if oAdmin.role == "slave" {
http.Error(w, `{"status":"error"}`, http.StatusLocked)
return
}
_ = r.ParseForm()
err, msg := oAdmin.userDelete(r.FormValue("username"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
} else {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, msg)
}
}
func (oAdmin *OvpnAdmin) userRevokeHandler(w http.ResponseWriter, r *http.Request) {
log.Info(r.RemoteAddr, " ", r.RequestURI)
if oAdmin.role == "slave" {
http.Error(w, `{"status":"error"}`, http.StatusLocked)
return
}
_ = r.ParseForm()
err, msg := oAdmin.userRevoke(r.FormValue("username"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
} else {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, msg)
}
} }
func (oAdmin *OvpnAdmin) userUnrevokeHandler(w http.ResponseWriter, r *http.Request) { func (oAdmin *OvpnAdmin) userUnrevokeHandler(w http.ResponseWriter, r *http.Request) {
log.Info(r.RemoteAddr, r.RequestURI) log.Info(r.RemoteAddr, " ", r.RequestURI)
if oAdmin.role == "slave" { if oAdmin.role == "slave" {
http.Error(w, `{"status":"error"}`, http.StatusLocked) http.Error(w, `{"status":"error"}`, http.StatusLocked)
return return
} }
_ = r.ParseForm() _ = r.ParseForm()
fmt.Fprintf(w, "%s", oAdmin.userUnrevoke(r.FormValue("username"))) err, msg := oAdmin.userUnrevoke(r.FormValue("username"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
} else {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, msg)
}
} }
func (oAdmin *OvpnAdmin) userChangePasswordHandler(w http.ResponseWriter, r *http.Request) { func (oAdmin *OvpnAdmin) userChangePasswordHandler(w http.ResponseWriter, r *http.Request) {
log.Info(r.RemoteAddr, r.RequestURI) log.Info(r.RemoteAddr, " ", r.RequestURI)
_ = r.ParseForm() _ = r.ParseForm()
if *authByPassword { if *authByPassword {
passwordChanged, passwordChangeMessage := oAdmin.userChangePassword(r.FormValue("username"), r.FormValue("password")) err, msg := oAdmin.userChangePassword(r.FormValue("username"), r.FormValue("password"))
if passwordChanged { if err != nil {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{"status":"ok", "message": "%s"}`, passwordChangeMessage)
return
} else {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, `{"status":"error", "message": "%s"}`, passwordChangeMessage) fmt.Fprintf(w, `{"status":"error", "message": "%s"}`, msg)
return
} else {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{"status":"ok", "message": "%s"}`, msg)
} }
} else { } else {
http.Error(w, `{"status":"error"}`, http.StatusNotImplemented) http.Error(w, `{"status":"error"}`, http.StatusNotImplemented)
@ -309,27 +361,27 @@ func (oAdmin *OvpnAdmin) userChangePasswordHandler(w http.ResponseWriter, r *htt
} }
func (oAdmin *OvpnAdmin) userShowConfigHandler(w http.ResponseWriter, r *http.Request) { func (oAdmin *OvpnAdmin) userShowConfigHandler(w http.ResponseWriter, r *http.Request) {
log.Info(r.RemoteAddr, r.RequestURI) log.Info(r.RemoteAddr, " ", r.RequestURI)
_ = r.ParseForm() _ = r.ParseForm()
fmt.Fprintf(w, "%s", oAdmin.renderClientConfig(r.FormValue("username"))) fmt.Fprintf(w, "%s", oAdmin.renderClientConfig(r.FormValue("username")))
} }
func (oAdmin *OvpnAdmin) userDisconnectHandler(w http.ResponseWriter, r *http.Request) { func (oAdmin *OvpnAdmin) userDisconnectHandler(w http.ResponseWriter, r *http.Request) {
log.Info(r.RemoteAddr, r.RequestURI) log.Info(r.RemoteAddr, " ", r.RequestURI)
_ = r.ParseForm() _ = r.ParseForm()
// fmt.Fprintf(w, "%s", userDisconnect(r.FormValue("username"))) // fmt.Fprintf(w, "%s", userDisconnect(r.FormValue("username")))
fmt.Fprintf(w, "%s", r.FormValue("username")) fmt.Fprintf(w, "%s", r.FormValue("username"))
} }
func (oAdmin *OvpnAdmin) userShowCcdHandler(w http.ResponseWriter, r *http.Request) { func (oAdmin *OvpnAdmin) userShowCcdHandler(w http.ResponseWriter, r *http.Request) {
log.Info(r.RemoteAddr, r.RequestURI) log.Info(r.RemoteAddr, " ", r.RequestURI)
_ = r.ParseForm() _ = r.ParseForm()
ccd, _ := json.Marshal(oAdmin.getCcd(r.FormValue("username"))) ccd, _ := json.Marshal(oAdmin.getCcd(r.FormValue("username")))
fmt.Fprintf(w, "%s", ccd) fmt.Fprintf(w, "%s", ccd)
} }
func (oAdmin *OvpnAdmin) userApplyCcdHandler(w http.ResponseWriter, r *http.Request) { func (oAdmin *OvpnAdmin) userApplyCcdHandler(w http.ResponseWriter, r *http.Request) {
log.Info(r.RemoteAddr, r.RequestURI) log.Info(r.RemoteAddr, " ", r.RequestURI)
if oAdmin.role == "slave" { if oAdmin.role == "slave" {
http.Error(w, `{"status":"error"}`, http.StatusLocked) http.Error(w, `{"status":"error"}`, http.StatusLocked)
return return
@ -357,7 +409,7 @@ func (oAdmin *OvpnAdmin) userApplyCcdHandler(w http.ResponseWriter, r *http.Requ
} }
func (oAdmin *OvpnAdmin) serverSettingsHandler(w http.ResponseWriter, r *http.Request) { func (oAdmin *OvpnAdmin) serverSettingsHandler(w http.ResponseWriter, r *http.Request) {
log.Info(r.RemoteAddr, r.RequestURI) log.Info(r.RemoteAddr, " ", r.RequestURI)
enabledModules, enabledModulesErr := json.Marshal(oAdmin.modules) enabledModules, enabledModulesErr := json.Marshal(oAdmin.modules)
if enabledModulesErr != nil { if enabledModulesErr != nil {
log.Errorln(enabledModulesErr) log.Errorln(enabledModulesErr)
@ -366,19 +418,23 @@ func (oAdmin *OvpnAdmin) serverSettingsHandler(w http.ResponseWriter, r *http.Re
} }
func (oAdmin *OvpnAdmin) lastSyncTimeHandler(w http.ResponseWriter, r *http.Request) { func (oAdmin *OvpnAdmin) lastSyncTimeHandler(w http.ResponseWriter, r *http.Request) {
log.Debug(r.RemoteAddr, r.RequestURI) log.Debug(r.RemoteAddr, " ", r.RequestURI)
fmt.Fprint(w, oAdmin.lastSyncTime) fmt.Fprint(w, oAdmin.lastSyncTime)
} }
func (oAdmin *OvpnAdmin) lastSuccessfulSyncTimeHandler(w http.ResponseWriter, r *http.Request) { func (oAdmin *OvpnAdmin) lastSuccessfulSyncTimeHandler(w http.ResponseWriter, r *http.Request) {
log.Debug(r.RemoteAddr, r.RequestURI) log.Debug(r.RemoteAddr, " ", r.RequestURI)
fmt.Fprint(w, oAdmin.lastSuccessfulSyncTime) fmt.Fprint(w, oAdmin.lastSuccessfulSyncTime)
} }
func (oAdmin *OvpnAdmin) downloadCertsHandler(w http.ResponseWriter, r *http.Request) { func (oAdmin *OvpnAdmin) downloadCertsHandler(w http.ResponseWriter, r *http.Request) {
log.Info(r.RemoteAddr, r.RequestURI) log.Info(r.RemoteAddr, " ", r.RequestURI)
if oAdmin.role == "slave" { if oAdmin.role == "slave" {
http.Error(w, `{"status":"error"}`, http.StatusLocked) http.Error(w, `{"status":"error"}`, http.StatusBadRequest)
return
}
if *storageBackend == "kubernetes.secrets" {
http.Error(w, `{"status":"error"}`, http.StatusBadRequest)
return return
} }
_ = r.ParseForm() _ = r.ParseForm()
@ -395,9 +451,13 @@ func (oAdmin *OvpnAdmin) downloadCertsHandler(w http.ResponseWriter, r *http.Req
} }
func (oAdmin *OvpnAdmin) downloadCcdHandler(w http.ResponseWriter, r *http.Request) { func (oAdmin *OvpnAdmin) downloadCcdHandler(w http.ResponseWriter, r *http.Request) {
log.Info(r.RemoteAddr, r.RequestURI) log.Info(r.RemoteAddr, " ", r.RequestURI)
if oAdmin.role == "slave" { if oAdmin.role == "slave" {
http.Error(w, `{"status":"error"}`, http.StatusLocked) http.Error(w, `{"status":"error"}`, http.StatusBadRequest)
return
}
if *storageBackend == "kubernetes.secrets" {
http.Error(w, `{"status":"error"}`, http.StatusBadRequest)
return return
} }
_ = r.ParseForm() _ = r.ParseForm()
@ -441,7 +501,7 @@ func main() {
ovpnAdmin.masterSyncToken = *masterSyncToken ovpnAdmin.masterSyncToken = *masterSyncToken
ovpnAdmin.promRegistry = prometheus.NewRegistry() ovpnAdmin.promRegistry = prometheus.NewRegistry()
ovpnAdmin.modules = []string{} ovpnAdmin.modules = []string{}
ovpnAdmin.createUserMutex = &sync.Mutex{}
ovpnAdmin.mgmtInterfaces = make(map[string]string) ovpnAdmin.mgmtInterfaces = make(map[string]string)
for _, mgmtInterface := range *mgmtAddress { for _, mgmtInterface := range *mgmtAddress {
@ -465,7 +525,11 @@ func main() {
ovpnAdmin.modules = append(ovpnAdmin.modules, "core") ovpnAdmin.modules = append(ovpnAdmin.modules, "core")
if *authByPassword { if *authByPassword {
ovpnAdmin.modules = append(ovpnAdmin.modules, "passwdAuth") if *storageBackend != "kubernetes.secrets" {
ovpnAdmin.modules = append(ovpnAdmin.modules, "passwdAuth")
} else {
log.Fatal("Right now the keys `--storage.backend=kubernetes.secret` and `--auth.password` are not working together. Please use only one of them ")
}
} }
if *ccdEnabled { if *ccdEnabled {
@ -487,6 +551,8 @@ func main() {
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/change-password", ovpnAdmin.userChangePasswordHandler)
http.HandleFunc("/api/user/rotate", ovpnAdmin.userRotateHandler)
http.HandleFunc("/api/user/delete", ovpnAdmin.userDeleteHandler)
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)
@ -522,6 +588,7 @@ func (oAdmin *OvpnAdmin) registerMetrics() {
oAdmin.promRegistry.MustRegister(ovpnClientsTotal) oAdmin.promRegistry.MustRegister(ovpnClientsTotal)
oAdmin.promRegistry.MustRegister(ovpnClientsRevoked) oAdmin.promRegistry.MustRegister(ovpnClientsRevoked)
oAdmin.promRegistry.MustRegister(ovpnClientsConnected) oAdmin.promRegistry.MustRegister(ovpnClientsConnected)
oAdmin.promRegistry.MustRegister(ovpnUniqClientsConnected)
oAdmin.promRegistry.MustRegister(ovpnClientsExpired) oAdmin.promRegistry.MustRegister(ovpnClientsExpired)
oAdmin.promRegistry.MustRegister(ovpnClientCertificateExpire) oAdmin.promRegistry.MustRegister(ovpnClientCertificateExpire)
oAdmin.promRegistry.MustRegister(ovpnClientConnectionInfo) oAdmin.promRegistry.MustRegister(ovpnClientConnectionInfo)
@ -544,6 +611,7 @@ func (oAdmin *OvpnAdmin) updateState() {
ovpnClientBytesReceived.Reset() ovpnClientBytesReceived.Reset()
ovpnClientConnectionFrom.Reset() ovpnClientConnectionFrom.Reset()
ovpnClientConnectionInfo.Reset() ovpnClientConnectionInfo.Reset()
ovpnClientCertificateExpire.Reset()
go oAdmin.setState() go oAdmin.setState()
} }
} }
@ -624,7 +692,6 @@ func (oAdmin *OvpnAdmin) renderClientConfig(username string) string {
} else { } else {
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.PasswdAuth = *authByPassword conf.PasswdAuth = *authByPassword
@ -670,7 +737,9 @@ func (oAdmin *OvpnAdmin) parseCcd(username string) Ccd {
if *storageBackend == "kubernetes.secrets" { if *storageBackend == "kubernetes.secrets" {
txtLinesArray = strings.Split(app.secretGetCcd(ccd.User), "\n") txtLinesArray = strings.Split(app.secretGetCcd(ccd.User), "\n")
} else { } else {
txtLinesArray = strings.Split(fRead(*ccdDir+"/"+username), "\n") if fExist(*ccdDir + "/" + username) {
txtLinesArray = strings.Split(fRead(*ccdDir+"/"+username), "\n")
}
} }
for _, v := range txtLinesArray { for _, v := range txtLinesArray {
@ -689,22 +758,25 @@ func (oAdmin *OvpnAdmin) parseCcd(username string) Ccd {
} }
func (oAdmin *OvpnAdmin) modifyCcd(ccd Ccd) (bool, string) { func (oAdmin *OvpnAdmin) modifyCcd(ccd Ccd) (bool, string) {
ccdValid, ccdErr := validateCcd(ccd) ccdValid, err := validateCcd(ccd)
if ccdErr != "" { if err != "" {
return false, ccdErr return false, err
} }
if ccdValid { if ccdValid {
t := oAdmin.getCcdTemplate() t := oAdmin.getCcdTemplate()
var tmp bytes.Buffer var tmp bytes.Buffer
tplErr := t.Execute(&tmp, ccd) err := t.Execute(&tmp, ccd)
if tplErr != nil { if err != nil {
log.Error(tplErr) log.Error(err)
} }
if *storageBackend == "kubernetes.secrets" { if *storageBackend == "kubernetes.secrets" {
app.secretUpdateCcd(ccd.User, tmp.Bytes()) app.secretUpdateCcd(ccd.User, tmp.Bytes())
} else { } else {
fWrite(*ccdDir+"/"+ccd.User, tmp.String()) err = fWrite(*ccdDir+"/"+ccd.User, tmp.String())
if err != nil {
log.Errorf("modifyCcd: fWrite(): %v", err)
}
} }
return true, "ccd updated successfully" return true, "ccd updated successfully"
@ -779,16 +851,20 @@ func checkStaticAddressIsFree(staticAddress string, username string) bool {
return false return false
} }
func validateUsername(username string) bool { func validateUsername(username string) error {
var validUsername = regexp.MustCompile(usernameRegexp) var validUsername = regexp.MustCompile(usernameRegexp)
return validUsername.MatchString(username) if validUsername.MatchString(username) {
return nil
} else {
return errors.New(fmt.Sprintf("Username can only contains %s", usernameRegexp))
}
} }
func validatePassword(password string) bool { func validatePassword(password string) error {
if len(password) < passwordMinLength { if utf8.RuneCountInString(password) < passwordMinLength {
return false return errors.New(fmt.Sprintf("Password too short, password length must be greater or equal %d", passwordMinLength))
} else { } else {
return true return nil
} }
} }
@ -808,36 +884,42 @@ func (oAdmin *OvpnAdmin) usersList() []OpenvpnClient {
validCerts := 0 validCerts := 0
revokedCerts := 0 revokedCerts := 0
expiredCerts := 0 expiredCerts := 0
connectedUsers := 0 connectedUniqUsers := 0
totalActiveConnections := 0
apochNow := time.Now().Unix() apochNow := time.Now().Unix()
for _, line := range indexTxtParser(fRead(*indexTxtPath)) { for _, line := range indexTxtParser(fRead(*indexTxtPath)) {
if line.Identity != "server" { if line.Identity != "server" && !strings.Contains(line.Identity, "REVOKED") {
totalCerts += 1 totalCerts += 1
ovpnClient := OpenvpnClient{Identity: line.Identity, ExpirationDate: parseDateToString(indexTxtDateLayout, line.ExpirationDate, stringDateFormat)} ovpnClient := OpenvpnClient{Identity: line.Identity, ExpirationDate: parseDateToString(indexTxtDateLayout, line.ExpirationDate, stringDateFormat)}
switch { switch {
case line.Flag == "V": case line.Flag == "V":
ovpnClient.AccountStatus = "Active" ovpnClient.AccountStatus = "Active"
ovpnClientCertificateExpire.WithLabelValues(line.Identity).Set(float64((parseDateToUnix(indexTxtDateLayout, line.ExpirationDate) - apochNow) / 3600 / 24))
validCerts += 1 validCerts += 1
case line.Flag == "R": case line.Flag == "R":
ovpnClient.AccountStatus = "Revoked" ovpnClient.AccountStatus = "Revoked"
ovpnClient.RevocationDate = parseDateToString(indexTxtDateLayout, line.RevocationDate, stringDateFormat) ovpnClient.RevocationDate = parseDateToString(indexTxtDateLayout, line.RevocationDate, stringDateFormat)
ovpnClientCertificateExpire.WithLabelValues(line.Identity).Set(float64((parseDateToUnix(indexTxtDateLayout, line.ExpirationDate) - apochNow) / 3600 / 24))
revokedCerts += 1 revokedCerts += 1
case line.Flag == "E": case line.Flag == "E":
ovpnClient.AccountStatus = "Expired" ovpnClient.AccountStatus = "Expired"
ovpnClientCertificateExpire.WithLabelValues(line.Identity).Set(float64((parseDateToUnix(indexTxtDateLayout, line.ExpirationDate) - apochNow) / 3600 / 24))
expiredCerts += 1 expiredCerts += 1
} }
ovpnClient.ConnectionServer = "" ovpnClientCertificateExpire.WithLabelValues(line.Identity).Set(float64((parseDateToUnix(indexTxtDateLayout, line.ExpirationDate) - apochNow) / 3600 / 24))
if (parseDateToUnix(indexTxtDateLayout, line.ExpirationDate) - apochNow) < 0 {
ovpnClient.AccountStatus = "Expired"
}
ovpnClient.Connections = 0
userConnected, userConnectedTo := isUserConnected(line.Identity, oAdmin.activeClients) userConnected, userConnectedTo := isUserConnected(line.Identity, oAdmin.activeClients)
if userConnected { if userConnected {
ovpnClient.ConnectionStatus = "Connected" ovpnClient.ConnectionStatus = "Connected"
ovpnClient.ConnectionServer = userConnectedTo for range userConnectedTo {
connectedUsers += 1 ovpnClient.Connections += 1
totalActiveConnections += 1
}
connectedUniqUsers += 1
} }
users = append(users, ovpnClient) users = append(users, ovpnClient)
@ -856,7 +938,8 @@ func (oAdmin *OvpnAdmin) usersList() []OpenvpnClient {
ovpnClientsTotal.Set(float64(totalCerts)) ovpnClientsTotal.Set(float64(totalCerts))
ovpnClientsRevoked.Set(float64(revokedCerts)) ovpnClientsRevoked.Set(float64(revokedCerts))
ovpnClientsExpired.Set(float64(expiredCerts)) ovpnClientsExpired.Set(float64(expiredCerts))
ovpnClientsConnected.Set(float64(connectedUsers)) ovpnClientsConnected.Set(float64(totalActiveConnections))
ovpnUniqClientsConnected.Set(float64(connectedUniqUsers))
return users return users
} }
@ -864,23 +947,24 @@ func (oAdmin *OvpnAdmin) usersList() []OpenvpnClient {
func (oAdmin *OvpnAdmin) userCreate(username, password string) (bool, string) { func (oAdmin *OvpnAdmin) userCreate(username, password string) (bool, string) {
ucErr := fmt.Sprintf("User \"%s\" created", username) ucErr := fmt.Sprintf("User \"%s\" created", username)
oAdmin.createUserMutex.Lock()
defer oAdmin.createUserMutex.Unlock()
if checkUserExist(username) { if checkUserExist(username) {
ucErr = fmt.Sprintf("User \"%s\" already exists\n", username) ucErr = fmt.Sprintf("User \"%s\" already exists\n", username)
log.Debugf("userCreate: %s", ucErr) log.Debugf("userCreate: checkUserExist(): %s", ucErr)
return false, ucErr return false, ucErr
} }
if !validateUsername(username) { if err := validateUsername(username); err != nil {
ucErr = fmt.Sprintf("Username \"%s\" incorrect, you can use only %s\n", username, usernameRegexp) log.Debugf("userCreate: validateUsername(): %s", err.Error())
log.Debugf("userCreate: %s", ucErr) return false, err.Error()
return false, ucErr
} }
if *authByPassword { if *authByPassword {
if !validatePassword(password) { if err := validatePassword(password); err != nil {
ucErr = fmt.Sprintf("Password too short, password length must be greater or equal %d", passwordMinLength) log.Debugf("userCreate: authByPassword(): %s", err.Error())
log.Debugf("userCreate: %s", ucErr) return false, err.Error()
return false, ucErr
} }
} }
@ -890,7 +974,7 @@ func (oAdmin *OvpnAdmin) userCreate(username, password string) (bool, string) {
log.Error(err) log.Error(err)
} }
} else { } else {
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("cd %s && easyrsa build-client-full %s nopass 1>/dev/null", *easyrsaDirPath, username))
log.Debug(o) log.Debug(o)
} }
@ -901,52 +985,50 @@ func (oAdmin *OvpnAdmin) userCreate(username, password string) (bool, string) {
log.Infof("Certificate for user %s issued", username) log.Infof("Certificate for user %s issued", username)
oAdmin.clients = oAdmin.usersList() //oAdmin.clients = oAdmin.usersList()
return true, ucErr return true, ucErr
} }
func (oAdmin *OvpnAdmin) userChangePassword(username, password string) (bool, string) { func (oAdmin *OvpnAdmin) userChangePassword(username, password string) (error, string) {
if checkUserExist(username) { if checkUserExist(username) {
o := runBash(fmt.Sprintf("openvpn-user check --db.path %s --user %s | grep %s | wc -l", *authDatabase, username, username)) o := runBash(fmt.Sprintf("openvpn-user check --db.path %s --user %s | grep %s | wc -l", *authDatabase, username, username))
log.Info(o) log.Debug(o)
if !validatePassword(password) { if err := validatePassword(password); err != nil {
ucpErr := fmt.Sprintf("Password for too short, password length must be greater or equal %d", passwordMinLength) log.Warningf("userChangePassword: %s", err.Error())
log.Debugf("userChangePassword: %s", ucpErr) return err, err.Error()
return false, ucpErr
} }
if strings.TrimSpace(o) == "0" { if strings.TrimSpace(o) == "0" {
log.Info("Creating user in users.db")
o = runBash(fmt.Sprintf("openvpn-user create --db.path %s --user %s --password %s", *authDatabase, username, password)) o = runBash(fmt.Sprintf("openvpn-user create --db.path %s --user %s --password %s", *authDatabase, username, password))
log.Info(o) log.Debug(o)
} }
o = runBash(fmt.Sprintf("openvpn-user change-password --db.path %s --user %s --password %s", *authDatabase, username, password)) o = runBash(fmt.Sprintf("openvpn-user change-password --db.path %s --user %s --password %s", *authDatabase, username, password))
log.Info(o) log.Debug(o)
log.Tracef("INFO: password for user %s was changed", username) log.Infof("Password for user %s was changed", username)
return true, "Password changed" return nil, "Password changed"
} }
return false, "User does not exist" return errors.New(fmt.Sprintf("User \"%s\" not found}", username)), fmt.Sprintf("{\"msg\":\"User \"%s\" not found\"}", username)
} }
func (oAdmin *OvpnAdmin) getUserStatistic(username string) clientStatus { func (oAdmin *OvpnAdmin) getUserStatistic(username string) []clientStatus {
var userStatistic []clientStatus
for _, u := range oAdmin.activeClients { for _, u := range oAdmin.activeClients {
if u.CommonName == username { if u.CommonName == username {
return u userStatistic = append(userStatistic, u)
} }
} }
return clientStatus{} return userStatistic
} }
func (oAdmin *OvpnAdmin) userRevoke(username string) string { func (oAdmin *OvpnAdmin) userRevoke(username string) (error, string) {
log.Infof("Revoke certificate for user %s", username) log.Infof("Revoke certificate for user %s", username)
var shellOut string
if checkUserExist(username) { if checkUserExist(username) {
// check certificate valid flag 'V' // check certificate valid flag 'V'
if *storageBackend == "kubernetes.secrets" { if *storageBackend == "kubernetes.secrets" {
@ -955,30 +1037,33 @@ func (oAdmin *OvpnAdmin) userRevoke(username string) string {
log.Error(err) log.Error(err)
} }
} else { } else {
shellOut = 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("cd %s && echo yes | easyrsa revoke %s 1>/dev/null && easyrsa gen-crl 1>/dev/null", *easyrsaDirPath, username))
log.Debug(shellOut) log.Debugln(o)
} }
if *authByPassword { if *authByPassword {
shellOut = runBash(fmt.Sprintf("openvpn-user revoke --db-path %s --user %s", *authDatabase, username)) o := runBash(fmt.Sprintf("openvpn-user revoke --db-path %s --user %s", *authDatabase, username))
log.Trace(shellOut) log.Debug(o)
} }
crlFix() crlFix()
userConnected, userConnectedTo := isUserConnected(username, oAdmin.activeClients) userConnected, userConnectedTo := isUserConnected(username, oAdmin.activeClients)
log.Tracef("User %s connected: %t", username, userConnected) log.Tracef("User %s connected: %t", username, userConnected)
if userConnected { if userConnected {
oAdmin.mgmtKillUserConnection(username, userConnectedTo) for _, connection := range userConnectedTo {
log.Infof("Session for user \"%s\" session killed", username) oAdmin.mgmtKillUserConnection(username, connection)
log.Infof("Session for user \"%s\" killed", username)
}
} }
oAdmin.clients = oAdmin.usersList()
return fmt.Sprintln(shellOut) oAdmin.setState()
return nil, fmt.Sprintf("user \"%s\" revoked", username)
} }
log.Infof("user \"%s\" not found", username) log.Infof("user \"%s\" not found", username)
return fmt.Sprintf("User \"%s\" not found", username) return errors.New(fmt.Sprintf("User \"%s\" not found}", username)), fmt.Sprintf("User \"%s\" not found", username)
} }
func (oAdmin *OvpnAdmin) userUnrevoke(username string) string { func (oAdmin *OvpnAdmin) userUnrevoke(username string) (error, string) {
if checkUserExist(username) { if checkUserExist(username) {
if *storageBackend == "kubernetes.secrets" { if *storageBackend == "kubernetes.secrets" {
err := app.easyrsaUnrevoke(username) err := app.easyrsaUnrevoke(username)
@ -989,48 +1074,182 @@ func (oAdmin *OvpnAdmin) userUnrevoke(username string) string {
// check certificate revoked flag 'R' // check certificate revoked flag 'R'
usersFromIndexTxt := indexTxtParser(fRead(*indexTxtPath)) usersFromIndexTxt := indexTxtParser(fRead(*indexTxtPath))
for i := range usersFromIndexTxt { for i := range usersFromIndexTxt {
if usersFromIndexTxt[i].DistinguishedName == ("/CN=" + username) { if usersFromIndexTxt[i].DistinguishedName == "/CN="+username {
if usersFromIndexTxt[i].Flag == "R" { if usersFromIndexTxt[i].Flag == "R" {
usersFromIndexTxt[i].Flag = "V" usersFromIndexTxt[i].Flag = "V"
usersFromIndexTxt[i].RevocationDate = "" usersFromIndexTxt[i].RevocationDate = ""
o := runBash(fmt.Sprintf("cd %s && cp pki/revoked/certs_by_serial/%s.crt pki/issued/%s.crt", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber, username))
//fmt.Println(o) err := fMove(fmt.Sprintf("%s/pki/revoked/certs_by_serial/%s.crt", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber), fmt.Sprintf("%s/pki/issued/%s.crt", *easyrsaDirPath, username))
o = runBash(fmt.Sprintf("cd %s && cp pki/revoked/certs_by_serial/%s.crt pki/certs_by_serial/%s.pem", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber, usersFromIndexTxt[i].SerialNumber)) if err != nil {
//fmt.Println(o) log.Error(err)
o = runBash(fmt.Sprintf("cd %s && cp pki/revoked/private_by_serial/%s.key pki/private/%s.key", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber, username))
//fmt.Println(o)
o = runBash(fmt.Sprintf("cd %s && cp pki/revoked/reqs_by_serial/%s.req pki/reqs/%s.req", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber, username))
//fmt.Println(o)
fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt))
//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)
} }
err = fMove(fmt.Sprintf("%s/pki/revoked/certs_by_serial/%s.crt", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber), fmt.Sprintf("%s/pki/certs_by_serial/%s.pem", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber))
if err != nil {
log.Error(err)
}
err = fMove(fmt.Sprintf("%s/pki/revoked/private_by_serial/%s.key", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber), fmt.Sprintf("%s/pki/private/%s.key", *easyrsaDirPath, username))
if err != nil {
log.Error(err)
}
err = fMove(fmt.Sprintf("%s/pki/revoked/reqs_by_serial/%s.req", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber), fmt.Sprintf("%s/pki/reqs/%s.req", *easyrsaDirPath, username))
if err != nil {
log.Error(err)
}
err = fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt))
if err != nil {
log.Error(err)
}
_ = runBash(fmt.Sprintf("cd %s && easyrsa gen-crl 1>/dev/null", *easyrsaDirPath))
if *authByPassword {
o := runBash(fmt.Sprintf("openvpn-user restore --db-path %s --user %s", *authDatabase, username))
log.Debug(o)
}
crlFix() crlFix()
o = ""
log.Trace(o)
break break
} }
} }
} }
fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt)) err := fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt))
fmt.Print(renderIndexTxt(usersFromIndexTxt)) if err != nil {
log.Error(err)
}
//fmt.Print(renderIndexTxt(usersFromIndexTxt))
} }
crlFix() crlFix()
oAdmin.clients = oAdmin.usersList() oAdmin.clients = oAdmin.usersList()
return fmt.Sprintf("{\"msg\":\"User %s successfully unrevoked\"}", username) return nil, fmt.Sprintf("{\"msg\":\"User %s successfully unrevoked\"}", username)
} }
return fmt.Sprintf("{\"msg\":\"User \"%s\" not found\"}", username) return errors.New(fmt.Sprintf("user \"%s\" not found", username)), fmt.Sprintf("{\"msg\":\"User \"%s\" not found\"}", username)
}
func (oAdmin *OvpnAdmin) userRotate(username, newPassword string) (error, string) {
if checkUserExist(username) {
if *storageBackend == "kubernetes.secrets" {
err := app.easyrsaRotate(username, newPassword)
if err != nil {
log.Error(err)
}
} else {
var oldUserIndex, newUserIndex int
var oldUserSerial string
uniqHash := strings.Replace(uuid.New().String(), "-", "", -1)
usersFromIndexTxt := indexTxtParser(fRead(*indexTxtPath))
for i := range usersFromIndexTxt {
if usersFromIndexTxt[i].DistinguishedName == "/CN="+username {
oldUserSerial = usersFromIndexTxt[i].SerialNumber
usersFromIndexTxt[i].DistinguishedName = "/CN=REVOKED-" + username + "-" + uniqHash
oldUserIndex = i
break
}
}
err := fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt))
if err != nil {
log.Error(err)
}
if *authByPassword {
o := runBash(fmt.Sprintf("openvpn-user delete --force --db.path %s --user %s", *authDatabase, username))
log.Debug(o)
}
userCreated, userCreateMessage := oAdmin.userCreate(username, newPassword)
if !userCreated {
usersFromIndexTxt = indexTxtParser(fRead(*indexTxtPath))
for i := range usersFromIndexTxt {
if usersFromIndexTxt[i].SerialNumber == oldUserSerial {
usersFromIndexTxt[i].DistinguishedName = "/CN=" + username
break
}
}
err = fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt))
if err != nil {
log.Error(err)
}
return errors.New(fmt.Sprintf("error rotaing user due: %s", userCreateMessage)), userCreateMessage
}
usersFromIndexTxt = indexTxtParser(fRead(*indexTxtPath))
for i := range usersFromIndexTxt {
if usersFromIndexTxt[i].DistinguishedName == "/CN="+username {
newUserIndex = i
}
if usersFromIndexTxt[i].SerialNumber == oldUserSerial {
oldUserIndex = i
}
}
usersFromIndexTxt[oldUserIndex], usersFromIndexTxt[newUserIndex] = usersFromIndexTxt[newUserIndex], usersFromIndexTxt[oldUserIndex]
err = fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt))
if err != nil {
log.Error(err)
}
_ = runBash(fmt.Sprintf("cd %s && easyrsa gen-crl 1>/dev/null", *easyrsaDirPath))
}
crlFix()
oAdmin.clients = oAdmin.usersList()
return nil, fmt.Sprintf("{\"msg\":\"User %s successfully rotated\"}", username)
}
return errors.New(fmt.Sprintf("user \"%s\" not found", username)), fmt.Sprintf("{\"msg\":\"User \"%s\" not found\"}", username)
}
func (oAdmin *OvpnAdmin) userDelete(username string) (error, string) {
if checkUserExist(username) {
if *storageBackend == "kubernetes.secrets" {
err := app.easyrsaDelete(username)
if err != nil {
log.Error(err)
}
} else {
uniqHash := strings.Replace(uuid.New().String(), "-", "", -1)
usersFromIndexTxt := indexTxtParser(fRead(*indexTxtPath))
for i := range usersFromIndexTxt {
if usersFromIndexTxt[i].DistinguishedName == "/CN="+username {
usersFromIndexTxt[i].DistinguishedName = "/CN=REVOKED-" + username + "-" + uniqHash
break
}
}
if *authByPassword {
_ = runBash(fmt.Sprintf("openvpn-user delete --force --db.path %s --user %s", *authDatabase, username))
}
err := fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt))
if err != nil {
log.Error(err)
}
_ = runBash(fmt.Sprintf("cd %s && easyrsa gen-crl 1>/dev/null ", *easyrsaDirPath))
}
crlFix()
oAdmin.clients = oAdmin.usersList()
return nil, fmt.Sprintf("{\"msg\":\"User %s successfully deleted\"}", username)
}
return errors.New(fmt.Sprintf("User \"%s\" not found}", username)), fmt.Sprintf("{\"msg\":\"User \"%s\" not found\"}", username)
} }
func (oAdmin *OvpnAdmin) mgmtRead(conn net.Conn) string { func (oAdmin *OvpnAdmin) mgmtRead(conn net.Conn) string {
buf := make([]byte, 32768) recvData := make([]byte, 32768)
bufLen, _ := conn.Read(buf) var out string
s := string(buf[:bufLen]) var n int
return s var err error
for {
n, err = conn.Read(recvData)
if n <= 0 || err != nil {
break
} else {
out += string(recvData[:n])
if strings.Contains(out, "type 'help' for more info") || strings.Contains(out, "END") || strings.Contains(out, "SUCCESS:") || strings.Contains(out, "ERROR:") {
break
}
}
}
return out
} }
func (oAdmin *OvpnAdmin) mgmtConnectedUsersParser(text, serverName string) []clientStatus { func (oAdmin *OvpnAdmin) mgmtConnectedUsersParser(text, serverName string) []clientStatus {
@ -1188,19 +1407,27 @@ func (oAdmin *OvpnAdmin) mgmtSetTimeFormat() {
} }
} }
func isUserConnected(username string, connectedUsers []clientStatus) (bool, string) { func isUserConnected(username string, connectedUsers []clientStatus) (bool, []string) {
var connections []string
var connected = false
for _, connectedUser := range connectedUsers { for _, connectedUser := range connectedUsers {
if connectedUser.CommonName == username { if connectedUser.CommonName == username {
return true, connectedUser.ConnectedTo connected = true
connections = append(connections, connectedUser.ConnectedTo)
} }
} }
return false, "" return connected, connections
} }
func (oAdmin *OvpnAdmin) downloadCerts() bool { func (oAdmin *OvpnAdmin) downloadCerts() bool {
if fExist(certsArchivePath) { if fExist(certsArchivePath) {
fDelete(certsArchivePath) err := fDelete(certsArchivePath)
if err != nil {
log.Error(err)
}
} }
err := fDownload(certsArchivePath, *masterHost+downloadCertsApiUrl+"?token="+oAdmin.masterSyncToken, oAdmin.masterHostBasicAuth) err := fDownload(certsArchivePath, *masterHost+downloadCertsApiUrl+"?token="+oAdmin.masterSyncToken, oAdmin.masterHostBasicAuth)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
@ -1212,7 +1439,10 @@ func (oAdmin *OvpnAdmin) downloadCerts() bool {
func (oAdmin *OvpnAdmin) downloadCcd() bool { func (oAdmin *OvpnAdmin) downloadCcd() bool {
if fExist(ccdArchivePath) { if fExist(ccdArchivePath) {
fDelete(ccdArchivePath) err := fDelete(ccdArchivePath)
if err != nil {
log.Error(err)
}
} }
err := fDownload(ccdArchivePath, *masterHost+downloadCcdApiUrl+"?token="+oAdmin.masterSyncToken, oAdmin.masterHostBasicAuth) err := fDownload(ccdArchivePath, *masterHost+downloadCcdApiUrl+"?token="+oAdmin.masterSyncToken, oAdmin.masterHostBasicAuth)
@ -1225,55 +1455,69 @@ func (oAdmin *OvpnAdmin) downloadCcd() bool {
} }
func archiveCerts() { func archiveCerts() {
o := runBash(fmt.Sprintf("cd %s && tar -czf %s *", *easyrsaDirPath+"/pki", certsArchivePath)) err := createArchiveFromDir(*easyrsaDirPath+"/pki", certsArchivePath)
log.Trace(o) if err != nil {
log.Warnf("archiveCerts(): %s", err)
}
} }
func archiveCcd() { func archiveCcd() {
o := runBash(fmt.Sprintf("cd %s && tar -czf %s *", *ccdDir, ccdArchivePath)) err := createArchiveFromDir(*ccdDir, ccdArchivePath)
log.Trace(o) if err != nil {
log.Warnf("archiveCcd(): %s", err)
}
} }
func unArchiveCerts() { func unArchiveCerts() {
runBash(fmt.Sprintf("mkdir -p %s", *easyrsaDirPath+"/pki")) if err := os.MkdirAll(*easyrsaDirPath+"/pki", 0755); err != nil {
o := runBash(fmt.Sprintf("cd %s && tar -xzf %s", *easyrsaDirPath+"/pki", certsArchivePath)) log.Warnf("unArchiveCerts(): error creating pki dir: %s", err)
log.Trace(o) }
err := extractFromArchive(certsArchivePath, *easyrsaDirPath+"/pki")
if err != nil {
log.Warnf("unArchiveCerts: extractFromArchive() %s", err)
}
} }
func unArchiveCcd() { func unArchiveCcd() {
runBash(fmt.Sprintf("mkdir -p %s", *ccdDir)) if err := os.MkdirAll(*ccdDir, 0755); err != nil {
o := runBash(fmt.Sprintf("cd %s && tar -xzf %s", *ccdDir, ccdArchivePath)) log.Warnf("unArchiveCcd(): error creating ccd dir: %s", err)
log.Trace(o) }
err := extractFromArchive(ccdArchivePath, *ccdDir)
if err != nil {
log.Warnf("unArchiveCcd: extractFromArchive() %s", err)
}
} }
func (oAdmin *OvpnAdmin) syncDataFromMaster() { func (oAdmin *OvpnAdmin) syncDataFromMaster() {
retryCountMax := 3 retryCountMax := 3
certsDownloadFailed := true certsDownloadFailed := true
ccdDownloadFailed := true ccdDownloadFailed := true
certsDownloadRetries := 0
ccdDownloadRetries := 0
for certsDownloadFailed && certsDownloadRetries < retryCountMax { for certsDownloadRetries := 0; certsDownloadRetries < retryCountMax; certsDownloadRetries++ {
certsDownloadRetries += 1 log.Infof("Downloading archive with certificates from master. Attempt %d", certsDownloadRetries)
log.Infof("Downloading certs archive from master. Attempt %d", certsDownloadRetries)
if oAdmin.downloadCerts() { if oAdmin.downloadCerts() {
certsDownloadFailed = false certsDownloadFailed = false
log.Info("Decompression certs archive from master") log.Info("Decompressing archive with certificates from master")
unArchiveCerts() unArchiveCerts()
log.Info("Decompression archive with certificates from master completed")
break
} else { } else {
log.Warnf("something goes wrong during downloading certs from master. Attempt %d", certsDownloadRetries) log.Warnf("Something goes wrong during downloading archive with certificates from master. Attempt %d", certsDownloadRetries)
} }
} }
for ccdDownloadFailed && ccdDownloadRetries < retryCountMax { for ccdDownloadRetries := 0; ccdDownloadRetries < retryCountMax; ccdDownloadRetries++ {
ccdDownloadRetries += 1 log.Infof("Downloading archive with ccd from master. Attempt %d", ccdDownloadRetries)
log.Infof("Downloading ccd archive from master. Attempt %d", ccdDownloadRetries)
if oAdmin.downloadCcd() { if oAdmin.downloadCcd() {
ccdDownloadFailed = false ccdDownloadFailed = false
log.Info("Decompression ccd archive from master") log.Info("Decompressing archive with ccd from master")
unArchiveCcd() unArchiveCcd()
log.Info("Decompression archive with ccd from master completed")
break
} else { } else {
log.Warnf("something goes wrong during downloading certs from master. Attempt %d", ccdDownloadRetries) log.Warnf("Something goes wrong during downloading archive with ccd from master. Attempt %d", ccdDownloadRetries)
} }
} }
@ -1310,9 +1554,9 @@ func getOvpnServerHostsFromKubeApi() ([]OpenvpnServer, error) {
log.Error(err) log.Error(err)
} }
log.Tracef("Debug: service from kube api %v", service) log.Tracef("service from kube api %v", service)
log.Tracef("Debug: service.Status from kube api %v", service.Status) log.Tracef("service.Status from kube api %v", service.Status)
log.Tracef("Debug: service.Status.LoadBalancer from kube api %v", service.Status.LoadBalancer) log.Tracef("service.Status.LoadBalancer from kube api %v", service.Status.LoadBalancer)
lbIngress := service.Status.LoadBalancer.Ingress lbIngress := service.Status.LoadBalancer.Ingress
if len(lbIngress) > 0 { if len(lbIngress) > 0 {

View File

@ -8,9 +8,9 @@ env
auth_usr=$(head -1 $1) auth_usr=$(head -1 $1)
auth_passwd=$(tail -1 $1) auth_passwd=$(tail -1 $1)
if [ $common_name = $username ]; then if [ $common_name = $auth_usr ]; then
openvpn-user auth --db.path /etc/openvpn/easyrsa/pki/users.db --user ${auth_usr} --password ${auth_passwd} openvpn-user auth --db.path /etc/openvpn/easyrsa/pki/users.db --user ${auth_usr} --password ${auth_passwd}
else else
echo "Authorization failed" echo "Authorization failed"
exit 1 exit 1
fi fi

View File

@ -14,6 +14,7 @@ keepalive 10 60
persist-key persist-key
persist-tun persist-tun
topology subnet topology subnet
#duplicate-cn
#proto tcp #proto tcp
#port 1194 #port 1194
#dev tun0 #dev tun0