1
0
Fork 0
mirror of synced 2024-11-13 20:58:58 -05:00

Compare commits

...

114 commits
1.1 ... master

Author SHA1 Message Date
Mike Klyuev
7134815ce6
Update README.md (#291)
Update current project status
2024-09-24 11:54:23 +03:00
Sprait
0c881c81e7
update start.sh
using compose v2
https://docs.docker.com/compose/migrate/#docker-compose-vs-docker-compose
2023-12-12 09:58:55 +03:00
Dmitry Shurupov
699cddc908
Fix the announcement blog link in README.md 2023-10-25 11:57:38 +07:00
Sprait
c83c581e21
fix revoke user (#243)
* fix reuse argument multiple times

* replace whitespace with tab
2023-09-12 11:05:28 +03:00
Sprait
35f76ec3b6
Update actions build binary (#240)
* disable-386-build

* update versison go-release-action
2023-09-11 09:18:48 +03:00
Sprait
8a35f70364
add variable for prometheus datasource to dashboard (#239) 2023-09-08 17:45:34 +03:00
Sprait
4981dcb919
Multi-platform build (#234)
* add multi-platform build
2023-09-04 19:24:13 +03:00
Sprait
dbc48ef3f1
Merge pull request #92 from strnk/master
listen.base-url parameter to support reverse-proxy setups
2023-08-22 13:40:46 +03:00
vitaliy-sn
f4d0212bfc
Merge pull request #199 from flant/update-user-list-in-ha-mode
Fix update user list for kubernetes.secrets backend
2023-04-27 16:04:00 +03:00
Vitaliy Snurnitsin
9024405232 Force update user list for kubernetes.secrets backend
Signed-off-by: Vitaliy Snurnitsin <vitaliy.snurnitsin@flant.com>
2023-04-27 15:18:29 +03:00
Christophe Huriaux
dfe2f3d756
Merge branch 'master' into master 2022-11-02 14:29:22 +01:00
Ilya Sosnovsky
a973b88463
Update README.md
Fix notes due #165 #166
2022-11-02 14:41:30 +03:00
Ilya Sosnovsky
3f3976ff7a Add dashboard example 2022-09-06 09:55:02 +03:00
Ilya Sosnovsky
5d41f6d91e
Fix multiarch build 2022-09-05 11:48:34 +03:00
Ilya Sosnovsky
7db35753ad
Merge pull request #137 from miklezzzz/master
fix absent labels for secrets
2022-09-01 14:42:26 +03:00
Mikhail Scherba
10f7441c49 fix absent labels for secrets 2022-09-01 13:15:22 +03:00
Ilya Sosnovsky
ca53605554
Merge pull request #132 from flant/rc/2.0.0
Release 2.0.0
2022-08-29 12:20:48 +03:00
Ilya Sosnovsky
d012141b51 refactoring 2022-08-12 13:52:45 +03:00
Ilya Sosnovsky
0dfe9f9494 Add example dashboard 2022-08-04 15:13:41 +03:00
Ilya Sosnovsky
3e599eb989 Merge branch 'rc/2.0.0' of github.com:flant/ovpn-admin into rc/2.0.0 2022-08-02 17:19:58 +03:00
Ilya Sosnovsky
67dd4538ad small refactoring to reduce os.exec calls 2022-08-02 17:19:27 +03:00
Ilya Sosnovsky
5146d04a0d
Merge branch 'master' into rc/2.0.0 2022-07-21 18:40:57 +03:00
Ilya Sosnovsky
a0daf5b4d7 add feature for rotate and delete users; fixes; refactoring 2022-07-21 18:37:34 +03:00
Ilya Sosnovsky
f369639a2a
Merge pull request #126 from sabuhish/master
Fix typo error
2022-06-08 13:17:21 +03:00
sabuhish
5a6724bf6a Fix typo error 2022-05-18 11:37:02 +04:00
vitaliy-sn
53119e17b2
Merge pull request #122 from flant/fix-crl-file-permissions
Fix crl.pem file permissions
2022-04-27 23:03:57 +03:00
Vitaliy Snurnitsin
5705d2f60b Fix crl file permissions 2022-04-27 21:51:43 +03:00
vitaliy-sn
99569daf31
Merge pull request #121 from flant/multiple-lb
Add ability to specify multiple --ovpn.service
2022-04-27 15:29:27 +03:00
Vitaliy Snurnitsin
47abe3bc1a Added ability to specify multiple --ovpn.service 2022-04-27 13:45:43 +03:00
vitaliy-sn
cd746c20b5
Merge pull request #99 from vitaliy-sn/k8s-secrets-storage
K8s secrets storage
2022-03-18 17:44:02 +03:00
Vitaliy Snurnitsin
b26d0968e1 add helm chart example 2022-03-18 17:36:57 +03:00
Vitaliy Snurnitsin
7f1da70b9d kubernetes.secrets storage.backend implementation 2022-03-18 17:36:57 +03:00
Vitaliy Snurnitsin
4b7ef65a66 add storage.backend selection (filesystem or kubernetes.secrets) 2022-03-18 17:36:57 +03:00
Vitaliy Snurnitsin
77adc1108c openssl binary replaced with golang crypto/x509 library 2022-01-29 03:07:45 +03:00
Vitaliy Snurnitsin
9b1b34d4c4 logs, gofmt 2022-01-29 03:06:35 +03:00
Vitaliy Snurnitsin
5af16605d2 go 1.17 2022-01-29 03:04:26 +03:00
Vitaliy Snurnitsin
f180a9cc5a node 16.13 2022-01-29 02:22:08 +03:00
strnk
0ee9be5744 Fix certs and ccd slave download API endpoints 2021-12-07 16:12:57 +01:00
strnk
f73626dd7b Add configuration parameter for the easyrsa script path 2021-12-07 15:57:31 +01:00
strnk
633ad79d6a Add base URL configuration to the webserver to support reverse-proxy setups 2021-12-07 15:44:52 +01:00
Ilya Sosnovsky
d3b5a77efb fixes for build scripts 2021-11-29 13:40:05 +03:00
Ilya Sosnovsky
af65b36d2b fix 2021-11-29 13:10:55 +03:00
Ilya Sosnovsky
2d75fb1f4b bump go version 2021-11-29 12:57:45 +03:00
Ilya Sosnovsky
5ddbfe81ed
Merge pull request #88 from flant/rc/1.7.5
fixed a broken release
2021-11-29 11:37:28 +03:00
Ilya Sosnovsky
9873c2cb76 fixed a broken release 2021-11-29 11:34:34 +03:00
Ilya Sosnovsky
4bdb74411c
Merge pull request #86 from smalot/fix-packr-path
Fix packr path dependency
2021-11-25 20:30:00 +03:00
Sebastien Malot
b378ae17dd Fix packr path dependency 2021-11-23 08:57:11 +01:00
Sebastien Malot
1b421070cb Fix packr path dependency 2021-11-23 08:55:59 +01:00
Dmitry Shurupov
3c77273990
Security disclaimer for README 2021-11-11 11:12:10 +07:00
Dmitry Shurupov
53e9cb7835 Fixing env vars descriptions and names 2021-10-22 11:35:16 +07:00
Dmitry Shurupov
f5507be24d
Adding env vars and other fixes for README 2021-10-22 11:26:09 +07:00
Ilya Sosnovsky
5543829717 fix arm workflow 2021-10-21 14:26:50 +03:00
Ilya Sosnovsky
2daadf30ae
Merge pull request #63 from flant/rc/1.7.4
Release 1.7.4
2021-10-21 14:15:46 +03:00
Ilya Sosnovsky
ed71ed1537 envvar configuraion; arm build; docs fixes 2021-10-21 13:01:03 +03:00
Dmitry Shurupov
a5d8f3dd96
Merge pull request #61 from MinimaJack/patch-1
Fix a typo in client.conf.tpl
2021-10-20 13:51:14 +07:00
Dmitry Shurupov
61905a911a
Merge pull request #62 from olegznv/oleg-readme
README text typo correction
2021-10-20 13:47:46 +07:00
olegznv
214d7b30c5 forgot fix 2021-10-20 09:37:10 +03:00
Evgeniy Vanzhula
e07c1b6dfe
Update client.conf.tpl
typo fix
2021-10-19 16:47:27 +03:00
Ilya Sosnovsky
c27229e920 build changes 2021-10-15 08:05:30 +03:00
Ilya Sosnovsky
8db6d93bcb show client config in log only in verbose mode; actions changes 2021-10-15 07:54:24 +03:00
Ilya Sosnovsky
c1970c26e4
Lowercase protocol for remote from k8s api 2021-10-14 08:11:07 +03:00
Ilya Sosnovsky
924230c6ba
Merge pull request #50 from noamkush/iptables-fix
Allow iptables delete command to fail
2021-10-13 11:50:45 +03:00
Noam
d17ea9aee9 Allowed iptables delete command to fail. 2021-10-07 15:18:58 +03:00
Ilya Sosnovsky
9fa3c44f9a
Merge pull request #47 from flant/rc-1.7.0
Release 1.7.0
2021-10-05 18:34:45 +03:00
Ilya Sosnovsky
77c0fbb778 Prepare for release 1.7.0 2021-10-05 18:09:29 +03:00
Ilya Sosnovsky
ace42f729e
Merge pull request #37 from wzooff/feat/add-custom-client-config-template
add option to specify custom user template path
2021-10-05 15:08:17 +03:00
Ilya Sosnovsky
2012f07135
Merge pull request #34 from CheyenneForbes/patch-1
Ensure `rows` is set to an array
2021-10-05 15:07:15 +03:00
Ilya Sosnovsky
63d76b5991
Merge pull request #10 from vabrn/patch-1
Update ccd.tpl
2021-10-05 15:05:22 +03:00
Andrii Veklychev
fa9022ee1b add option to specify custom user template path 2021-07-22 01:44:04 +03:00
CheyenneForbes
44c2b34f6d
Ensure rows is set to an array 2021-07-16 03:03:12 -05:00
Dmitry Shurupov
05d7462a79
Merge pull request #21 from flant/boris/readme_fix
Update README.md
2021-04-22 15:45:13 +07:00
Uzhinskiy Boris
f4d8a4ab01
Update README.md
correct names for coreutils & easy-rsa packages
2021-04-22 11:42:57 +03:00
Dmitry Shurupov
c954d05f26
Announcement link & story for README 2021-03-26 15:04:34 +07:00
vabrn
8c36329b0c
Update ccd.tpl 2021-03-22 13:37:26 +03:00
Ilya Sosnovsky
edaf13f557 renaming; fixes 2021-03-17 13:44:12 +03:00
Dmitry Shurupov
f5eb9eaff6 Fixing metrics screenshot in README 2021-03-17 16:53:00 +07:00
Dmitry Shurupov
f1251307bd Adding screenshots to README 2021-03-17 16:50:43 +07:00
Dmitry Shurupov
5c43db3269
More details in README 2021-03-17 16:33:46 +07:00
Dmitry Shurupov
e0d2355024
Tiny fixes in README 2021-03-17 16:30:12 +07:00
Dmitry Shurupov
e7eb841805
Adding Features & small fixes 2021-03-17 16:27:08 +07:00
Dmitry Shurupov
9629f51e76 Renaming to ovpn-admin 2021-03-17 16:05:37 +07:00
Dmitry Shurupov
5c5c874788
Renaming to ovpn-admin 2021-03-17 15:23:04 +07:00
Ilya Sosnovsky
f735051916 small fixes 2021-03-15 11:35:12 +03:00
Ilya Sosnovsky
30bdc3243f Fix template rendering bug 2021-03-10 18:02:23 +03:00
Ilya Sosnovsky
409377d014 change action syntax 2021-03-09 18:28:56 +03:00
Ilya Sosnovsky
8fe289ad5f werf2docker 2021-03-09 18:14:25 +03:00
Ilya Sosnovsky
c31a40fa13 workflows changes 2021-03-09 17:37:01 +03:00
Ilya Sosnovsky
c145fc7a10
Update werf.yaml
fix openvpn dockerfile name
2021-03-09 16:23:21 +03:00
Ilya Sosnovsky
f96af697e8
Update build.yaml 2021-03-09 16:13:00 +03:00
Ilya Sosnovsky
5586ab7d06
Update build.yaml 2021-03-09 16:00:47 +03:00
Vasily Marmer
0e6d21ab44 change docker login action 2021-03-09 14:14:46 +03:00
Vasily Marmer
f14e49bed5 change docker login to action 2021-03-09 14:12:29 +03:00
Ilya Sosnovsky
db354c0ae0
fix syntax in workflow
fix syntax in workflow
2021-03-09 12:57:29 +03:00
Ilya Sosnovsky
0a410a1724
Update build.yaml 2021-03-09 12:55:54 +03:00
Ilya Sosnovsky
aebab90257 rename actions dir 2021-03-09 12:51:41 +03:00
Ilya Sosnovsky
da53aaae63
Merge pull request #2 from flant/v1.6
v1.6
2021-03-09 12:36:17 +03:00
Ilya Sosnovsky
75a383f259
Merge pull request #3 from flant/actions
Actions
2021-03-09 12:22:21 +03:00
Ilya Sosnovsky
6eb14a0397
Update build.yaml
use local WERF_STAGES_STORAGE
2021-03-09 12:21:56 +03:00
Vasily Marmer
d7b58621ee init 2021-03-05 12:07:08 +03:00
Ilya Sosnovsky
bcd7b9ee62
Update Dockerfile 2021-03-01 12:22:07 +03:00
Ilya Sosnovsky
4f5847f7c2 added disclaimer 2021-02-26 15:25:10 +03:00
Ilya Sosnovsky
ece35822c1 Refactoring; docs; embeded frontend; fixes 2021-02-26 15:11:13 +03:00
Ilya Sosnovsky
6d6c35fb9a bugfix for opvn status date layout 2021-02-20 18:46:30 +03:00
Ilya Sosnovsky
a1c0bb6b4b Fix docs 2021-02-20 16:40:14 +03:00
Ilya Sosnovsky
3db757659a
Merge pull request #1 from pashcovich/draft-password-auth
Additional password auth
Multiple mgmt interface usgae
Layout changes
Small fixes
2021-02-20 15:51:28 +03:00
Ilya Sosnovsky
3614ab6ba5 Additional password auth; Multiple mgmt interface usgae; Fixes; style changes; 2021-02-20 15:48:41 +03:00
Ilya Sosnovsky
bec0e738d1 Add passwd auth feature; Fixes; Some layout changes 2021-02-15 09:03:38 +03:00
Ilya Sosnovsky
0af5fc3622 Fix server certs expire metrics math 2020-11-27 12:00:03 +03:00
Ilya Sosnovsky
8d82c43fa2 Fix cert expire metrics math 2020-11-27 11:56:42 +03:00
Ilya Sosnovsky
bb257692af Code refactoring to singleton; Added metrics 2020-11-27 10:23:59 +03:00
Ilya Sosnovsky
7df1ea3a0d Bug fixes; Small changes for slave UI 2020-11-20 19:11:58 +03:00
Ilya Sosnovsky
bf37066475 Fixed slave server crashed issue if the master is unvailable 2020-11-19 20:08:55 +03:00
Ilya Sosnovsky
9f8098ab03 Addded master-slave mode for openvpn-admin 2020-11-17 20:48:26 +03:00
Ilya Sosnovsky
f3a7f1f869 Added persistence for revoked accounts filter 2020-11-03 19:32:37 +03:00
57 changed files with 9230 additions and 6852 deletions

View file

@ -3,12 +3,24 @@
*.iml
out
gen
.github
easyrsa
easyrsa_master
easyrsa_slave
ccd
ccd_master
ccd_slave
werf.yaml
frontend/node_modules
frontend/static/dist
openvpn-web-ui
openvpn-ui
openvpn-admin
ovpn-admin
docker-compose.yaml
docker-compose-slave.yaml
img
dashboard
.helm

24
.editorconfig Normal file
View file

@ -0,0 +1,24 @@
; https://editorconfig.org/
root = true
[*]
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[{Makefile,go.mod,go.sum,*.go,.gitmodules}]
indent_style = tab
indent_size = 4
[*.md]
indent_size = 4
trim_trailing_whitespace = false
eclint_indent_style = unset
[Dockerfile]
indent_size = 4

35
.github/workflows/publish-latest.yaml vendored Normal file
View file

@ -0,0 +1,35 @@
name: Build and publish latest tag to Docker Hub (releases only)
on:
release:
types: [created]
jobs:
build:
name: build latest images for release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}
- name: Push openvpn image to Docker Hub
uses: docker/build-push-action@v4
with:
tags: flant/ovpn-admin:openvpn-latest
platforms: linux/amd64,linux/arm64,linux/arm
file: Dockerfile.openvpn
push: true
- name: Push ovpn-admin image to Docker Hub
uses: docker/build-push-action@v4
with:
tags: flant/ovpn-admin:latest
platforms: linux/amd64,linux/arm64,linux/arm
file: Dockerfile
push: true

39
.github/workflows/publish-tag.yaml vendored Normal file
View file

@ -0,0 +1,39 @@
name: Build and publish tags to Docker Hub (tags only)
on:
push:
tags:
- '*'
jobs:
build:
name: build images for tag
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Get the version
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}
- name: Push openvpn image to Docker Hub
uses: docker/build-push-action@v4
with:
tags: flant/ovpn-admin:openvpn-${{ steps.get_version.outputs.VERSION }}
platforms: linux/amd64,linux/arm64,linux/arm
file: Dockerfile.openvpn
push: true
- name: Push ovpn-admin image to Docker Hub
uses: docker/build-push-action@v4
with:
tags: flant/ovpn-admin:${{ steps.get_version.outputs.VERSION }}
platforms: linux/amd64,linux/arm64,linux/arm
file: Dockerfile
push: true

29
.github/workflows/release.yaml vendored Normal file
View file

@ -0,0 +1,29 @@
name: Build and publish binaries (releases only)
on:
release:
types: [created]
jobs:
releases-matrix:
name: Release Go Binary
runs-on: ubuntu-latest
env:
CGO_ENABLED: 1
strategy:
matrix:
goos: [linux]
goarch: ["386", "amd64"]
steps:
- name: checkout code
uses: actions/checkout@v2
- name: build binaries
uses: wangyoucao577/go-release-action@v1.40
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goversion: 1.17
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
build_command: bash -ex ./build.sh
pre_command: bash -ex ./install-deps.sh
binary_name: "ovpn-admin"
asset_name: ovpn-admin-${{ matrix.goos }}-${{ matrix.goarch }}

29
.github/workflows/release_arm.yaml vendored Normal file
View file

@ -0,0 +1,29 @@
name: Build and publish arm binaries (releases only)
on:
release:
types: [created]
jobs:
releases-matrix:
name: Release Go Binary
runs-on: ubuntu-latest
env:
CGO_ENABLED: 1
strategy:
matrix:
goos: [linux]
goarch: ["arm", "arm64"]
steps:
- name: checkout code
uses: actions/checkout@v2
- name: build binaries
uses: wangyoucao577/go-release-action@v1.40
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goversion: 1.17
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
build_command: bash -ex ./build_arm.sh
pre_command: bash -ex ./install-deps-arm.sh
binary_name: "ovpn-admin"
asset_name: ovpn-admin-${{ matrix.goos }}-${{ matrix.goarch }}

23
.gitignore vendored
View file

@ -1,6 +1,21 @@
easyrsa
easyrsa_master
easyrsa_slave
ccd
openvpn-web-ui
openvpn-ui
openvpn-admin
frontend/node_modules
ccd_master
ccd_slave
openvpn-web-ui*
openvpn-ui*
openvpn-admin*
ovpn-admin*
frontend/node_modules
main-packr.go
packrd/
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln

View file

@ -1,33 +0,0 @@
#!/bin/bash
set -x
EASY_RSA_LOC="/etc/openvpn/easyrsa"
SERVER_CERT="${EASY_RSA_LOC}/pki/issued/server.crt"
cd $EASY_RSA_LOC
if [ -e "$SERVER_CERT" ]; then
echo "found existing certs - reusing"
else
easyrsa init-pki
cp -R /usr/share/easy-rsa/* $EASY_RSA_LOC/pki
echo "ca" | easyrsa build-ca nopass
easyrsa build-server-full server nopass
easyrsa gen-dh
openvpn --genkey --secret ./pki/ta.key
fi
easyrsa gen-crl
iptables -t nat -A POSTROUTING -s 172.16.100.0/255.255.255.0 ! -d 172.16.100.0/255.255.255.0 -j MASQUERADE
mkdir -p /dev/net
if [ ! -c /dev/net/tun ]; then
mknod /dev/net/tun c 10 200
fi
cp -f /etc/openvpn/setup/openvpn.conf /etc/openvpn/openvpn.conf
[ -d $EASY_RSA_LOC/pki ] && chmod 755 $EASY_RSA_LOC/pki
[ -f $EASY_RSA_LOC/pki/crl.pem ] && chmod 644 $EASY_RSA_LOC/pki/crl.pem
mkdir -p /etc/openvpn/ccd
openvpn --config /etc/openvpn/openvpn.conf --client-config-dir /etc/openvpn/ccd

View file

@ -1,19 +1,20 @@
FROM golang:1.14.2-alpine3.11 AS backend-builder
COPY . /app
#RUN apk --no-cache add build-base git gcc
RUN cd /app && env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags='-extldflags "-static" -s -w' -o openvpn-admin
FROM node:14.2-alpine3.11 AS frontend-builder
FROM node:16-alpine3.15 AS frontend-builder
COPY frontend/ /app
RUN cd /app && npm install && npm run build
RUN apk add --update python3 make g++ && cd /app && npm install && npm run build
FROM alpine:3.11
FROM golang:1.17.3-buster AS backend-builder
RUN go install github.com/gobuffalo/packr/v2/packr2@latest
COPY --from=frontend-builder /app/static /app/frontend/static
COPY . /app
ARG TARGETARCH
RUN cd /app && packr2 && env CGO_ENABLED=1 GOOS=linux GOARCH=${TARGETARCH} go build -a -tags netgo -ldflags '-linkmode external -extldflags -static -s -w' -o ovpn-admin && packr2 clean
FROM alpine:3.16
WORKDIR /app
COPY --from=backend-builder /app/openvpn-admin /app
COPY --from=frontend-builder /app/static /app/static
COPY client.conf.tpl /app/client.conf.tpl
COPY ccd.tpl /app/ccd.tpl
RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \
apk add --update bash easy-rsa && \
COPY --from=backend-builder /app/ovpn-admin /app
ARG TARGETARCH
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.4/openvpn-user-linux-${TARGETARCH}.tar.gz -O - | tar xz -C /usr/local/bin && \
rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*
RUN if [ -f "/usr/local/bin/openvpn-user-${TARGETARCH}" ]; then ln -s /usr/local/bin/openvpn-user-${TARGETARCH} /usr/local/bin/openvpn-user; fi

View file

@ -1,7 +1,9 @@
FROM alpine:3.11
RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \
apk add --update bash openvpn easy-rsa && \
FROM alpine:3.16
ARG TARGETARCH
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.4/openvpn-user-linux-${TARGETARCH}.tar.gz -O - | tar xz -C /usr/local/bin && \
rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*
COPY .werffiles /etc/openvpn/setup
RUN chmod +x /etc/openvpn/setup/configure.sh
RUN if [ -f "/usr/local/bin/openvpn-user-${TARGETARCH}" ]; then ln -s /usr/local/bin/openvpn-user-${TARGETARCH} /usr/local/bin/openvpn-user; fi
COPY setup/ /etc/openvpn/setup
RUN chmod +x /etc/openvpn/setup/configure.sh

178
README.md
View file

@ -1 +1,177 @@
# openvpn-web-ui
# ovpn-admin
Simple web UI to manage OpenVPN users, their certificates & routes in Linux. While backend is written in Go, frontend is based on Vue.js.
Originally created in [Flant](https://flant.com/) for internal needs & used for years, then updated to be more modern and [publicly released](https://medium.com/flant-com/introducing-ovpn-admin-a-web-interface-to-manage-openvpn-users-d81705ad8f23) in March'21. Please note that the project is currently on pause, no new Issues or PRs are accepted.
***DISCLAIMER!** This project was created for experienced users (system administrators) and private (e.g., protected by network policies) environments only. Thus, it is not implemented with security in mind (e.g., it doesn't strictly check all parameters passed by users, etc.). It also relies heavily on files and fails if required files aren't available.*
## Features
* 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;
* (optionally) Operating in a master/slave mode (syncing certs & CCD with other server);
* (optionally) Specifying/changing password for additional authorization in OpenVPN;
* (optionally) Specifying the Kubernetes LoadBalancer if it's used in front of the OpenVPN server (to get an automatically defined `remote` in the `client.conf.tpl` template).
* (optionally) Storing certificates and other files in Kubernetes Secrets (**Attention, this feature is experimental!**).
### Screenshots
Managing users in ovpn-admin:
![ovpn-admin UI](https://raw.githubusercontent.com/flant/ovpn-admin/master/img/ovpn-admin-users.png)
An example of dashboard made using ovpn-admin metrics:
![ovpn-admin metrics](https://raw.githubusercontent.com/flant/ovpn-admin/master/img/ovpn-admin-metrics.png)
## Installation
### 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.
Commands to execute:
```bash
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
Requirements. You need Linux with the following components installed:
- [golang](https://golang.org/doc/install)
- [packr2](https://github.com/gobuffalo/packr#installation)
- [nodejs/npm](https://nodejs.org/en/download/package-manager/)
Commands to execute:
```bash
git clone https://github.com/flant/ovpn-admin.git
cd ovpn-admin
./bootstrap.sh
./build.sh
./ovpn-admin
```
(Please don't forget to configure all needed params in advance.)
### 3. Prebuilt binary
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.
## 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.5 with only tls-auth mode
* not tested with EasyRsa version > 3.0.8
* status of users connections update every 28 second(*no need to ask why =)*)
## Usage
```
usage: ovpn-admin [<flags>]
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)
--listen.port="8080" port for ovpn-admin
(or OVPN_LISTEN_PORT)
--listen.base-url="/" base URL for ovpn-admin web files
(or $OVPN_LISTEN_BASE_URL)
--role="master" server role, master or slave
(or OVPN_ROLE)
--master.host="http://127.0.0.1"
(or OVPN_MASTER_HOST) URL for the master server
--master.basic-auth.user="" user for master server's Basic Auth
(or OVPN_MASTER_USER)
--master.basic-auth.password=""
(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)
--master.sync-token=TOKEN master host data sync security token
(or OVPN_MASTER_TOKEN)
--ovpn.network="172.16.100.0/24"
(or OVPN_NETWORK) NETWORK/MASK_PREFIX for OpenVPN server
--ovpn.server=HOST:PORT:PROTOCOL ...
(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
--ovpn.service="openvpn-external"
(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;
can have multiple values
--metrics.path="/metrics" URL path for exposing collected metrics
(or OVPN_METRICS_PATH)
--easyrsa.path="./easyrsa/" path to easyrsa dir
(or EASYRSA_PATH)
--easyrsa.index-path="./easyrsa/pki/index.txt"
(or OVPN_INDEX_PATH) path to easyrsa index file
--ccd enable client-config-dir
(or OVPN_CCD)
--ccd.path="./ccd" path to client-config-dir
(or OVPN_CCD_PATH)
--templates.clientconfig-path=""
(or OVPN_TEMPLATES_CC_PATH) path to custom client.conf.tpl
--templates.ccd-path="" path to custom ccd.tpl
(or OVPN_TEMPLATES_CCD_PATH)
--auth.password enable additional password authorization
(or OVPN_AUTH)
--auth.db="./easyrsa/pki/users.db"
(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)
--log.format set log format: text, json (default text)
(or LOG_FORMAT)
--storage.backend storage backend: filesystem, kubernetes.secrets (default filesystem)
(or STORAGE_BACKEND)
--version show application version
```
## Further information
Please feel free to use [issues](https://github.com/flant/ovpn-admin/issues) and [discussions](https://github.com/flant/ovpn-admin/discussions) to get help from maintainers & community.

View file

@ -1,8 +1,6 @@
#!/bin/bash
#!/usr/bin/env bash
if [ ! -d easyrsa ]; then
mkdir easyrsa
fi
mkdir -p {easyrsa,ccd}
cd easyrsa
@ -15,6 +13,6 @@ if [ -d pki ]; then
fi
./easyrsa init-pki
echo "ca\n" | ./easyrsa build-ca nopass
echo "ca" | ./easyrsa build-ca nopass
./easyrsa build-server-full server nopass
./easyrsa build-client-full client nopass

View file

@ -1,3 +1,11 @@
#!/bin/bash
#!/usr/bin/env bash
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags='-extldflags "-static" -s -w' -o openvpn-admin
PATH=$PATH:~/go/bin
cd frontend && npm install && npm run build && cd ..
packr2
CGO_ENABLED=1 GOOS=linux GOARCH=${GOARCH:-amd64} go build -a -tags netgo -ldflags "-linkmode external -extldflags -static -s -w" $@
packr2 clean

18
build_arm.sh Executable file
View file

@ -0,0 +1,18 @@
#!/usr/bin/env bash
PATH=$PATH:~/go/bin
cd frontend && npm install && npm run build && cd ..
packr2
if [[ "$GOOS" == "linux" ]]; then
if [[ "$GOARCH" == "arm" ]]; then
CC=arm-linux-gnueabi-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm go build -a -tags netgo -ldflags "-linkmode external -extldflags -static -s -w" $@
fi
if [[ "$GOARCH" == "arm64" ]]; then
CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -a -tags netgo -ldflags "-linkmode external -extldflags -static -s -w" $@
fi
fi
packr2 clean

195
certificates.go Normal file
View file

@ -0,0 +1,195 @@
package main
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"math/big"
"time"
)
// decode certificate from PEM to x509
func decodeCert(certPEMBytes []byte) (cert *x509.Certificate, err error) {
certPem, _ := pem.Decode(certPEMBytes)
certPemBytes := certPem.Bytes
cert, err = x509.ParseCertificate(certPemBytes)
if err != nil {
return
}
return
}
// decode private key from PEM to RSA format
func decodePrivKey(privKey []byte) (key *rsa.PrivateKey, err error) {
privKeyPem, _ := pem.Decode(privKey)
key, err = x509.ParsePKCS1PrivateKey(privKeyPem.Bytes)
if err == nil {
return
}
tmp, err := x509.ParsePKCS8PrivateKey(privKeyPem.Bytes)
if err != nil {
err = errors.New("error parse private key")
return
}
key, _ = tmp.(*rsa.PrivateKey)
return
}
// return PEM encoded private key
func genPrivKey() (privKeyPEM *bytes.Buffer, err error) {
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
//privKeyPKCS1 := x509.MarshalPKCS1PrivateKey(privKey)
privKeyPKCS8, err := x509.MarshalPKCS8PrivateKey(privKey)
if err != nil {
return
}
privKeyPEM = new(bytes.Buffer)
err = pem.Encode(privKeyPEM, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: privKeyPKCS8,
})
return
}
// return PEM encoded certificate
func genCA(privKey *rsa.PrivateKey) (issuerPEM *bytes.Buffer, err error) {
serialNumberRange := new(big.Int).Lsh(big.NewInt(1), 128)
issuerSerial, err := rand.Int(rand.Reader, serialNumberRange)
issuerTemplate := x509.Certificate{
BasicConstraintsValid: true,
IsCA: true,
SerialNumber: issuerSerial,
Subject: pkix.Name{
CommonName: "ca",
},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0),
}
issuerBytes, err := x509.CreateCertificate(rand.Reader, &issuerTemplate, &issuerTemplate, &privKey.PublicKey, privKey)
if err != nil {
return
}
issuerPEM = new(bytes.Buffer)
_ = pem.Encode(issuerPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: issuerBytes,
})
return
}
// return PEM encoded certificate
func genServerCert(privKey, caPrivKey *rsa.PrivateKey, ca *x509.Certificate, cn string) (issuerPEM *bytes.Buffer, err error) {
serialNumberRange := new(big.Int).Lsh(big.NewInt(1), 128)
serial, err := rand.Int(rand.Reader, serialNumberRange)
template := x509.Certificate{
BasicConstraintsValid: true,
DNSNames: []string{cn},
SerialNumber: serial,
Subject: pkix.Name{
CommonName: cn,
},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
NotBefore: time.Now(),
NotAfter: ca.NotAfter,
}
issuerBytes, err := x509.CreateCertificate(rand.Reader, &template, ca, &privKey.PublicKey, caPrivKey)
if err != nil {
return
}
issuerPEM = new(bytes.Buffer)
_ = pem.Encode(issuerPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: issuerBytes,
})
return
}
// return PEM encoded certificate
func genClientCert(privKey, caPrivKey *rsa.PrivateKey, ca *x509.Certificate, cn string) (issuerPEM *bytes.Buffer, err error) {
serialNumberRange := new(big.Int).Lsh(big.NewInt(1), 128)
serial, err := rand.Int(rand.Reader, serialNumberRange)
template := x509.Certificate{
BasicConstraintsValid: true,
DNSNames: []string{cn},
SerialNumber: serial,
Subject: pkix.Name{
CommonName: cn,
},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
NotBefore: time.Now(),
NotAfter: ca.NotAfter,
}
issuerBytes, err := x509.CreateCertificate(rand.Reader, &template, ca, &privKey.PublicKey, caPrivKey)
if err != nil {
return
}
issuerPEM = new(bytes.Buffer)
_ = pem.Encode(issuerPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: issuerBytes,
})
return
}
// return PEM encoded CRL
func genCRL(certs []*RevokedCert, ca *x509.Certificate, caKey *rsa.PrivateKey) (crlPEM *bytes.Buffer, err error) {
var revokedCertificates []pkix.RevokedCertificate
for _, cert := range certs {
revokedCertificates = append(revokedCertificates, pkix.RevokedCertificate{SerialNumber: cert.Cert.SerialNumber, RevocationTime: cert.RevokedTime})
}
revocationList := &x509.RevocationList{
//SignatureAlgorithm: x509.SHA256WithRSA,
RevokedCertificates: revokedCertificates,
Number: big.NewInt(1),
ThisUpdate: time.Now(),
NextUpdate: time.Now().Add(180 * time.Hour * 24),
//ExtraExtensions: []pkix.Extension{},
}
crl, err := x509.CreateRevocationList(rand.Reader, revocationList, ca, caKey)
if err != nil {
return nil, err
}
crlPEM = new(bytes.Buffer)
err = pem.Encode(crlPEM, &pem.Block{
Type: "X509 CRL",
Bytes: crl,
})
if err != nil {
return
}
return
}

View file

@ -1,26 +0,0 @@
remote {{ .Host }} {{ .Port }} tcp
verb 4
client
nobind
dev tun
cipher AES-128-CBC
key-direction 1
#redirect-gateway def1
tls-client
remote-cert-tls server
# for update resolv.conf on ubuntu
#script-security 2 system
#up /etc/openvpn/update-resolv-conf
#down /etc/openvpn/update-resolv-conf
<cert>
{{ .Cert -}}
</cert>
<key>
{{ .Key -}}
</key>
<ca>
{{ .CA -}}
</ca>
<tls-auth>
{{ .TLS -}}
</tls-auth>

974
dashboard/ovpn-admin.json Normal file
View file

@ -0,0 +1,974 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 54,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"fieldConfig": {
"defaults": {
"decimals": 1,
"mappings": [],
"thresholds": {
"mode": "percentage",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 10
}
]
},
"unit": "d"
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 7,
"x": 5,
"y": 0
},
"id": 2,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"last"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "8.5.2",
"targets": [
{
"expr": "ovpn_server_cert_expire",
"interval": "",
"legendFormat": "",
"refId": "A"
}
],
"title": "Server cert valid time",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"fieldConfig": {
"defaults": {
"decimals": 1,
"mappings": [],
"thresholds": {
"mode": "percentage",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 10
}
]
},
"unit": "d"
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 7,
"x": 12,
"y": 0
},
"id": 3,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"last"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "8.5.2",
"targets": [
{
"expr": "ovpn_server_ca_cert_expire",
"interval": "",
"legendFormat": "",
"refId": "A"
}
],
"title": "Server CA cert valid time",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "semi-dark-orange",
"value": 200
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 6,
"x": 0,
"y": 5
},
"id": 4,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"last"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "8.5.2",
"targets": [
{
"expr": "ovpn_clients_total",
"interval": "",
"legendFormat": "",
"refId": "A"
}
],
"title": "Total clients",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 6,
"x": 6,
"y": 5
},
"id": 5,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"last"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "8.5.2",
"targets": [
{
"expr": "ovpn_clients_connected",
"interval": "",
"legendFormat": "",
"refId": "A"
}
],
"title": "Connected clients",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "semi-dark-orange",
"value": 10
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 6,
"x": 12,
"y": 5
},
"id": 7,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"last"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "8.5.13",
"targets": [
{
"expr": "ovpn_clients_expired",
"interval": "",
"legendFormat": "",
"refId": "A"
}
],
"title": "Revoked clients",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 1
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 6,
"x": 18,
"y": 5
},
"id": 6,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"last"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "8.5.2",
"targets": [
{
"expr": "ovpn_clients_expired",
"interval": "",
"legendFormat": "",
"refId": "A"
}
],
"title": "Expired clients",
"type": "stat"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"fieldConfig": {
"defaults": {
"links": []
},
"overrides": []
},
"fill": 2,
"fillGradient": 0,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 10
},
"hiddenSeries": false,
"id": 9,
"legend": {
"avg": false,
"current": false,
"hideEmpty": true,
"max": false,
"min": false,
"show": false,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null as zero",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "8.5.2",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "ovpn_client_bytes_received",
"interval": "",
"legendFormat": "{{ client }}",
"refId": "A"
}
],
"thresholds": [],
"timeRegions": [],
"title": "Сlient bytes received",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"mode": "time",
"show": true,
"values": []
},
"yaxes": [
{
"format": "decbytes",
"logBase": 1,
"show": true
},
{
"format": "short",
"logBase": 1,
"show": false
}
],
"yaxis": {
"align": false
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"fieldConfig": {
"defaults": {
"links": []
},
"overrides": []
},
"fill": 2,
"fillGradient": 0,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 10
},
"hiddenSeries": false,
"id": 10,
"legend": {
"avg": false,
"current": false,
"hideEmpty": true,
"max": false,
"min": false,
"show": false,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null as zero",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "8.5.2",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "ovpn_client_bytes_sent",
"interval": "",
"legendFormat": "{{ client }}",
"refId": "A"
}
],
"thresholds": [],
"timeRegions": [],
"title": "Сlient bytes sent",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"mode": "time",
"show": true,
"values": []
},
"yaxes": [
{
"format": "decbytes",
"logBase": 1,
"show": true
},
{
"format": "short",
"logBase": 1,
"show": false
}
],
"yaxis": {
"align": false
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"fieldConfig": {
"defaults": {
"links": []
},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 18
},
"hiddenSeries": false,
"id": 16,
"legend": {
"avg": false,
"current": false,
"hideEmpty": true,
"max": false,
"min": false,
"show": false,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null as zero",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "8.5.2",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "rate(ovpn_client_bytes_received[1m])",
"interval": "",
"legendFormat": "{{ client }}",
"refId": "A"
}
],
"thresholds": [],
"timeRegions": [],
"title": "Clients bytes received rate",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"mode": "time",
"show": true,
"values": []
},
"yaxes": [
{
"$$hashKey": "object:93",
"format": "Bps",
"logBase": 1,
"show": true
},
{
"$$hashKey": "object:94",
"format": "short",
"logBase": 1,
"show": false
}
],
"yaxis": {
"align": false
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"fieldConfig": {
"defaults": {
"links": []
},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 18
},
"hiddenSeries": false,
"id": 17,
"legend": {
"avg": false,
"current": false,
"hideEmpty": true,
"max": false,
"min": false,
"show": false,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null as zero",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "8.5.2",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "rate(ovpn_client_bytes_sent[1m])",
"interval": "",
"legendFormat": "{{ client }}",
"refId": "A"
}
],
"thresholds": [],
"timeRegions": [],
"title": "Client bytes sent rate ",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"mode": "time",
"show": true,
"values": []
},
"yaxes": [
{
"$$hashKey": "object:174",
"format": "Bps",
"logBase": 1,
"show": true
},
{
"$$hashKey": "object:175",
"format": "short",
"logBase": 1,
"show": false
}
],
"yaxis": {
"align": false
}
},
{
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"description": "value show last connection check time",
"fieldConfig": {
"defaults": {
"custom": {
"align": "center",
"displayMode": "auto",
"width": 20
},
"mappings": [],
"noValue": "Currently there are no connections",
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
}
]
},
"unit": "dateTimeAsIso"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 26
},
"id": 12,
"maxDataPoints": 1,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"reduceOptions": {
"calcs": [
"last"
],
"fields": "",
"values": false
}
},
"pluginVersion": "7.0.6",
"targets": [
{
"expr": "ovpn_client_connection_info * 1000",
"format": "time_series",
"interval": "",
"legendFormat": "{{ client }}-{{ip}}",
"refId": "A"
}
],
"title": "Connection info",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"description": "value shows when connection was started",
"fieldConfig": {
"defaults": {
"custom": {
"align": "center",
"displayMode": "auto",
"width": 20
},
"mappings": [],
"noValue": "Currently there are no connections",
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
}
]
},
"unit": "dateTimeAsIso"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 26
},
"id": 13,
"maxDataPoints": 1,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"reduceOptions": {
"calcs": [
"last"
],
"fields": "",
"values": false
}
},
"pluginVersion": "7.0.6",
"targets": [
{
"expr": "ovpn_client_connection_from * 1000",
"format": "time_series",
"interval": "",
"legendFormat": "{{ client }}-{{ip}}",
"refId": "A"
}
],
"title": "Connection from",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"fieldConfig": {
"defaults": {
"custom": {},
"mappings": [],
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 7
},
{
"color": "dark-orange",
"value": 14
},
{
"color": "#EAB839",
"value": 30
},
{
"color": "green",
"value": 31
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 14,
"w": 24,
"x": 0,
"y": 34
},
"id": 19,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"last"
],
"fields": "",
"values": false
}
},
"pluginVersion": "7.0.6",
"targets": [
{
"expr": "ovpn_client_cert_expire ",
"format": "time_series",
"interval": "",
"legendFormat": "{{ client }}",
"refId": "A"
}
],
"title": "Client cert valid days",
"type": "stat"
}
],
"refresh": false,
"schemaVersion": 36,
"style": "dark",
"tags": [],
"templating": {
"list": [
{
"current": {
"selected": false,
"text": "default",
"value": "default"
},
"hide": 0,
"includeAll": false,
"multi": false,
"label": "Prometheus",
"name": "ds_prometheus",
"options": [],
"query": "prometheus",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"type": "datasource"
}
]
},
"time": {
"from": "now-15m",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
]
},
"timezone": "",
"title": "Ovpn-Admin",
"uid": "Z7qmFI0Gk",
"version": 1,
"weekStart": ""
}

30
docker-compose-slave.yaml Normal file
View file

@ -0,0 +1,30 @@
version: '3'
services:
openvpn:
build:
context: .
dockerfile: Dockerfile.openvpn
image: openvpn:local
command: /etc/openvpn/setup/configure.sh
environment:
- OVPN_ROLE=slave
cap_add:
- NET_ADMIN
ports:
- 7778:1194 # for openvpn
- 8081:8080 # for ovpn-admin because of network_mode
volumes:
- ./easyrsa_slave:/etc/openvpn/easyrsa
- ./ccd_slave:/etc/openvpn/ccd
ovpn-admin:
build:
context: .
image: ovpn-admin:local
command: /app/ovpn-admin --debug --ovpn.network="172.16.100.0/22" --master.sync-token="TOKEN" --master.host="http://172.20.0.1:8080" --role="slave" --ovpn.server="127.0.0.1:7777:tcp" --ovpn.server="127.0.0.1:7778:tcp" --easyrsa.path="/mnt/easyrsa" --easyrsa.index-path="/mnt/easyrsa/pki/index.txt"
environment:
- OVPN_SLAVE=1
network_mode: service:openvpn
volumes:
- ./easyrsa_slave:/mnt/easyrsa
- ./ccd_slave:/mnt/ccd

View file

@ -7,20 +7,36 @@ services:
dockerfile: Dockerfile.openvpn
image: openvpn:local
command: /etc/openvpn/setup/configure.sh
environment:
OVPN_SERVER_NET: "192.168.100.0"
OVPN_SERVER_MASK: "255.255.255.0"
OVPN_PASSWD_AUTH: "true"
cap_add:
- NET_ADMIN
ports:
- 7777:1194
- 7777:1194 # for openvpn
- 8080:8080 # for ovpn-admin because of network_mode
volumes:
- ./easyrsa:/etc/openvpn/easyrsa
- ./ccd:/etc/openvpn/ccd
openvpn-admin:
- ./easyrsa_master:/etc/openvpn/easyrsa
- ./ccd_master:/etc/openvpn/ccd
ovpn-admin:
build:
context: .
image: openvpn-admin:local
command: /app/openvpn-admin --debug --ovpn.network="172.16.100.0/22" --mgmt.host="openvpn"
ports:
- 8080:8080
image: ovpn-admin:local
command: /app/ovpn-admin
environment:
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:/mnt/easyrsa
- ./ccd:/mnt/ccd
- ./easyrsa_master:/mnt/easyrsa
- ./ccd_master:/mnt/ccd

View file

@ -1,6 +1,6 @@
#!/bin/bash
image="node:14.2-alpine3.11"
image="node:16.13.0-alpine3.12"
uid="$(id -u $USER)"
docker run -u $uid -w /app -v $(pwd):/app $image npm i && \

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
{
"name": "openvpn-admin",
"name": "ovpn-admin",
"description": "Vue.js admin ui for openvpn and easyrsa",
"version": "1.0.1a",
"author": "vitaliy.snurnitsin@gmail.com",
@ -7,13 +7,18 @@
"private": true,
"scripts": {
"dev": "cross-env NODE_ENV=development webpack-dev-server --hot",
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
"build": "cross-env NODE_ENV=production webpack --progress"
},
"dependencies": {
"axios": "^0.19.2",
"vue": "^2.6.12",
"vue-clipboard2": "^0.2.1",
"vue-good-table": "^2.21.1"
"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-style-loader": "^4.1.3"
},
"browserslist": [
"> 1%",
@ -21,23 +26,23 @@
"not ie <= 8"
],
"devDependencies": {
"@babel/core": "^7.8.6",
"@babel/plugin-proposal-class-properties": "^7.0.0",
"@babel/plugin-proposal-json-strings": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-syntax-import-meta": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-loader": "^8.0.0",
"cross-env": "^7.0.0",
"css-loader": "^3.4.2",
"file-loader": "^5.1.0",
"node-sass": "^4.13.1",
"sass-loader": "^8.0.2",
"terser-webpack-plugin": "^2.3.5",
"vue-loader": "^15.9.0",
"vue-template-compiler": "^2.6.11",
"webpack": "^4.42.0",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3"
"@babel/core": "^7.16.5",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/plugin-proposal-json-strings": "^7.16.7",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/preset-env": "^7.16.5",
"babel-loader": "^8.2.3",
"cross-env": "^7.0.3",
"css-loader": "^6.5.1",
"file-loader": "^6.2.0",
"node-sass": "^7.0.1",
"sass-loader": "^12.4.0",
"terser-webpack-plugin": "^5.3.0",
"vue-loader": "^17.0.0",
"vue-template-compiler": "^2.6.14",
"webpack": "^5.65.0",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.7.2"
}
}

View file

@ -1,11 +1,15 @@
import Vue from 'vue';
import axios from 'axios';
import VueCookies from 'vue-cookies'
import BootstrapVue from 'bootstrap-vue'
import VueClipboard from 'vue-clipboard2'
import Notifications from 'vue-notification'
import VueGoodTablePlugin from 'vue-good-table'
import 'vue-good-table/dist/vue-good-table.css'
Vue.use(VueCookies)
Vue.use(VueClipboard)
Vue.use(BootstrapVue)
Vue.use(Notifications)
Vue.use(VueGoodTablePlugin)
var axios_cfg = function(url, data='', type='form') {
@ -52,6 +56,11 @@ new Vue({
field: 'AccountStatus',
filterable: true,
},
{
label: 'Active Connections',
field: 'Connections',
filterable: true,
},
{
label: 'Expiration Date',
field: 'ExpirationDate',
@ -82,42 +91,116 @@ new Vue({
],
rows: [],
actions: [
{
name: 'u-change-password',
label: 'Change password',
class: 'btn-warning',
showWhenStatus: 'Active',
showForServerRole: ['master'],
showForModule: ['passwdAuth'],
},
{
name: 'u-revoke',
label: 'Revoke',
showWhenStatus: 'Active'
class: 'btn-warning',
showWhenStatus: 'Active',
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',
showWhenStatus: 'Revoked'
},
{
name: 'u-show-config',
label: 'Show config',
showWhenStatus: 'Active'
class: 'btn-primary',
showWhenStatus: 'Revoked',
showForServerRole: ['master'],
showForModule: ["core"],
},
// {
// name: 'u-show-config',
// label: 'Show config',
// class: 'btn-primary',
// showWhenStatus: 'Active',
// showForServerRole: ['master', 'slave'],
// showForModule: ["core"],
// },
{
name: 'u-download-config',
label: 'Download config',
showWhenStatus: 'Active'
class: 'btn-info',
showWhenStatus: 'Active',
showForServerRole: ['master', 'slave'],
showForModule: ["core"],
},
{
name: 'u-edit-ccd',
label: 'Edit routes',
showWhenStatus: 'Active'
class: 'btn-primary',
showWhenStatus: 'Active',
showForServerRole: ['master'],
showForModule: ["ccd"],
},
{
name: 'u-edit-ccd',
label: 'Show routes',
class: 'btn-primary',
showWhenStatus: 'Active',
showForServerRole: ['slave'],
showForModule: ["ccd"],
}
],
filters: {
hide_revoked: true
hideRevoked: true,
},
serverRole: "master",
lastSync: "unknown",
modulesEnabled: [],
u: {
newUserName: '',
// newUserPassword: 'nopass',
newUserPassword: '',
newUserCreateError: '',
newPassword: '',
passwordChangeStatus: '',
passwordChangeMessage: '',
rotateUserMessage: '',
deleteUserMessage: '',
modalNewUserVisible: false,
modalShowConfigVisible: false,
modalShowCcdVisible: false,
modalChangePasswordVisible: false,
modalRotateUserVisible: false,
modalDeleteUserVisible: false,
openvpnConfig: '',
ccd: {
Name: '',
@ -132,16 +215,20 @@ new Vue({
watch: {
},
mounted: function () {
this.u_get_data()
this.getUserData();
this.getServerSetting();
this.filters.hideRevoked = this.$cookies.isKey('hideRevoked') ? (this.$cookies.get('hideRevoked') == "true") : false
},
created() {
var _this = this
var _this = this;
_this.$root.$on('u-revoke', function (msg) {
var data = new URLSearchParams();
data.append('username', _this.username);
axios.request(axios_cfg('api/user/revoke', data, 'form'))
.then(function(response) {
_this.u_get_data();
_this.getUserData();
_this.$notify({title: 'User ' + _this.username + ' revoked!', type: 'warn'})
});
})
_this.$root.$on('u-unrevoke', function () {
@ -149,9 +236,20 @@ new Vue({
data.append('username', _this.username);
axios.request(axios_cfg('api/user/unrevoke', data, 'form'))
.then(function(response) {
_this.u_get_data();
_this.getUserData();
_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();
@ -183,23 +281,37 @@ new Vue({
_this.u.ccd = response.data;
});
})
_this.$root.$on('u-disconnect-use', function () {
_this.$root.$on('u-disconnect-user', function () {
_this.u.modalShowCcdVisible = true;
var data = new URLSearchParams();
data.append('username', _this.username);
axios.request(axios_cfg('api/user/disconnect', data, 'form'))
.then(function(response) {
_this.u.ccd = response.data;
console.log(response.data);
});
})
_this.$root.$on('u-change-password', function () {
_this.u.modalChangePasswordVisible = true;
var data = new URLSearchParams();
data.append('username', _this.username);
})
},
computed: {
customAddressEnabled: function () {
customAddressDynamic: function () {
return this.u.ccd.ClientAddress == "dynamic"
},
ccdApplyStatusCssClass: function () {
return this.u.ccdApplyStatus == 200 ? "alert-success" : "alert-danger"
},
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'} : {}
},
@ -209,55 +321,97 @@ new Vue({
modalShowCcdDisplay: function () {
return this.u.modalShowCcdVisible ? {display: 'flex'} : {}
},
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"
},
filteredRows: function() {
var _this = this;
if(_this.filters.hide_revoked) {
return _this.rows.filter(function(account) {
return account.AccountStatus === "Active";
if (this.filters.hideRevoked) {
return this.rows.filter(function(account) {
return account.AccountStatus == "Active"
});
} else {
return _this.rows;
return this.rows
}
}
},
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;
this.$root.$emit(e.target.dataset.name);
},
u_get_data: function() {
getUserData: function() {
var _this = this;
axios.request(axios_cfg('api/users/list'))
.then(function(response) {
_this.rows = Array.isArray(response.data) ? response.data : [];
});
},
getServerSetting: function() {
var _this = this;
axios.request(axios_cfg('api/server/settings'))
.then(function(response) {
_this.rows = response.data;
_this.serverRole = response.data.serverRole;
_this.modulesEnabled = response.data.modules;
if (_this.serverRole == "slave") {
axios.request(axios_cfg('api/sync/last/successful'))
.then(function(response) {
_this.lastSync = response.data;
});
}
});
},
create_user: function() {
createUser: function() {
var _this = this;
_this.u.newUserCreateError = "";
var data = new URLSearchParams();
data.append('username', _this.u.newUserName);
// data.append('password', this.u.newUserPassword);
data.append('password', _this.u.newUserPassword);
_this.username = _this.u.newUserName;
axios.request(axios_cfg('api/user/create', data, 'form'))
.then(function(response) {
_this.u_get_data();
_this.$notify({title: 'New user ' + _this.username + ' created', type: 'success'})
_this.u.modalNewUserVisible = false;
_this.u.newUserName = '';
// _this.u.newUserPassword = 'nopass';
_this.u.newUserPassword = '';
_this.getUserData();
})
.catch(function(error) {
_this.u.newUserCreateError = error.response.data;
_this.$notify({title: 'New user ' + _this.username + ' creation failed.', type: 'error'})
});
},
ccd_apply: function() {
ccdApply: function() {
var _this = this;
_this.u.ccdApplyStatus = "";
@ -267,11 +421,84 @@ new Vue({
.then(function(response) {
_this.u.ccdApplyStatus = 200;
_this.u.ccdApplyStatusMessage = response.data;
_this.$notify({title: 'Ccd for user ' + _this.username + ' applied', type: 'success'})
})
.catch(function(error) {
_this.u.ccdApplyStatus = error.response.status;
_this.u.ccdApplyStatusMessage = error.response.data;
_this.$notify({title: 'Ccd for user ' + _this.username + ' apply failed ', type: 'error'})
});
}
},
changeUserPassword: function(user) {
var _this = this;
_this.u.passwordChangeMessage = "";
var data = new URLSearchParams();
data.append('username', user);
data.append('password', _this.u.newPassword);
axios.request(axios_cfg('api/user/change-password', data, 'form'))
.then(function(response) {
_this.u.passwordChangeStatus = 200;
_this.u.newPassword = '';
_this.getUserData();
_this.u.modalChangePasswordVisible = false;
_this.$notify({title: 'Password for user ' + _this.username + ' changed!', type: 'success'})
})
.catch(function(error) {
_this.u.passwordChangeStatus = error.response.status;
_this.u.passwordChangeMessage = error.response.data.message;
_this.$notify({title: 'Changing password for user ' + _this.username + ' failed!', type: 'error'})
});
},
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);
}
.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;
}
@ -58,3 +66,7 @@ body {
background-color: #ffffff;
padding: 1rem;
}
.ccd-routes {
}

5
frontend/src/style.js Normal file
View file

@ -0,0 +1,5 @@
import 'normalize.css';
import 'vue-good-table/dist/vue-good-table.css'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
import './style.css'

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,461 +0,0 @@
/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */
/**
* 1. Change the default font family in all browsers (opinionated).
* 2. Correct the line height in all browsers.
* 3. Prevent adjustments of font size after orientation changes in
* IE on Windows Phone and in iOS.
*/
/* Document
========================================================================== */
html {
font-family: sans-serif; /* 1 */
line-height: 1.15; /* 2 */
-ms-text-size-adjust: 100%; /* 3 */
-webkit-text-size-adjust: 100%; /* 3 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers (opinionated).
*/
body {
margin: 0;
}
/**
* Add the correct display in IE 9-.
*/
article,
aside,
footer,
header,
nav,
section {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* Add the correct display in IE 9-.
* 1. Add the correct display in IE.
*/
figcaption,
figure,
main { /* 1 */
display: block;
}
/**
* Add the correct margin in IE 8.
*/
figure {
margin: 1em 40px;
}
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* 1. Remove the gray background on active links in IE 10.
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
*/
a {
background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */
}
/**
* Remove the outline on focused links when they are also active or hovered
* in all browsers (opinionated).
*/
a:active,
a:hover {
outline-width: 0;
}
/**
* 1. Remove the bottom border in Firefox 39-.
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Prevent the duplicate application of `bolder` by the next rule in Safari 6.
*/
b,
strong {
font-weight: inherit;
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font style in Android 4.3-.
*/
dfn {
font-style: italic;
}
/**
* Add the correct background and color in IE 9-.
*/
mark {
background-color: #ff0;
color: #000;
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
audio,
video {
display: inline-block;
}
/**
* Add the correct display in iOS 4-7.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Remove the border on images inside links in IE 10-.
*/
img {
border-style: none;
}
/**
* Hide the overflow in IE.
*/
svg:not(:root) {
overflow: hidden;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers (opinionated).
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: sans-serif; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
* controls in Android 4.
* 2. Correct the inability to style clickable types in iOS and Safari.
*/
button,
html [type="button"], /* 1 */
[type="reset"],
[type="submit"] {
-webkit-appearance: button; /* 2 */
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Change the border, margin, and padding in all browsers (opinionated).
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* 1. Add the correct display in IE 9-.
* 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Remove the default vertical scrollbar in IE.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10-.
* 2. Remove the padding in IE 10-.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in IE 9-.
* 1. Add the correct display in Edge, IE, and Firefox.
*/
details, /* 1 */
menu {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Scripting
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
canvas {
display: inline-block;
}
/**
* Add the correct display in IE.
*/
template {
display: none;
}
/* Hidden
========================================================================== */
/**
* Add the correct display in IE 10-.
*/
[hidden] {
display: none;
}

View file

@ -2,12 +2,10 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>openvpn-admin</title>
<link rel="stylesheet" href="css/normalize.css">
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/style.css">
<title>ovpn-admin</title>
</head>
<body>
<script src="dist/style.min.js"></script>
<div id="app">
<vue-good-table
:columns="columns"
@ -16,18 +14,19 @@
:row-style-class="rowStyleClassFn"
:search-options="{ enabled: true}" >
<div slot="table-actions">
<button type="button" class="btn btn-sm btn-success el-square" v-on:click.stop="u.modalNewUserVisible=true">Add user</button>
<button type="button" class="btn btn-sm btn-secondary el-square" v-on:click.stop="filters.hide_revoked=!filters.hide_revoked" v-show="filters.hide_revoked">Show revoked</button>
<button type="button" class="btn btn-sm btn-secondary el-square" v-on:click.stop="filters.hide_revoked=!filters.hide_revoked" v-show="!filters.hide_revoked">Hide revoked</button>
<button type="button" class="btn btn-sm btn-success el-square" v-show="serverRole == 'master'" v-on:click.stop="u.modalNewUserVisible=true">Add user</button>
<b-badget class="btn btn-sm btn-info el-square" v-if="serverRole == 'slave'">Slave - last sync: {{ lastSync }}</b-badget>
<button type="button" class="btn btn-sm btn-secondary el-square" v-on:click.stop="filters.hideRevoked=!filters.hideRevoked;this.$cookies.set('hideRevoked',!(this.$cookies.get('hideRevoked') == 'true'), -1);">{{ revokeFilterText }}</button>
</div>
<div slot="emptystate" class="d-flex justify-content-center">
<h4>No users have been created yet.</h4>
<button type="button" class="btn btn-sm btn-success el-square" v-on:click.stop="u.modalNewUserVisible=true">Add user</button>
<br>
<button type="button" class="btn btn-sm btn-success el-square" v-if="serverRole == 'master'" v-on:click.stop="u.modalNewUserVisible=true">Add user</button>
</div>
<template slot="table-row" slot-scope="props">
<span v-if="props.column.field == 'actions'">
<button
class="btn btn-sm btn-success el-square modal-el-margin"
class="btn btn-sm el-square modal-el-margin"
type="button"
:title="action.label"
:data-username="props.row.Identity"
@ -35,26 +34,23 @@
:data-text="action.label"
@click.left.stop="rowActionFn"
v-for="action in actions"
v-if="action.showWhenStatus == props.row.AccountStatus">
v-bind:class="action.class"
v-if="action.showWhenStatus == props.row.AccountStatus && action.showForServerRole.includes(serverRole) && action.showForModule.some(p=> modulesEnabled.includes(p))">
{{ action.label }}
</button>
</span>
</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">
<div class="modal-header">
<h4>Add new user </h4>
<h4>Add new user</h4>
</div>
<div class="modal-body">
<input type="text" class="form-control el-square modal-el-margin" placeholder="Username [_a-zA-Z0-9\.-]" v-model="u.newUserName">
<!-- <input type="password" class="form-control el-square modal-el-margin" minlength="6" autocomplete="off" placeholder="Password [_a-zA-Z0-9\.-]" v-model="u.newUserPassword">-->
<input type="password" class="form-control el-square modal-el-margin" minlength="6" autocomplete="off" placeholder="Password [_a-zA-Z0-9\.-]" v-model="u.newUserPassword" v-if="modulesEnabled.includes('passwdAuth')">
</div>
<div class="modal-footer justify-content-center" v-if="u.newUserCreateError.length > 0">
@ -63,18 +59,41 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="create_user();">Create</button>
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="createUser()">Create</button>
<button type="button" class="btn btn-primary el-square d-flex justify-content-sm-end modal-el-margin" v-on:click.stop="u.newUserName='';u.newUserPassword='nopass';u.modalNewUserVisible=false">Close</button>
</div>
</div>
</div>
</div>
<div class="modal-wrapper" v-if="u.modalChangePasswordVisible" v-bind:style="modalChangePasswordDisplay">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4>Change password for: <strong>{{ username }}</strong></h4>
</div>
<div class="modal-body">
<input type="password" class="form-control el-square modal-el-margin" minlength="6" autocomplete="off" placeholder="Password [_a-zA-Z0-9\.-]" v-model="u.newPassword">
</div>
<div class="modal-footer justify-content-center" v-if="u.passwordChangeMessage.length > 0">
<div class="alert" v-bind:class="passwordChangeStatusCssClass" role="alert" >
{{ u.passwordChangeMessage }}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="changeUserPassword(username)">Change password</button>
<button type="button" class="btn btn-primary el-square d-flex justify-content-sm-end modal-el-margin" v-on:click.stop="u.newPassword='';u.passwordChangeMessage='';u.modalChangePasswordVisible=false">Close</button>
</div>
</div>
</div>
</div>
<div class="modal-wrapper" v-if="u.modalShowConfigVisible" v-bind:style="modalShowConfigDisplay">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4>ovpn config for {{ username }}</h4>
<h4>ovpn config for: <strong>{{ username }}</strong></h4>
</div>
<div class="modal-body">
<div class="d-flex">
@ -95,42 +114,57 @@
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h3 class="static-address-label ">Routes table for: <strong>{{ username }}</strong></h3>
</div>
<div class="modal-body">
<div class="input-group">
<h4 class="static-address-label ">Client "{{ username }}" static address</h4>
<div class="input-group-prepend">
<div class="input-group-text">
<input id="enable-static" type="checkbox" onchange="document.getElementById('staticAddress').disabled=!this.checked;" v-bind:checked="!customAddressEnabled">
</div>
<h5 class="static-address-label ">Static address:</h5>
<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="staticAddress" type="text" class="form-control" v-model="u.ccd.ClientAddress" placeholder="127.0.0.1" v-bind:disabled="customAddressEnabled">
</div>
</div>
<div class="modal-body">
<div class="d-flex ">
<table class="table table-bordered table-hover">
<table class="table table-bordered table-hover ccd-routes" >
<thead>
<tr>
<th scope="col">Address</th>
<th scope="col">Mask</th>
<th scope="col">Description</th>
<th scope="col">Action</th>
<th scope="col" v-if="serverRole == 'master'">Action</th>
</tr>
</thead>
<tbody>
<tr v-for="(customRoute, index) in u.ccd.CustomRoutes">
<td>{{ customRoute.Address }}</td>
<td>{{ customRoute.Mask }}</td>
<td>{{ customRoute.Description }}</td>
<td>
<button type="button" class="btn btn-primary btn-sm el-square modal-el-margin" v-on:click.stop="u.ccd.CustomRoutes.splice(index, 1)">Delete</button>
<div v-if="serverRole == 'slave'">
{{ customRoute.Address }}
</div>
<input v-if="serverRole == 'master'" v-model="customRoute.Address">
</td>
<td>
<div v-if="serverRole == 'slave'">
{{ customRoute.Mask }}
</div>
<input v-if="serverRole == 'master'" v-model="customRoute.Mask">
</td>
<td>
<div v-if="serverRole == 'slave'">
{{ customRoute.Description }}
</div>
<input v-if="serverRole == 'master'" v-model="customRoute.Description">
</td>
<td class="text-right" v-if="serverRole == 'master'">
<button type="button" class="btn btn-danger btn-sm el-square modal-el-margin" v-if="serverRole == 'master'" v-on:click.stop="u.ccd.CustomRoutes.splice(index, 1)">Delete</button>
</td>
</tr>
<tr>
<tr v-if="serverRole == 'master'">
<td><input type="text" v-model="u.newRoute.Address"/></td>
<td><input type="text" v-model="u.newRoute.Mask"/></td>
<td><input type="text" v-model="u.newRoute.Description"/></td>
<td>
<td class="text-right" v-if="serverRole == 'master'">
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="u.ccd.CustomRoutes.push(u.newRoute);u.newRoute={};">Add</button>
</td>
</tr>
@ -144,14 +178,59 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="ccd_apply()">Save</button>
<button type="button" class="btn btn-success el-square modal-el-margin" v-if="serverRole == 'master'" v-on:click.stop="ccdApply()">Save</button>
<button type="button" class="btn btn-primary el-square modal-el-margin" v-on:click.stop="u.ccd={Name:'',ClientAddress:'',CustomRoutes:[]};u.ccdApplyStatusMessage='';u.ccdApplyStatus='';u.modalShowCcdVisible=false">Close</button>
</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" />
</div>
<script src="dist/build.js"></script>
<script src="dist/bundle.min.js"></script>
</body>
</html>

View file

@ -1,106 +1,50 @@
var path = require('path')
var webpack = require('webpack')
const TerserPlugin = require('terser-webpack-plugin');
const path = require('path');
//const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, './static/dist'),
publicPath: '/dist/',
filename: 'build.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'vue-style-loader',
'css-loader'
],
},
{
test: /\.scss$/,
use: [
'vue-style-loader',
'css-loader',
'sass-loader'
],
},
{
test: /\.sass$/,
use: [
'vue-style-loader',
'css-loader',
'sass-loader?indentedSyntax'
],
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
// Since sass-loader (weirdly) has SCSS as its default parse mode, we map
// the "scss" and "sass" values for the lang attribute to the right configs here.
// other preprocessors should work out of the box, no loader config like this necessary.
'scss': [
'vue-style-loader',
'css-loader',
'sass-loader'
],
'sass': [
'vue-style-loader',
'css-loader',
'sass-loader?indentedSyntax'
]
}
// other vue-loader options go here
}
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: 'file-loader',
options: {
name: '[name].[ext]?[hash]'
}
}
]
},
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js'
mode: 'production',
entry: {
bundle: [
'./src/main.js',
],
style: [
'./src/style.js',
]
},
extensions: ['*', '.js', '.vue', '.json']
},
devServer: {
historyApiFallback: true,
noInfo: true,
overlay: true
},
performance: {
hints: false
},
devtool: '#eval-source-map'
}
output: {
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\/(?!bootstrap-vue\/src\/)/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
},
],
},
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js',
//'bootstrap-vue$': 'bootstrap-vue/src/index.js'
},
extensions: ['*', '.js', '.vue', '.json']
},
}
if (process.env.NODE_ENV === 'production') {
module.exports.devtool = '#source-map'
// http://vue-loader.vuejs.org/en/workflow/production.html
module.exports.plugins = (module.exports.plugins || []).concat([
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
}),
new TerserPlugin({
sourceMap: true
}),
new webpack.LoaderOptionsPlugin({
minimize: true
})
])
}

