commit
ca53605554
21 changed files with 1859 additions and 10225 deletions
|
@ -3,7 +3,7 @@
|
|||
*.iml
|
||||
out
|
||||
gen
|
||||
|
||||
.github
|
||||
|
||||
easyrsa
|
||||
easyrsa_master
|
||||
|
@ -13,6 +13,7 @@ ccd_master
|
|||
ccd_slave
|
||||
werf.yaml
|
||||
frontend/node_modules
|
||||
frontend/static/dist
|
||||
openvpn-web-ui
|
||||
openvpn-ui
|
||||
openvpn-admin
|
||||
|
@ -20,4 +21,6 @@ ovpn-admin
|
|||
|
||||
docker-compose.yaml
|
||||
docker-compose-slave.yaml
|
||||
img
|
||||
img
|
||||
dashboard
|
||||
.helm
|
||||
|
|
2
.github/workflows/publish-latest.yaml
vendored
2
.github/workflows/publish-latest.yaml
vendored
|
@ -5,7 +5,7 @@ on:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
name: build latest images for relase
|
||||
name: build latest images for release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
|
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
|||
- name: checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: build binaries
|
||||
uses: wangyoucao577/go-release-action@v1.22
|
||||
uses: wangyoucao577/go-release-action@v1.28
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
goversion: 1.17
|
||||
|
|
2
.github/workflows/release_arm.yaml
vendored
2
.github/workflows/release_arm.yaml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
|||
- name: checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: build binaries
|
||||
uses: wangyoucao577/go-release-action@v1.22
|
||||
uses: wangyoucao577/go-release-action@v1.28
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
goversion: 1.17
|
||||
|
|
|
@ -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
|
||||
RUN cd /app && npm install && npm run build
|
||||
|
||||
|
@ -8,10 +8,10 @@ COPY --from=frontend-builder /app/static /app/frontend/static
|
|||
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
|
||||
|
||||
FROM alpine:3.14
|
||||
FROM alpine:3.16
|
||||
WORKDIR /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 && \
|
||||
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/*
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
FROM alpine:3.14
|
||||
RUN apk add --update bash openvpn easy-rsa && \
|
||||
FROM alpine:3.16
|
||||
RUN apk add --update bash openvpn easy-rsa iptables && \
|
||||
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/*
|
||||
COPY setup/ /etc/openvpn/setup
|
||||
RUN chmod +x /etc/openvpn/setup/configure.sh
|
||||
|
|
84
README.md
84
README.md
|
@ -8,8 +8,8 @@ Originally created in [Flant](https://flant.com/) for internal needs & used for
|
|||
|
||||
## Features
|
||||
|
||||
* Adding OpenVPN users (generating certificates for them);
|
||||
* Revoking/restoring users certificates;
|
||||
* Adding, deleting OpenVPN users (generating certificates for them);
|
||||
* Revoking/restoring/rotating users certificates;
|
||||
* Generating ready-to-user config files;
|
||||
* 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;
|
||||
|
@ -28,15 +28,12 @@ An example of dashboard made using ovpn-admin metrics:
|
|||
|
||||
## Installation
|
||||
|
||||
### Disclaimer
|
||||
|
||||
This tool uses external calls for `bash`, `coreutils` and `easy-rsa`, thus **Linux systems only are supported** at the moment.
|
||||
|
||||
### 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).
|
||||
|
||||
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:
|
||||
|
||||
|
@ -45,6 +42,9 @@ git clone https://github.com/flant/ovpn-admin.git
|
|||
cd ovpn-admin
|
||||
./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
|
||||
|
||||
|
@ -65,11 +65,19 @@ cd ovpn-admin
|
|||
|
||||
(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
|
||||
|
||||
|
@ -80,88 +88,82 @@ Flags:
|
|||
--help show context-sensitive help (try also --help-long and --help-man)
|
||||
|
||||
--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
|
||||
(or $OVPN_LISTEN_PORT)
|
||||
(or OVPN_LISTEN_PORT)
|
||||
|
||||
--role="master" server role, master or slave
|
||||
(or $OVPN_ROLE)
|
||||
(or OVPN_ROLE)
|
||||
|
||||
--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
|
||||
(or $OVPN_MASTER_USER)
|
||||
(or OVPN_MASTER_USER)
|
||||
|
||||
--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
|
||||
(or $OVPN_MASTER_SYNC_FREQUENCY)
|
||||
(or OVPN_MASTER_SYNC_FREQUENCY)
|
||||
|
||||
--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"
|
||||
(or $OVPN_NETWORK) NETWORK/MASK_PREFIX for OpenVPN server
|
||||
(or OVPN_NETWORK) NETWORK/MASK_PREFIX for OpenVPN server
|
||||
|
||||
--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
|
||||
|
||||
--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"
|
||||
(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
|
||||
|
||||
--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
|
||||
|
||||
--metrics.path="/metrics" URL path for exposing collected metrics
|
||||
(or $OVPN_METRICS_PATH)
|
||||
(or OVPN_METRICS_PATH)
|
||||
|
||||
--easyrsa.path="./easyrsa/" path to easyrsa dir
|
||||
(or $EASYRSA_PATH)
|
||||
(or EASYRSA_PATH)
|
||||
|
||||
--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
|
||||
(or $OVPN_CCD)
|
||||
(or OVPN_CCD)
|
||||
|
||||
--ccd.path="./ccd" path to client-config-dir
|
||||
(or $OVPN_CCD_PATH)
|
||||
(or OVPN_CCD_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
|
||||
(or $OVPN_TEMPLATES_CCD_PATH)
|
||||
(or OVPN_TEMPLATES_CCD_PATH)
|
||||
|
||||
--auth.password enable additional password authorization
|
||||
(or $OVPN_AUTH)
|
||||
(or OVPN_AUTH)
|
||||
|
||||
--auth.db="./easyrsa/pki/users.db"
|
||||
(or $OVPN_AUTH_DB_PATH) database path for password authorization
|
||||
|
||||
--debug enable debug mode
|
||||
(or $OVPN_DEBUG)
|
||||
|
||||
--verbose enable verbose mode
|
||||
(or $OVPN_VERBOSE)
|
||||
(or OVPN_AUTH_DB_PATH) database path for password authorization
|
||||
|
||||
--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)
|
||||
(or $LOG_FORMAT)
|
||||
(or LOG_FORMAT)
|
||||
|
||||
--storage.backend storage backend: filesystem, kubernetes.secrets (default filesystem)
|
||||
(or $STORAGE_BACKEND)
|
||||
(or STORAGE_BACKEND)
|
||||
|
||||
--version show application version
|
||||
```
|
||||
|
|
|
@ -10,6 +10,7 @@ services:
|
|||
environment:
|
||||
OVPN_SERVER_NET: "192.168.100.0"
|
||||
OVPN_SERVER_MASK: "255.255.255.0"
|
||||
OVPN_PASSWD_AUTH: "true"
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
ports:
|
||||
|
@ -24,12 +25,17 @@ services:
|
|||
image: ovpn-admin:local
|
||||
command: /app/ovpn-admin
|
||||
environment:
|
||||
OVPN_DEBUG: "True"
|
||||
OVPN_VERBOSE: "True"
|
||||
OVPN_DEBUG: "true"
|
||||
OVPN_VERBOSE: "true"
|
||||
OVPN_NETWORK: "192.168.100.0/24"
|
||||
OVPN_CCD: "true"
|
||||
OVPN_CCD_PATH: "/mnt/ccd"
|
||||
EASYRSA_PATH: "/mnt/easyrsa"
|
||||
OVPN_SERVER: "127.0.0.1:7777:tcp"
|
||||
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
|
||||
volumes:
|
||||
- ./easyrsa_master:/mnt/easyrsa
|
||||
|
|
10828
frontend/package-lock.json
generated
10828
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -10,14 +10,15 @@
|
|||
"build": "cross-env NODE_ENV=production webpack --progress"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.24.0",
|
||||
"bootstrap-vue": "^2.21.2",
|
||||
"axios": "^0.27.1",
|
||||
"bootstrap-vue": "^2.22.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"vue": "^2.6.14",
|
||||
"vue-clipboard2": "^0.3.3",
|
||||
"vue-cookies": "^1.7.4",
|
||||
"vue-good-table": "^2.21.11",
|
||||
"vue-notification": "^1.3.20"
|
||||
"vue-notification": "^1.3.20",
|
||||
"vue-style-loader": "^4.1.3"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
@ -38,7 +39,7 @@
|
|||
"node-sass": "^7.0.1",
|
||||
"sass-loader": "^12.4.0",
|
||||
"terser-webpack-plugin": "^5.3.0",
|
||||
"vue-loader": "^15.9.8",
|
||||
"vue-loader": "^17.0.0",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"webpack": "^5.65.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
|
|
|
@ -57,8 +57,8 @@ new Vue({
|
|||
filterable: true,
|
||||
},
|
||||
{
|
||||
label: 'Connection Server',
|
||||
field: 'ConnectionServer',
|
||||
label: 'Active Connections',
|
||||
field: 'Connections',
|
||||
filterable: true,
|
||||
},
|
||||
{
|
||||
|
@ -107,6 +107,38 @@ new Vue({
|
|||
showForServerRole: ['master'],
|
||||
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',
|
||||
label: 'Unrevoke',
|
||||
|
@ -161,10 +193,14 @@ new Vue({
|
|||
newPassword: '',
|
||||
passwordChangeStatus: '',
|
||||
passwordChangeMessage: '',
|
||||
rotateUserMessage: '',
|
||||
deleteUserMessage: '',
|
||||
modalNewUserVisible: false,
|
||||
modalShowConfigVisible: false,
|
||||
modalShowCcdVisible: false,
|
||||
modalChangePasswordVisible: false,
|
||||
modalRotateUserVisible: false,
|
||||
modalDeleteUserVisible: false,
|
||||
openvpnConfig: '',
|
||||
ccd: {
|
||||
Name: '',
|
||||
|
@ -204,6 +240,16 @@ new Vue({
|
|||
_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.u.modalShowConfigVisible = true;
|
||||
var data = new URLSearchParams();
|
||||
|
@ -251,8 +297,8 @@ new Vue({
|
|||
})
|
||||
},
|
||||
computed: {
|
||||
customAddressDisabled: function () {
|
||||
return this.serverRole == "master" ? this.u.ccd.ClientAddress == "dynamic" : true
|
||||
customAddressDynamic: function () {
|
||||
return this.u.ccd.ClientAddress == "dynamic"
|
||||
},
|
||||
ccdApplyStatusCssClass: function () {
|
||||
return this.u.ccdApplyStatus == 200 ? "alert-success" : "alert-danger"
|
||||
|
@ -260,6 +306,12 @@ new Vue({
|
|||
passwordChangeStatusCssClass: function () {
|
||||
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 () {
|
||||
return this.u.modalNewUserVisible ? {display: 'flex'} : {}
|
||||
},
|
||||
|
@ -272,6 +324,12 @@ new Vue({
|
|||
modalChangePasswordDisplay: function () {
|
||||
return this.u.modalChangePasswordVisible ? {display: 'flex'} : {}
|
||||
},
|
||||
modalRotateUserDisplay: function () {
|
||||
return this.u.modalRotateUserVisible ? {display: 'flex'} : {}
|
||||
},
|
||||
modalDeleteUserDisplay: function () {
|
||||
return this.u.modalDeleteUserVisible ? {display: 'flex'} : {}
|
||||
},
|
||||
revokeFilterText: function() {
|
||||
return this.filters.hideRevoked ? "Show revoked" : "Hide revoked"
|
||||
},
|
||||
|
@ -288,7 +346,16 @@ new Vue({
|
|||
},
|
||||
methods: {
|
||||
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) {
|
||||
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() {
|
||||
var _this = this;
|
||||
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'})
|
||||
});
|
||||
},
|
||||
|
||||
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'})
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
})
|
||||
|
|
|
@ -48,6 +48,14 @@ body {
|
|||
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 {
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
|
|
@ -42,10 +42,6 @@
|
|||
</template>
|
||||
</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-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
|
@ -123,12 +119,10 @@
|
|||
<div class="modal-body">
|
||||
<div class="input-group">
|
||||
<h5 class="static-address-label ">Static address:</h5>
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<input id="enable-static" type="checkbox" @change="staticAddrCheckboxOnChange()" v-if="serverRole == 'master'" v-bind:checked="!customAddressDisabled">
|
||||
</div>
|
||||
<input id="static-address" type="text" class="form-control" v-model="u.ccd.ClientAddress" placeholder="127.0.0.1">
|
||||
<div class="input-group-append">
|
||||
<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>
|
||||
<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">
|
||||
|
@ -191,6 +185,50 @@
|
|||
</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" />
|
||||
</div>
|
||||
<script src="dist/bundle.min.js"></script>
|
||||
|
|
|
@ -1,55 +1,50 @@
|
|||
// Generated using webpack-cli https://github.com/webpack/webpack-cli
|
||||
|
||||
const path = require('path');
|
||||
//const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
|
||||
const isProduction = process.env.NODE_ENV == 'production';
|
||||
|
||||
|
||||
const config = {
|
||||
module.exports = {
|
||||
mode: 'production',
|
||||
entry: {
|
||||
bundle: [
|
||||
'./src/main.js',
|
||||
],
|
||||
],
|
||||
style: [
|
||||
'./src/style.js',
|
||||
]
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, './static/dist'),
|
||||
publicPath: '/dist/',
|
||||
filename: '[name].min.js'
|
||||
path: path.resolve(__dirname, './static/dist'),
|
||||
publicPath: '/dist/',
|
||||
filename: '[name].min.js'
|
||||
},
|
||||
plugins: [
|
||||
//new BundleAnalyzerPlugin(),
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
'vue-style-loader',
|
||||
'css-loader'
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-env']
|
||||
}
|
||||
},
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
'vue-style-loader',
|
||||
'css-loader'
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
//exclude: /node_modules\/(?!bootstrap-vue\/src\/)/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-env']
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'vue$': 'vue/dist/vue.esm.js'
|
||||
'vue$': 'vue/dist/vue.esm.js',
|
||||
//'bootstrap-vue$': 'bootstrap-vue/src/index.js'
|
||||
},
|
||||
extensions: ['*', '.js', '.vue', '.json']
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = () => {
|
||||
config.mode = 'production';
|
||||
|
||||
return config;
|
||||
};
|
||||
|
|
4
go.mod
4
go.mod
|
@ -8,6 +8,9 @@ require (
|
|||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
k8s.io/apimachinery v0.23.1
|
||||
k8s.io/client-go v0.23.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
|
@ -22,6 +25,7 @@ require (
|
|||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-cmp v0.5.6 // 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/json-iterator/go v1.1.12 // indirect
|
||||
github.com/karrick/godirwalk v1.16.1 // indirect
|
||||
|
|
2
go.sum
2
go.sum
|
@ -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.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.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.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
|
||||
|
|
215
helpers.go
215
helpers.go
|
@ -1,17 +1,22 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
|
@ -19,11 +24,11 @@ func parseDate(layout,datetime string) time.Time {
|
|||
return t
|
||||
}
|
||||
|
||||
func parseDateToString(layout,datetime,format string) string {
|
||||
func parseDateToString(layout, datetime, format string) string {
|
||||
return parseDate(layout, datetime).Format(format)
|
||||
}
|
||||
|
||||
func parseDateToUnix(layout,datetime string) int64 {
|
||||
func parseDateToUnix(layout, datetime string) int64 {
|
||||
return parseDate(layout, datetime).Unix()
|
||||
}
|
||||
|
||||
|
@ -32,7 +37,7 @@ func runBash(script string) string {
|
|||
cmd := exec.Command("bash", "-c", script)
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return (fmt.Sprint(err) + " : " + string(stdout))
|
||||
return fmt.Sprint(err) + " : " + string(stdout)
|
||||
}
|
||||
return string(stdout)
|
||||
}
|
||||
|
@ -43,7 +48,7 @@ func fExist(path string) bool {
|
|||
if os.IsNotExist(err) {
|
||||
return false
|
||||
} else if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("fExist: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -53,37 +58,102 @@ func fExist(path string) bool {
|
|||
func fRead(path string) string {
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Warning(err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(content)
|
||||
}
|
||||
|
||||
func fCreate(path string) bool {
|
||||
func fCreate(path string) error {
|
||||
var _, err = os.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
var file, err = os.Create(path)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return false
|
||||
return err
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fDelete(path string) {
|
||||
func fDelete(path string) error {
|
||||
err := os.Remove(path)
|
||||
if err != nil {
|
||||
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 {
|
||||
|
@ -99,7 +169,7 @@ func fDownload(path, url string, basicAuth bool) error {
|
|||
}
|
||||
|
||||
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()
|
||||
|
||||
|
@ -113,3 +183,124 @@ func fDownload(path, url string, basicAuth bool) error {
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -7,9 +7,11 @@ import (
|
|||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
@ -22,7 +24,7 @@ import (
|
|||
const (
|
||||
secretCA = "openvpn-pki-ca"
|
||||
secretServer = "openvpn-pki-server"
|
||||
secretClientTmpl = "openvpn-pki-%s"
|
||||
secretClientTmpl = "openvpn-pki-%d"
|
||||
secretCRL = "openvpn-pki-crl"
|
||||
secretIndexTxt = "openvpn-pki-index-txt"
|
||||
secretDHandTA = "openvpn-pki-dh-and-ta"
|
||||
|
@ -228,11 +230,11 @@ func (openVPNPKI *OpenVPNPKI) indexTxtUpdate() (err error) {
|
|||
log.Trace(cert.Subject.CommonName)
|
||||
|
||||
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()) {
|
||||
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 {
|
||||
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),
|
||||
"notAfter": clientCert.NotAfter.Format(indexTxtDateFormat),
|
||||
"revokedAt": "",
|
||||
"serialNumber": clientCert.SerialNumber.String(),
|
||||
"serialNumber": fmt.Sprintf("%d", clientCert.SerialNumber),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -447,6 +449,78 @@ func (openVPNPKI *OpenVPNPKI) easyrsaUnrevoke(commonName string) (err error) {
|
|||
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) {
|
||||
secret, err := openVPNPKI.secretGetByName(name)
|
||||
if err != nil {
|
||||
|
|
558
main.go
558
main.go
|
@ -7,20 +7,23 @@ import (
|
|||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"io/ioutil"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gobuffalo/packr/v2"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
@ -73,7 +76,7 @@ var (
|
|||
certsArchivePath = "/tmp/" + certsArchiveFileName
|
||||
ccdArchivePath = "/tmp/" + ccdArchiveFileName
|
||||
|
||||
version = "1.7.5"
|
||||
version = "2.0.0"
|
||||
)
|
||||
|
||||
var logLevels = map[string]log.Level{
|
||||
|
@ -122,7 +125,13 @@ var (
|
|||
|
||||
ovpnClientsConnected = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
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
|
||||
modules []string
|
||||
mgmtStatusTimeFormat string
|
||||
createUserMutex *sync.Mutex
|
||||
}
|
||||
|
||||
type OpenvpnServer struct {
|
||||
|
@ -198,7 +208,7 @@ type OpenvpnClient struct {
|
|||
ExpirationDate string `json:"ExpirationDate"`
|
||||
RevocationDate string `json:"RevocationDate"`
|
||||
ConnectionStatus string `json:"ConnectionStatus"`
|
||||
ConnectionServer string `json:"ConnectionServer"`
|
||||
Connections int `json:"Connections"`
|
||||
}
|
||||
|
||||
type ccdRoute struct {
|
||||
|
@ -237,20 +247,20 @@ type clientStatus struct {
|
|||
}
|
||||
|
||||
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)
|
||||
fmt.Fprintf(w, "%s", usersList)
|
||||
}
|
||||
|
||||
func (oAdmin *OvpnAdmin) userStatisticHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Info(r.RemoteAddr, r.RequestURI)
|
||||
log.Info(r.RemoteAddr, " ", r.RequestURI)
|
||||
_ = r.ParseForm()
|
||||
userStatistic, _ := json.Marshal(oAdmin.getUserStatistic(r.FormValue("username")))
|
||||
fmt.Fprintf(w, "%s", userStatistic)
|
||||
}
|
||||
|
||||
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" {
|
||||
http.Error(w, `{"status":"error"}`, http.StatusLocked)
|
||||
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"))
|
||||
|
||||
if userCreated {
|
||||
oAdmin.clients = oAdmin.usersList()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(w, userCreateStatus)
|
||||
return
|
||||
|
@ -266,41 +277,82 @@ func (oAdmin *OvpnAdmin) userCreateHandler(w http.ResponseWriter, r *http.Reques
|
|||
http.Error(w, userCreateStatus, http.StatusUnprocessableEntity)
|
||||
}
|
||||
}
|
||||
|
||||
func (oAdmin *OvpnAdmin) userRevokeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Info(r.RemoteAddr, r.RequestURI)
|
||||
func (oAdmin *OvpnAdmin) userRotateHandler(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()
|
||||
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) {
|
||||
log.Info(r.RemoteAddr, r.RequestURI)
|
||||
log.Info(r.RemoteAddr, " ", r.RequestURI)
|
||||
if oAdmin.role == "slave" {
|
||||
http.Error(w, `{"status":"error"}`, http.StatusLocked)
|
||||
return
|
||||
}
|
||||
|
||||