60
go.mod
View file

@ -1,9 +1,61 @@
module openvpn-web-ui
module ovpn-admin
go 1.14
go 1.17
require (
github.com/gobuffalo/packr/v2 v2.8.3
github.com/prometheus/client_golang v1.11.0
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-20190924025748-f65c72e2690d // indirect
gopkg.in/alecthomas/kingpin.v2 v2.2.6
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.2.2 // indirect
github.com/gobuffalo/envy v1.10.1 // indirect
github.com/gobuffalo/logger v1.0.6 // indirect
github.com/gobuffalo/packd v1.0.1 // indirect
github.com/gobuffalo/packr v1.30.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
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
github.com/markbates/errx v1.1.0 // indirect
github.com/markbates/oncer v1.0.0 // indirect
github.com/markbates/safe v1.0.1 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 // indirect
golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
k8s.io/api v0.23.1 // indirect
k8s.io/klog/v2 v2.40.1 // indirect
k8s.io/kube-openapi v0.0.0-20220114203427-a0453230fd26 // indirect
k8s.io/utils v0.0.0-20211208161948-7d6a63dca704 // indirect
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)

1028
go.sum

File diff suppressed because it is too large Load diff

2
helm/Chart.yaml Normal file
View file

@ -0,0 +1,2 @@
name: ovpn-admin
version: 1.0.0

1
helm/README.md Normal file
View file

@ -0,0 +1 @@
helm chart example

View file

@ -0,0 +1,88 @@
{{ $openvpnNetwork := required "A valid .Values.openvpn.subnet entry required!" .Values.openvpn.subnet }}
{{ $openvpnNetworkAddress := index (splitList "/" $openvpnNetwork) 0 }}
{{ $openvpnNetworkNetmask := index (splitList "/" $openvpnNetwork) 1 }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: openvpn
data:
openvpn.conf: |-
user nobody
group nogroup
mode server
tls-server
# dev-type tun
dev tun
proto tcp-server
port 1194
# local 127.0.0.1
management 127.0.0.1 8989
tun-mtu 1500
mssfix
# only udp
#fragment 1300
keepalive 10 60
client-to-client
persist-key
persist-tun
cipher AES-128-CBC
duplicate-cn
server {{ $openvpnNetworkAddress }} {{ $openvpnNetworkNetmask }}
topology subnet
push "topology subnet"
push "route-metric 9999"
verb 4
ifconfig-pool-persist /tmp/openvpn.ipp
status /tmp/openvpn.status
key-direction 0
ca /etc/openvpn/certs/pki/ca.crt
key /etc/openvpn/certs/pki/private/server.key
cert /etc/openvpn/certs/pki/issued/server.crt
dh /etc/openvpn/certs/pki/dh.pem
crl-verify /etc/openvpn/certs/pki/crl.pem
tls-auth /etc/openvpn/certs/pki/ta.key
client-config-dir /etc/openvpn/ccd
entrypoint.sh: |-
#!/bin/sh
set -x
iptables -t nat -A POSTROUTING -s {{ $openvpnNetworkAddress }}/{{ $openvpnNetworkNetmask }} ! -d {{ $openvpnNetworkAddress }}/{{ $openvpnNetworkNetmask }} -j MASQUERADE
mkdir -p /dev/net
if [ ! -c /dev/net/tun ]; then
mknod /dev/net/tun c 10 200
fi
wait_file() {
file_path="$1"
while true; do
if [ -f $file_path ]; then
break
fi
echo "wait $file_path"
sleep 2
done
}
easyrsa_path="/etc/openvpn/certs"
wait_file "$easyrsa_path/pki/ca.crt"
wait_file "$easyrsa_path/pki/private/server.key"
wait_file "$easyrsa_path/pki/issued/server.crt"
wait_file "$easyrsa_path/pki/ta.key"
wait_file "$easyrsa_path/pki/dh.pem"
wait_file "$easyrsa_path/pki/crl.pem"
openvpn --config /etc/openvpn/openvpn.conf

View file

@ -0,0 +1,117 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: openvpn
spec:
selector:
matchLabels:
app: openvpn
template:
metadata:
labels:
app: openvpn
spec:
{{- if .Values.openvpn.nodeSelector }}
nodeSelector:
{{- .Values.openvpn.nodeSelector | toYaml | indent 8 | printf "\n%s" }}
{{- end }}
{{- if .Values.openvpn.tolerations }}
tolerations:
{{- .Values.openvpn.tolerations | toYaml | indent 8 | printf "\n%s" }}
{{- end }}
terminationGracePeriodSeconds: 0
serviceAccountName: openvpn
containers:
- name: ovpn-admin
image: {{ .Values.ovpnAdmin.image }}
command:
- /bin/sh
- -c
- /app/ovpn-admin
--storage.backend="kubernetes.secrets"
--listen.host="0.0.0.0"
--listen.port="8000"
--role="master"
{{- if hasKey .Values.openvpn "inlet" }}
{{- if eq .Values.openvpn.inlet "LoadBalancer" }}
--ovpn.server.behindLB
--ovpn.service="openvpn-external"
{{- end }}
{{- end }}
--mgmt=main="127.0.0.1:8989"
--ccd --ccd.path="/mnt/ccd"
--easyrsa.path="/mnt/certs"
{{- $externalHost := "" }}
{{- if hasKey .Values.openvpn "inlet" }}
{{- if eq .Values.openvpn.inlet "ExternalIP" }}{{ $externalHost = .Values.openvpn.externalIP }}{{- end }}
{{- end }}
{{- if hasKey .Values.openvpn "externalHost" }}{{ $externalHost = .Values.openvpn.externalHost }}{{- end }}
{{- if ne $externalHost "" }}
--ovpn.server="{{ $externalHost }}:{{ .Values.openvpn.externalPort | default 5416 | quote }}:tcp"
{{- end }}
ports:
- name: ovpn-admin
protocol: TCP
containerPort: 8000
volumeMounts:
- name: certs
mountPath: /mnt/certs
- name: ccd
mountPath: /mnt/ccd
- name: openvpn
image: {{ .Values.openvpn.image }}
command: [ '/entrypoint.sh' ]
# imagePullPolicy: Always
securityContext:
allowPrivilegeEscalation: false
capabilities:
add:
- NET_ADMIN
- NET_RAW
- MKNOD
- SETGID
- SETUID
drop:
- ALL
ports:
- name: openvpn-tcp
protocol: TCP
containerPort: 1194
{{- if eq .Values.openvpn.inlet "HostPort" }}
hostPort: {{ .Values.openvpn.hostPort }}
{{- end }}
volumeMounts:
- name: tmp
mountPath: /tmp
- name: dev-net
mountPath: /dev/net
- name: certs
mountPath: /etc/openvpn/certs
- name: ccd
mountPath: /etc/openvpn/ccd
- name: config
mountPath: /etc/openvpn/openvpn.conf
subPath: openvpn.conf
readOnly: true
- name: entrypoint
mountPath: /entrypoint.sh
subPath: entrypoint.sh
readOnly: true
volumes:
- name: tmp
emptyDir: {}
- name: dev-net
emptyDir: {}
- name: certs
emptyDir: {}
- name: ccd
emptyDir: {}
- name: config
configMap:
name: openvpn
defaultMode: 0644
- name: entrypoint
configMap:
name: openvpn
defaultMode: 0755

View file

@ -0,0 +1,39 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ovpn-admin
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/backend-protocol: HTTP
nginx.ingress.kubernetes.io/auth-type: basic
nginx.ingress.kubernetes.io/auth-realm: "Authentication Required"
nginx.ingress.kubernetes.io/auth-secret: basic-auth
spec:
tls:
- hosts:
- {{ .Values.domain }}
secretName: ingress-tls
rules:
- host: {{ .Values.domain }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ovpn-admin
port:
name: http
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: ovpn-admin
spec:
secretName: ingress-tls
dnsNames:
- {{ .Values.domain }}
issuerRef:
name: letsencrypt
kind: ClusterIssuer

36
helm/templates/rbac.yaml Normal file
View file

@ -0,0 +1,36 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: openvpn
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: openvpn
rules:
- apiGroups:
- ""
resources:
- services
verbs:
- get
- list
- apiGroups:
- ""
resources:
- secrets
verbs:
- "*"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: openvpn
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: openvpn
subjects:
- kind: ServiceAccount
name: openvpn

View file

@ -0,0 +1,8 @@
---
apiVersion: v1
kind: Secret
metadata:
name: basic-auth
type: Opaque
data:
auth: {{ print .Values.ovpnAdmin.basicAuth.user ":{PLAIN}" .Values.ovpnAdmin.basicAuth.password | b64enc | quote }}

View file

@ -0,0 +1,57 @@
---
apiVersion: v1
kind: Service
metadata:
name: ovpn-admin
spec:
clusterIP: None
ports:
- name: http
port: 8000
protocol: TCP
targetPort: 8000
selector:
app: openvpn
---
{{- if hasKey .Values.openvpn "inlet" }}
{{- if eq .Values.openvpn.inlet "LoadBalancer" }}
---
apiVersion: v1
kind: Service
metadata:
name: openvpn-external
spec:
externalTrafficPolicy: Local
type: LoadBalancer
ports:
- name: openvpn-tcp
protocol: TCP
port: {{ .Values.openvpn.externalPort | default 1194 }}
targetPort: openvpn-tcp
selector:
app: openvpn
{{- else if eq .Values.openvpn.inlet "ExternalIP" }}
---
apiVersion: v1
kind: Service
metadata:
name: openvpn-external
spec:
type: ClusterIP
externalIPs:
- {{ .Values.openvpn.externalIP }}
ports:
- name: openvpn-tcp
port: {{ .Values.openvpn.externalPort | default 1194 }}
protocol: TCP
targetPort: openvpn-tcp
selector:
app: openvpn
{{- else if eq .Values.openvpn.inlet "HostPort" }}
---
{{- else }}
{{- cat "Unsupported inlet type" .inlet | fail }}
{{- end }}
{{- end }}

26
helm/values.yaml Normal file
View file

@ -0,0 +1,26 @@
domain: changeme
ovpnAdmin:
image: changeme
basicAuth:
user: admin
password: changeme
openvpn:
image: changeme
subnet: 172.16.200.0/255.255.255.0
# nodeSelector:
# node-role.kubernetes.io/master: ""
# tolerations:
# - effect: NoSchedule
# key: node-role.kubernetes.io/master
#
# // LoadBalancer or ExternalIP or HostPort
inlet: HostPort
#
# If inlet: ExternalIP
# externalIP: 1.2.3.4
# externalPort: 1194
#
# If inlet: HostPort
hostPort: 1194
# Domain or ip for connect to OpenVPN server
# externalHost: 1.2.3.4

306
helpers.go Normal file
View file

@ -0,0 +1,306 @@
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 {
t, err := time.Parse(layout, datetime)
if err != nil {
log.Errorln(err)
}
return t
}
func parseDateToString(layout, datetime, format string) string {
return parseDate(layout, datetime).Format(format)
}
func parseDateToUnix(layout, datetime string) int64 {
return parseDate(layout, datetime).Unix()
}
func runBash(script string) string {
log.Debugln(script)
cmd := exec.Command("bash", "-c", script)
stdout, err := cmd.CombinedOutput()
if err != nil {
return fmt.Sprint(err) + " : " + string(stdout)
}
return string(stdout)
}
func fExist(path string) bool {
var _, err = os.Stat(path)
if os.IsNotExist(err) {
return false
} else if err != nil {
log.Fatalf("fExist: %s", err)
return false
}
return true
}
func fRead(path string) string {
content, err := ioutil.ReadFile(path)
if err != nil {
log.Warning(err)
return ""
}
return string(content)
}
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 err
}
defer file.Close()
}
return nil
}
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) 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 {
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if basicAuth {
req.SetBasicAuth(*masterBasicAuthUser, *masterBasicAuthPassword)
}
resp, err := client.Do(req)
if err != nil {
return err
}
if resp.StatusCode != 200 {
log.Warnf("WARNING: Download file operation for url %s finished with status code %d\n", url, resp.StatusCode)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
fCreate(path)
fWrite(path, string(body))
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
}

BIN
img/ovpn-admin-metrics.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

BIN
img/ovpn-admin-users.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

12
install-deps-arm.sh Executable file
View file

@ -0,0 +1,12 @@
#!/usr/bin/env bash
apt-get update
apt-get install -y curl
apt-get install -y libc6 libc6-dev gcc-arm-linux-gnueabi gcc-aarch64-linux-gnu
curl -sL https://deb.nodesource.com/setup_16.x | bash -
apt-get install -y nodejs
PATH=$PATH:~/go/bin
go install github.com/gobuffalo/packr/v2/packr2@latest

12
install-deps.sh Executable file
View file

@ -0,0 +1,12 @@
#!/usr/bin/env bash
apt-get update
apt-get install -y curl
apt-get install -y libc6 libc6-dev libc6-dev-i386
curl -sL https://deb.nodesource.com/setup_16.x | bash -
apt-get install -y nodejs
PATH=$PATH:~/go/bin
go install github.com/gobuffalo/packr/v2/packr2@latest

776
kubernetes.go Normal file
View file

@ -0,0 +1,776 @@
package main
import (
"bytes"
"context"
"crypto/rsa"
"crypto/x509"
"errors"
"fmt"
"github.com/google/uuid"
"io/ioutil"
"os"
"os/exec"
"strings"
"time"
log "github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
const (
secretCA = "openvpn-pki-ca"
secretServer = "openvpn-pki-server"
secretClientTmpl = "openvpn-pki-%d"
secretCRL = "openvpn-pki-crl"
secretIndexTxt = "openvpn-pki-index-txt"
secretDHandTA = "openvpn-pki-dh-and-ta"
certFileName = "tls.crt"
privKeyFileName = "tls.key"
)
//<year><month><day><hour><minute><second>Z
const indexTxtDateFormat = "060102150405Z"
var namespace = "default"
type OpenVPNPKI struct {
CAPrivKeyRSA *rsa.PrivateKey
CAPrivKeyPEM *bytes.Buffer
CACert *x509.Certificate
CACertPEM *bytes.Buffer
ServerPrivKeyRSA *rsa.PrivateKey
ServerPrivKeyPEM *bytes.Buffer
ServerCert *x509.Certificate
ServerCertPEM *bytes.Buffer
ClientCerts []ClientCert
RevokedCerts []RevokedCert
KubeClient *kubernetes.Clientset
}
type ClientCert struct {
PrivKeyRSA *rsa.PrivateKey
PrivKeyPEM *bytes.Buffer
Cert *x509.Certificate
CertPEM *bytes.Buffer
}
type RevokedCert struct {
RevokedTime time.Time `json:"revokedTime"`
CommonName string `json:"commonName"`
Cert *x509.Certificate `json:"cert"`
}
func (openVPNPKI *OpenVPNPKI) run() (err error) {
if _, err := os.Stat(kubeNamespaceFilePath); err == nil {
file, err := ioutil.ReadFile(kubeNamespaceFilePath)
if err != nil {
return err
}
namespace = string(file)
}
err = openVPNPKI.initKubeClient()
if err != nil {
return
}
err = openVPNPKI.initPKI()
if err != nil {
return
}
err = openVPNPKI.indexTxtUpdate()
if err != nil {
log.Error(err)
}
err = openVPNPKI.easyrsaGenCRL()
if err != nil {
log.Error(err)
}
if res, _ := openVPNPKI.secretCheckExists(secretDHandTA); !res {
err := openVPNPKI.secretGenTaKeyAndDHParam()
if err != nil {
log.Error(err)
}
}
err = openVPNPKI.updateFilesFromSecrets()
if err != nil {
log.Error(err)
}
err = openVPNPKI.updateCRLOnDisk()
if err != nil {
log.Error(err)
}
err = openVPNPKI.updateIndexTxtOnDisk()
if err != nil {
log.Error(err)
}
err = openVPNPKI.updateCcdOnDisk()
if err != nil {
log.Error(err)
}
return
}
func (openVPNPKI *OpenVPNPKI) initKubeClient() (err error) {
config, _ := rest.InClusterConfig()
openVPNPKI.KubeClient, err = kubernetes.NewForConfig(config)
return
}
func (openVPNPKI *OpenVPNPKI) initPKI() (err error) {
if res, _ := openVPNPKI.secretCheckExists(secretCA); res {
cert, err := openVPNPKI.secretGetClientCert(secretCA)
if err != nil {
return err
}
openVPNPKI.CAPrivKeyPEM = cert.PrivKeyPEM
openVPNPKI.CAPrivKeyRSA = cert.PrivKeyRSA
openVPNPKI.CACertPEM = cert.CertPEM
openVPNPKI.CACert = cert.Cert
} else {
openVPNPKI.CAPrivKeyPEM, err = genPrivKey()
if err != nil {
return
}
openVPNPKI.CAPrivKeyRSA, err = decodePrivKey(openVPNPKI.CAPrivKeyPEM.Bytes())
openVPNPKI.CACertPEM, _ = genCA(openVPNPKI.CAPrivKeyRSA)
openVPNPKI.CACert, err = decodeCert(openVPNPKI.CACertPEM.Bytes())
if err != nil {
return
}
secretMetaData := metav1.ObjectMeta{Name: secretCA}
secretData := map[string][]byte{
certFileName: openVPNPKI.CACertPEM.Bytes(),
privKeyFileName: openVPNPKI.CAPrivKeyPEM.Bytes(),
}
err = openVPNPKI.secretCreate(secretMetaData, secretData, v1.SecretTypeTLS)
if err != nil {
return
}
}
if res, _ := openVPNPKI.secretCheckExists(secretServer); res {
cert, err := openVPNPKI.secretGetClientCert(secretServer)
if err != nil {
return err
}
openVPNPKI.ServerPrivKeyPEM = cert.PrivKeyPEM
openVPNPKI.ServerPrivKeyRSA = cert.PrivKeyRSA
openVPNPKI.ServerCertPEM = cert.CertPEM
openVPNPKI.ServerCert = cert.Cert
} else {
openVPNPKI.ServerPrivKeyPEM, err = genPrivKey()
if err != nil {
return
}
openVPNPKI.ServerPrivKeyRSA, err = decodePrivKey(openVPNPKI.ServerPrivKeyPEM.Bytes())
if err != nil {
return
}
openVPNPKI.ServerCertPEM, _ = genServerCert(openVPNPKI.ServerPrivKeyRSA, openVPNPKI.CAPrivKeyRSA, openVPNPKI.CACert, "server")
openVPNPKI.ServerCert, err = decodeCert(openVPNPKI.ServerCertPEM.Bytes())
secretMetaData := metav1.ObjectMeta{
Name: secretServer,
Labels: map[string]string{
"index.txt": "",
"name": "server",
"type": "serverAuth",
},
}
secretData := map[string][]byte{
certFileName: openVPNPKI.ServerCertPEM.Bytes(),
privKeyFileName: openVPNPKI.ServerPrivKeyPEM.Bytes(),
}
err = openVPNPKI.secretCreate(secretMetaData, secretData, v1.SecretTypeTLS)
if err != nil {
return
}
}
return
}
func (openVPNPKI *OpenVPNPKI) indexTxtUpdate() (err error) {
secrets, err := openVPNPKI.secretsGetByLabels("index.txt=")
if err != nil {
return
}
var indexTxt string
for _, secret := range secrets.Items {
certPEM := bytes.NewBuffer(secret.Data[certFileName])
log.Trace("indexTxtUpdate:" + secret.Name)
cert, err := decodeCert(certPEM.Bytes())
if err != nil {
return nil
}
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), fmt.Sprintf("%d", cert.SerialNumber), "unknown", "/CN="+secret.Labels["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), fmt.Sprintf("%d", cert.SerialNumber), "unknown", "/CN="+secret.Labels["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"], fmt.Sprintf("%d", cert.SerialNumber), "unknown", "/CN="+secret.Labels["name"])
}
}
secretMetaData := metav1.ObjectMeta{Name: secretIndexTxt}
secretData := map[string][]byte{"index.txt": []byte(indexTxt)}
if res, _ := openVPNPKI.secretCheckExists(secretIndexTxt); !res {
err = openVPNPKI.secretCreate(secretMetaData, secretData, v1.SecretTypeOpaque)
} else {
err = openVPNPKI.secretUpdate(secretMetaData, secretData, v1.SecretTypeOpaque)
}
return
}
func (openVPNPKI *OpenVPNPKI) updateIndexTxtOnDisk() (err error) {
secret, err := openVPNPKI.secretGetByName(secretIndexTxt)
indexTxt := secret.Data["index.txt"]
err = ioutil.WriteFile(fmt.Sprintf("%s/pki/index.txt", *easyrsaDirPath), indexTxt, 0600)
return
}
func (openVPNPKI *OpenVPNPKI) easyrsaGenCRL() (err error) {
err = openVPNPKI.indexTxtUpdate()
if err != nil {
return
}
secrets, err := openVPNPKI.secretsGetByLabels("index.txt=,type=clientAuth")
if err != nil {
return
}
var revoked []*RevokedCert
for _, secret := range secrets.Items {
if secret.Annotations["revokedAt"] != "" {
revokedAt, err := time.Parse(indexTxtDateFormat, secret.Annotations["revokedAt"])
if err != nil {
log.Warning(err)
}
cert, err := decodeCert(secret.Data[certFileName])
revoked = append(revoked, &RevokedCert{RevokedTime: revokedAt, Cert: cert})
}
}
crl, err := genCRL(revoked, openVPNPKI.CACert, openVPNPKI.CAPrivKeyRSA)
if err != nil {
return
}
secretMetaData := metav1.ObjectMeta{Name: secretCRL}
secretData := map[string][]byte{
"crl.pem": crl.Bytes(),
}
//err = openVPNPKI.secretCreate(secretMetaData, secretData)
if res, _ := openVPNPKI.secretCheckExists(secretCRL); !res {
err = openVPNPKI.secretCreate(secretMetaData, secretData, v1.SecretTypeOpaque)
} else {
err = openVPNPKI.secretUpdate(secretMetaData, secretData, v1.SecretTypeOpaque)
}
return
}
func (openVPNPKI *OpenVPNPKI) easyrsaBuildClient(commonName string) (err error) {
// check certificate exists
_, err = openVPNPKI.secretGetByLabels("name=" + commonName)
if err == nil {
return errors.New(fmt.Sprintf("certificate for user (%s) already exists", commonName))
}
clientPrivKeyPEM, err := genPrivKey()
if err != nil {
return
}
clientPrivKeyRSA, err := decodePrivKey(clientPrivKeyPEM.Bytes())
if err != nil {
return
}
clientCertPEM, _ := genClientCert(clientPrivKeyRSA, openVPNPKI.CAPrivKeyRSA, openVPNPKI.CACert, commonName)
clientCert, err := decodeCert(clientCertPEM.Bytes())
secretMetaData := metav1.ObjectMeta{
Name: fmt.Sprintf(secretClientTmpl, clientCert.SerialNumber),
Labels: map[string]string{
"index.txt": "",
"type": "clientAuth",
"name": commonName,
"app.kubernetes.io/managed-by": "ovpn-admin",
},
Annotations: map[string]string{
"commonName": commonName,
"notBefore": clientCert.NotBefore.Format(indexTxtDateFormat),
"notAfter": clientCert.NotAfter.Format(indexTxtDateFormat),
"revokedAt": "",
"serialNumber": fmt.Sprintf("%d", clientCert.SerialNumber),
},
}
secretData := map[string][]byte{
certFileName: clientCertPEM.Bytes(),
privKeyFileName: clientPrivKeyPEM.Bytes(),
}
err = openVPNPKI.secretCreate(secretMetaData, secretData, v1.SecretTypeTLS)
if err != nil {
return
}
err = openVPNPKI.indexTxtUpdate()
if err != nil {
return
}
err = openVPNPKI.updateIndexTxtOnDisk()
return
}
func (openVPNPKI *OpenVPNPKI) easyrsaGetCACert() string {
return openVPNPKI.CACertPEM.String()
}
func (openVPNPKI *OpenVPNPKI) easyrsaGetClientCert(commonName string) (cert, key string) {
secret, err := openVPNPKI.secretGetByLabels("name=" + commonName)
if err != nil {
log.Error(err)
}
cert = string(secret.Data[certFileName])
key = string(secret.Data[privKeyFileName])
return
}
func (openVPNPKI *OpenVPNPKI) easyrsaRevoke(commonName string) (err error) {
secret, err := openVPNPKI.secretGetByLabels("name=" + commonName)
if err != nil {
log.Error(err)
}
if secret.Annotations["revokedAt"] != "" {
log.Warnf("user (%s) already revoked", commonName)
return
}
secret.Annotations["revokedAt"] = time.Now().Format(indexTxtDateFormat)
_, 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) easyrsaUnrevoke(commonName string) (err error) {
secret, err := openVPNPKI.secretGetByLabels("name=" + commonName)
if err != nil {
log.Error(err)
}
secret.Annotations["revokedAt"] = ""
_, 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) 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 {
return
}
cert.CertPEM = bytes.NewBuffer(secret.Data[certFileName])
cert.Cert, err = decodeCert(cert.CertPEM.Bytes())
if err != nil {
return
}
cert.PrivKeyPEM = bytes.NewBuffer(secret.Data[privKeyFileName])
cert.PrivKeyRSA, err = decodePrivKey(cert.PrivKeyPEM.Bytes())
if err != nil {
return
}
return
}
func (openVPNPKI *OpenVPNPKI) updateFilesFromSecrets() (err error) {
ca, err := openVPNPKI.secretGetClientCert(secretCA)
if err != nil {
return
}
server, err := openVPNPKI.secretGetClientCert(secretServer)
if err != nil {
return
}
secret, err := openVPNPKI.secretGetByName(secretDHandTA)
takey := secret.Data["ta.key"]
dhparam := secret.Data["dh.pem"]
if _, err := os.Stat(fmt.Sprintf("%s/pki/issued", *easyrsaDirPath)); os.IsNotExist(err) {
err = os.MkdirAll(fmt.Sprintf("%s/pki/issued", *easyrsaDirPath), 0755)
}
if _, err := os.Stat(fmt.Sprintf("%s/pki/private", *easyrsaDirPath)); os.IsNotExist(err) {
err = os.MkdirAll(fmt.Sprintf("%s/pki/private", *easyrsaDirPath), 0755)
}
err = ioutil.WriteFile(fmt.Sprintf("%s/pki/ca.crt", *easyrsaDirPath), ca.CertPEM.Bytes(), 0600)
if err != nil {
return
}
err = ioutil.WriteFile(fmt.Sprintf("%s/pki/issued/server.crt", *easyrsaDirPath), server.CertPEM.Bytes(), 0600)
if err != nil {
return
}
err = ioutil.WriteFile(fmt.Sprintf("%s/pki/private/server.key", *easyrsaDirPath), server.PrivKeyPEM.Bytes(), 0600)
if err != nil {
return
}
err = ioutil.WriteFile(fmt.Sprintf("%s/pki/ta.key", *easyrsaDirPath), takey, 0600)
if err != nil {
return
}
err = ioutil.WriteFile(fmt.Sprintf("%s/pki/dh.pem", *easyrsaDirPath), dhparam, 0600)
if err != nil {
return
}
err = openVPNPKI.updateCRLOnDisk()
return
}
func (openVPNPKI *OpenVPNPKI) updateCRLOnDisk() (err error) {
secret, err := openVPNPKI.secretGetByName(secretCRL)
crl := secret.Data["crl.pem"]
err = ioutil.WriteFile(fmt.Sprintf("%s/pki/crl.pem", *easyrsaDirPath), crl, 0644)
if err != nil {
log.Errorf("error write crl.pem:%s", err.Error())
}
return
}
func (openVPNPKI *OpenVPNPKI) secretGenTaKeyAndDHParam() (err error) {
taKeyPath := "/tmp/ta.key"
cmd := exec.Command("bash", "-c", fmt.Sprintf("/usr/sbin/openvpn --genkey --secret %s", taKeyPath))
stdout, err := cmd.CombinedOutput()
log.Info(fmt.Sprintf("/usr/sbin/openvpn --genkey --secret %s: %s", taKeyPath, string(stdout)))
if err != nil {
return
}
taKey, err := ioutil.ReadFile(taKeyPath)
dhparamPath := "/tmp/dh.pem"
cmd = exec.Command("bash", "-c", fmt.Sprintf("openssl dhparam -out %s 2048", dhparamPath))
_, err = cmd.CombinedOutput()
if err != nil {
return
}
dhparam, err := ioutil.ReadFile(dhparamPath)
secretMetaData := metav1.ObjectMeta{Name: secretDHandTA}
secretData := map[string][]byte{
"ta.key": taKey,
"dh.pem": dhparam,
}
err = openVPNPKI.secretCreate(secretMetaData, secretData, v1.SecretTypeOpaque)
if err != nil {
return
}
return
}
// ccd
func (openVPNPKI *OpenVPNPKI) secretGetCcd(commonName string) (ccd string) {
secret, err := openVPNPKI.secretGetByLabels("name=" + commonName)
if err != nil {
log.Error(err)
return
}
for k, _ := range secret.Data {
if k == "ccd" {
ccd = string(secret.Data["ccd"])
return
}
}
return
}
func (openVPNPKI *OpenVPNPKI) secretUpdateCcd(commonName string, ccd []byte) {
secret, err := openVPNPKI.secretGetByLabels("name=" + commonName)
if err != nil {
log.Error(err)
return
}
secret.Data["ccd"] = ccd
err = openVPNPKI.secretUpdate(secret.ObjectMeta, secret.Data, v1.SecretTypeTLS)
if err != nil {
log.Errorf("secret (%s) update error: %s", secret.Name, err.Error())
}
err = openVPNPKI.updateCcdOnDisk()
if err != nil {
log.Error(err)
}
}
func (openVPNPKI *OpenVPNPKI) updateCcdOnDisk() (err error) {
secrets, err := openVPNPKI.secretsGetByLabels("index.txt=,type=clientAuth")
if err != nil {
return
}
if _, err := os.Stat(*ccdDir); os.IsNotExist(err) {
err = os.MkdirAll(*ccdDir, 0755)
}
for _, secret := range secrets.Items {
ccd := secret.Data["ccd"]
if len(ccd) > 0 {
err = ioutil.WriteFile(fmt.Sprintf("%s/%s", *ccdDir, secret.Labels["name"]), ccd, 0644)
if err != nil {
log.Error(err)
}
}
}
return
}
//
func (openVPNPKI *OpenVPNPKI) secretCreate(objectMeta metav1.ObjectMeta, data map[string][]byte, secretType v1.SecretType) (err error) {
if objectMeta.Name == "nil" {
err = errors.New("secret name not defined")
return
}
secret := &v1.Secret{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: objectMeta,
Data: data,
Type: secretType,
}
_, err = openVPNPKI.KubeClient.CoreV1().Secrets(namespace).Create(context.TODO(), secret, metav1.CreateOptions{})
return
}
func (openVPNPKI *OpenVPNPKI) secretUpdate(objectMeta metav1.ObjectMeta, data map[string][]byte, secretType v1.SecretType) (err error) {
secret := &v1.Secret{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: objectMeta,
Data: data,
Type: secretType,
}
_, err = openVPNPKI.KubeClient.CoreV1().Secrets(namespace).Update(context.TODO(), secret, metav1.UpdateOptions{})
return
}
func (openVPNPKI *OpenVPNPKI) secretGetByName(name string) (secret *v1.Secret, err error) {
secret, err = openVPNPKI.KubeClient.CoreV1().Secrets(namespace).Get(context.TODO(), name, metav1.GetOptions{})
return
}
func (openVPNPKI *OpenVPNPKI) secretsGetByLabels(labels string) (secrets *v1.SecretList, err error) {
secrets, err = openVPNPKI.KubeClient.CoreV1().Secrets(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: labels})
if err != nil {
return
}
if len(secrets.Items) == 0 {
log.Debugf("secrets with labels %s not found", labels)
}
return
}
func (openVPNPKI *OpenVPNPKI) secretGetByLabels(labels string) (secret *v1.Secret, err error) {
secrets, err := openVPNPKI.secretsGetByLabels(labels)
if err != nil {
return
}
if len(secrets.Items) > 1 {
err = errors.New(fmt.Sprintf("found more than one secret with labels %s", labels))
return
}
if len(secrets.Items) == 0 {
err = errors.New(fmt.Sprintf("secret not found"))
return
}
secret = &secrets.Items[0]
return
}
func (openVPNPKI *OpenVPNPKI) secretCheckExists(name string) (bool, string) {
secret, err := openVPNPKI.KubeClient.CoreV1().Secrets(namespace).Get(context.TODO(), name, metav1.GetOptions{})
if err != nil {
log.Debug(err)
return false, ""
}
return true, secret.ResourceVersion
}

1748
main.go

File diff suppressed because it is too large Load diff

16
setup/auth.sh Normal file
View file

@ -0,0 +1,16 @@
#!/usr/bin/env sh
PATH=$PATH:/usr/local/bin
set -e
env
auth_usr=$(head -1 $1)
auth_passwd=$(tail -1 $1)
if [ $common_name = $auth_usr ]; then
openvpn-user auth --db.path /etc/openvpn/easyrsa/pki/users.db --user ${auth_usr} --password ${auth_passwd}
else
echo "Authorization failed"
exit 1
fi

59
setup/configure.sh Normal file
View file

@ -0,0 +1,59 @@
#!/usr/bin/env bash
set -ex
EASY_RSA_LOC="/etc/openvpn/easyrsa"
SERVER_CERT="${EASY_RSA_LOC}/pki/issued/server.crt"
OVPN_SRV_NET=${OVPN_SERVER_NET:-172.16.100.0}
OVPN_SRV_MASK=${OVPN_SERVER_MASK:-255.255.255.0}
cd $EASY_RSA_LOC
if [ -e "$SERVER_CERT" ]; then
echo "Found existing certs - reusing"
else
if [ ${OVPN_ROLE:-"master"} = "slave" ]; then
echo "Waiting for initial sync data from master"
while [ $(wget -q localhost/api/sync/last/try -O - | wc -m) -lt 1 ]
do
sleep 5
done
else
echo "Generating new certs"
easyrsa init-pki
cp -R /usr/share/easy-rsa/* $EASY_RSA_LOC/pki
echo "ca" | easyrsa build-ca nopass
easyrsa build-server-full server nopass
easyrsa gen-dh
openvpn --genkey --secret ./pki/ta.key
fi
fi
easyrsa gen-crl
iptables -t nat -D POSTROUTING -s ${OVPN_SRV_NET}/${OVPN_SRV_MASK} ! -d ${OVPN_SRV_NET}/${OVPN_SRV_MASK} -j MASQUERADE || true
iptables -t nat -A POSTROUTING -s ${OVPN_SRV_NET}/${OVPN_SRV_MASK} ! -d ${OVPN_SRV_NET}/${OVPN_SRV_MASK} -j MASQUERADE
mkdir -p /dev/net
if [ ! -c /dev/net/tun ]; then
mknod /dev/net/tun c 10 200
fi
cp -f /etc/openvpn/setup/openvpn.conf /etc/openvpn/openvpn.conf
if [ ${OVPN_PASSWD_AUTH} = "true" ]; then
mkdir -p /etc/openvpn/scripts/
cp -f /etc/openvpn/setup/auth.sh /etc/openvpn/scripts/auth.sh
chmod +x /etc/openvpn/scripts/auth.sh
echo "auth-user-pass-verify /etc/openvpn/scripts/auth.sh via-file" | tee -a /etc/openvpn/openvpn.conf
echo "script-security 2" | tee -a /etc/openvpn/openvpn.conf
echo "verify-client-cert require" | tee -a /etc/openvpn/openvpn.conf
openvpn-user db-init --db.path=$EASY_RSA_LOC/pki/users.db
fi
[ -d $EASY_RSA_LOC/pki ] && chmod 755 $EASY_RSA_LOC/pki
[ -f $EASY_RSA_LOC/pki/crl.pem ] && chmod 644 $EASY_RSA_LOC/pki/crl.pem
mkdir -p /etc/openvpn/ccd
openvpn --config /etc/openvpn/openvpn.conf --client-config-dir /etc/openvpn/ccd --port 1194 --proto tcp --management 127.0.0.1 8989 --dev tun0 --server ${OVPN_SRV_NET} ${OVPN_SRV_MASK}

View file

@ -1,4 +1,4 @@
server 172.16.100.0 255.255.255.0
# server 172.16.100.0 255.255.255.0
verb 3
tls-server
ca /etc/openvpn/easyrsa/pki/ca.crt
@ -8,19 +8,19 @@ dh /etc/openvpn/easyrsa/pki/dh.pem
crl-verify /etc/openvpn/easyrsa/pki/crl.pem
tls-auth /etc/openvpn/easyrsa/pki/ta.key
key-direction 0
duplicate-cn
cipher AES-128-CBC
management 127.0.0.1 8989
#management 127.0.0.1 8989
keepalive 10 60
persist-key
persist-tun
topology subnet
proto tcp
port 1194
dev tun0
#duplicate-cn
#proto tcp
#port 1194
#dev tun0
status /tmp/openvpn-status.log
user nobody
group nogroup
push "topology subnet"
push "route-metric 9999"
push "dhcp-option DNS 1.1.1.1"
push "dhcp-option DNS 1.1.1.1"

4
start-with-slave.sh Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env bash
./start.sh
docker-compose -p openvpn-slave -f docker-compose-slave.yaml up -d

3
start.sh Executable file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
docker compose -p openvpn-master up -d --build

View file

@ -1,5 +1,5 @@
{{- if (ne .ClientAddress "dynamic") }}
ifconfig-push {{ .ClientAddress }} 255.255.255.255
ifconfig-push {{ .ClientAddress }} 255.255.255.0
{{- end }}
{{- range $route := .CustomRoutes }}
push "route {{ $route.Address }} {{ $route.Mask }}" # {{ $route.Description }}

38
templates/client.conf.tpl Normal file
View file

@ -0,0 +1,38 @@
{{- range $server := .Hosts }}
remote {{ $server.Host }} {{ $server.Port }} {{ $server.Protocol }}
{{- end }}
verb 4
client
nobind
dev tun
cipher AES-128-CBC
key-direction 1
#redirect-gateway def1
tls-client
remote-cert-tls server
# uncomment below lines for use with linux
#script-security 2
# if you use resolved
#up /etc/openvpn/update-resolv-conf
#down /etc/openvpn/update-resolv-conf
# if you use systemd-resolved first install openvpn-systemd-resolved package
#up /etc/openvpn/update-systemd-resolved
#down /etc/openvpn/update-systemd-resolved
{{- if .PasswdAuth }}
auth-user-pass
{{- end }}
<cert>
{{ .Cert -}}
</cert>
<key>
{{ .Key -}}
</key>
<ca>
{{ .CA -}}
</ca>
<tls-auth>
{{ .TLS -}}
</tls-auth>

118
werf.yaml
View file

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