Compare commits
111 commits
Author | SHA1 | Date | |
---|---|---|---|
|
7134815ce6 | ||
|
0c881c81e7 | ||
|
699cddc908 | ||
|
c83c581e21 | ||
|
35f76ec3b6 | ||
|
8a35f70364 | ||
|
4981dcb919 | ||
|
dbc48ef3f1 | ||
|
f4d0212bfc | ||
|
9024405232 | ||
|
dfe2f3d756 | ||
|
a973b88463 | ||
|
3f3976ff7a | ||
|
5d41f6d91e | ||
|
7db35753ad | ||
|
10f7441c49 | ||
|
ca53605554 | ||
|
d012141b51 | ||
|
0dfe9f9494 | ||
|
3e599eb989 | ||
|
67dd4538ad | ||
|
5146d04a0d | ||
|
a0daf5b4d7 | ||
|
f369639a2a | ||
|
5a6724bf6a | ||
|
53119e17b2 | ||
|
5705d2f60b | ||
|
99569daf31 | ||
|
47abe3bc1a | ||
|
cd746c20b5 | ||
|
b26d0968e1 | ||
|
7f1da70b9d | ||
|
4b7ef65a66 | ||
|
77adc1108c | ||
|
9b1b34d4c4 | ||
|
5af16605d2 | ||
|
f180a9cc5a | ||
|
0ee9be5744 | ||
|
f73626dd7b | ||
|
633ad79d6a | ||
|
d3b5a77efb | ||
|
af65b36d2b | ||
|
2d75fb1f4b | ||
|
5ddbfe81ed | ||
|
9873c2cb76 | ||
|
4bdb74411c | ||
|
b378ae17dd | ||
|
1b421070cb | ||
|
3c77273990 | ||
|
53e9cb7835 | ||
|
f5507be24d | ||
|
5543829717 | ||
|
2daadf30ae | ||
|
ed71ed1537 | ||
|
a5d8f3dd96 | ||
|
61905a911a | ||
|
214d7b30c5 | ||
|
e07c1b6dfe | ||
|
c27229e920 | ||
|
8db6d93bcb | ||
|
c1970c26e4 | ||
|
924230c6ba | ||
|
d17ea9aee9 | ||
|
9fa3c44f9a | ||
|
77c0fbb778 | ||
|
ace42f729e | ||
|
2012f07135 | ||
|
63d76b5991 | ||
|
fa9022ee1b | ||
|
44c2b34f6d | ||
|
05d7462a79 | ||
|
f4d8a4ab01 | ||
|
c954d05f26 | ||
|
8c36329b0c | ||
|
edaf13f557 | ||
|
f5eb9eaff6 | ||
|
f1251307bd | ||
|
5c43db3269 | ||
|
e0d2355024 | ||
|
e7eb841805 | ||
|
9629f51e76 | ||
|
5c5c874788 | ||
|
f735051916 | ||
|
30bdc3243f | ||
|
409377d014 | ||
|
8fe289ad5f | ||
|
c31a40fa13 | ||
|
c145fc7a10 | ||
|
f96af697e8 | ||
|
5586ab7d06 | ||
|
0e6d21ab44 | ||
|
f14e49bed5 | ||
|
db354c0ae0 | ||
|
0a410a1724 | ||
|
aebab90257 | ||
|
da53aaae63 | ||
|
75a383f259 | ||
|
6eb14a0397 | ||
|
d7b58621ee | ||
|
bcd7b9ee62 | ||
|
4f5847f7c2 | ||
|
ece35822c1 | ||
|
6d6c35fb9a | ||
|
a1c0bb6b4b | ||
|
3db757659a | ||
|
3614ab6ba5 | ||
|
bec0e738d1 | ||
|
0af5fc3622 | ||
|
8d82c43fa2 | ||
|
bb257692af | ||
|
7df1ea3a0d |
56 changed files with 8491 additions and 6913 deletions
|
@ -3,7 +3,7 @@
|
||||||
*.iml
|
*.iml
|
||||||
out
|
out
|
||||||
gen
|
gen
|
||||||
|
.github
|
||||||
|
|
||||||
easyrsa
|
easyrsa
|
||||||
easyrsa_master
|
easyrsa_master
|
||||||
|
@ -13,6 +13,14 @@ ccd_master
|
||||||
ccd_slave
|
ccd_slave
|
||||||
werf.yaml
|
werf.yaml
|
||||||
frontend/node_modules
|
frontend/node_modules
|
||||||
|
frontend/static/dist
|
||||||
openvpn-web-ui
|
openvpn-web-ui
|
||||||
openvpn-ui
|
openvpn-ui
|
||||||
openvpn-admin
|
openvpn-admin
|
||||||
|
ovpn-admin
|
||||||
|
|
||||||
|
docker-compose.yaml
|
||||||
|
docker-compose-slave.yaml
|
||||||
|
img
|
||||||
|
dashboard
|
||||||
|
.helm
|
||||||
|
|
24
.editorconfig
Normal file
24
.editorconfig
Normal 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
35
.github/workflows/publish-latest.yaml
vendored
Normal 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
39
.github/workflows/publish-tag.yaml
vendored
Normal 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
29
.github/workflows/release.yaml
vendored
Normal 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
29
.github/workflows/release_arm.yaml
vendored
Normal 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 }}
|
10
.gitignore
vendored
10
.gitignore
vendored
|
@ -4,11 +4,15 @@ easyrsa_slave
|
||||||
ccd
|
ccd
|
||||||
ccd_master
|
ccd_master
|
||||||
ccd_slave
|
ccd_slave
|
||||||
openvpn-web-ui
|
openvpn-web-ui*
|
||||||
openvpn-ui
|
openvpn-ui*
|
||||||
openvpn-admin
|
openvpn-admin*
|
||||||
|
ovpn-admin*
|
||||||
frontend/node_modules
|
frontend/node_modules
|
||||||
|
|
||||||
|
main-packr.go
|
||||||
|
packrd/
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.idea
|
.idea
|
||||||
*.suo
|
*.suo
|
||||||
|
|
29
Dockerfile
29
Dockerfile
|
@ -1,19 +1,20 @@
|
||||||
FROM golang:1.14.2-alpine3.11 AS backend-builder
|
FROM node:16-alpine3.15 AS frontend-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
|
|
||||||
COPY frontend/ /app
|
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
|
WORKDIR /app
|
||||||
COPY --from=backend-builder /app/openvpn-admin /app
|
COPY --from=backend-builder /app/ovpn-admin /app
|
||||||
COPY --from=frontend-builder /app/static /app/static
|
ARG TARGETARCH
|
||||||
COPY client.conf.tpl /app/client.conf.tpl
|
RUN apk add --update bash easy-rsa openssl openvpn coreutils && \
|
||||||
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 && \
|
|
||||||
ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \
|
ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \
|
||||||
|
wget https://github.com/pashcovich/openvpn-user/releases/download/v1.0.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/*
|
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
|
|
@ -1,7 +1,9 @@
|
||||||
FROM alpine:3.11
|
FROM alpine:3.16
|
||||||
RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \
|
ARG TARGETARCH
|
||||||
apk add --update bash openvpn easy-rsa && \
|
RUN apk add --update bash openvpn easy-rsa iptables && \
|
||||||
ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \
|
ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \
|
||||||
|
wget https://github.com/pashcovich/openvpn-user/releases/download/v1.0.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/*
|
rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*
|
||||||
COPY .werffiles /etc/openvpn/setup
|
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
|
RUN chmod +x /etc/openvpn/setup/configure.sh
|
192
README.md
192
README.md
|
@ -1,25 +1,177 @@
|
||||||
# openvpn-admin
|
# 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
|
||||||
```
|
```
|
||||||
usage: openvpn-admin [<flags>]
|
#### 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:
|
Flags:
|
||||||
--help Show context-sensitive help (also try --help-long and --help-man).
|
--help show context-sensitive help (try also --help-long and --help-man)
|
||||||
--listen.host="0.0.0.0" host(s) for openvpn-admin
|
|
||||||
--listen.port="8080" port for openvpn-admin
|
--listen.host="0.0.0.0" host for ovpn-admin
|
||||||
--role="master" server role master or slave
|
(or OVPN_LISTEN_HOST)
|
||||||
--master.host="http://127.0.0.1" url for master server
|
|
||||||
--master.basic-auth.user="" user for basic auth on master server url
|
--listen.port="8080" port for ovpn-admin
|
||||||
--master.basic-auth.password="" password for basic auth on master server url
|
(or OVPN_LISTEN_PORT)
|
||||||
--master.sync-frequency=600 master host data sync frequency in seconds.
|
|
||||||
|
--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
|
--master.sync-token=TOKEN master host data sync security token
|
||||||
--ovpn.host=HOST:PORT ... host for openvpn server
|
(or OVPN_MASTER_TOKEN)
|
||||||
--ovpn.network="172.16.100.0/24" network for openvpn server
|
|
||||||
--mgmt.host="127.0.0.1" host for openvpn server mgmt interface
|
--ovpn.network="172.16.100.0/24"
|
||||||
--mgmt.port="8989" port for openvpn server mgmt interface
|
(or OVPN_NETWORK) NETWORK/MASK_PREFIX for OpenVPN server
|
||||||
--easyrsa.path="/mnt/easyrsa" path to easyrsa dir
|
|
||||||
--easyrsa.index-path="/mnt/easyrsa/pki/index.txt"
|
--ovpn.server=HOST:PORT:PROTOCOL ...
|
||||||
path to easyrsa index file.
|
(or OVPN_SERVER) HOST:PORT:PROTOCOL for OpenVPN server
|
||||||
--ccd.path="/mnt/ccd" path to client-config-dir
|
can have multiple values
|
||||||
--static.path="./static" path to static dir
|
|
||||||
--debug Enable debug mode.
|
--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.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
mkdir -p easyrsa
|
mkdir -p {easyrsa,ccd}
|
||||||
|
|
||||||
cd easyrsa
|
cd easyrsa
|
||||||
|
|
12
build.sh
12
build.sh
|
@ -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
18
build_arm.sh
Executable 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
|
6
ccd.tpl
6
ccd.tpl
|
@ -1,6 +0,0 @@
|
||||||
{{- if (ne .ClientAddress "dynamic") }}
|
|
||||||
ifconfig-push {{ .ClientAddress }} 255.255.255.255
|
|
||||||
{{- end }}
|
|
||||||
{{- range $route := .CustomRoutes }}
|
|
||||||
push "route {{ $route.Address }} {{ $route.Mask }}" ; {{ $route.Description }}
|
|
||||||
{{- end }}
|
|
195
certificates.go
Normal file
195
certificates.go
Normal 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
|
||||||
|
}
|
|
@ -1,29 +0,0 @@
|
||||||
{{- range $server := .Hosts }}
|
|
||||||
remote {{ $server.Host }} {{ $server.Port }} tcp
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
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
974
dashboard/ovpn-admin.json
Normal 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": ""
|
||||||
|
}
|
|
@ -8,22 +8,22 @@ services:
|
||||||
image: openvpn:local
|
image: openvpn:local
|
||||||
command: /etc/openvpn/setup/configure.sh
|
command: /etc/openvpn/setup/configure.sh
|
||||||
environment:
|
environment:
|
||||||
- OPVN_ROLE=slave
|
- OVPN_ROLE=slave
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
ports:
|
ports:
|
||||||
- 7778:1194 # for openvpn
|
- 7778:1194 # for openvpn
|
||||||
- 8081:8080 # for openvpn-admin because of network_mode
|
- 8081:8080 # for ovpn-admin because of network_mode
|
||||||
volumes:
|
volumes:
|
||||||
- ./easyrsa_slave:/etc/openvpn/easyrsa
|
- ./easyrsa_slave:/etc/openvpn/easyrsa
|
||||||
- ./ccd_slave:/etc/openvpn/ccd
|
- ./ccd_slave:/etc/openvpn/ccd
|
||||||
openvpn-admin:
|
ovpn-admin:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
image: openvpn-admin:local
|
image: ovpn-admin:local
|
||||||
command: /app/openvpn-admin --debug --ovpn.network="172.16.100.0/22" --master.sync-token="TOKEN" --master.host="http://172.20.0.1:8080" --role="slave" --ovpn.host="127.0.0.1:7744" --ovpn.host="127.0.0.1:7778"
|
command: /app/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:
|
environment:
|
||||||
- OPVN_SLAVE=1
|
- OVPN_SLAVE=1
|
||||||
network_mode: service:openvpn
|
network_mode: service:openvpn
|
||||||
volumes:
|
volumes:
|
||||||
- ./easyrsa_slave:/mnt/easyrsa
|
- ./easyrsa_slave:/mnt/easyrsa
|
||||||
|
|
|
@ -7,19 +7,35 @@ services:
|
||||||
dockerfile: Dockerfile.openvpn
|
dockerfile: Dockerfile.openvpn
|
||||||
image: openvpn:local
|
image: openvpn:local
|
||||||
command: /etc/openvpn/setup/configure.sh
|
command: /etc/openvpn/setup/configure.sh
|
||||||
|
environment:
|
||||||
|
OVPN_SERVER_NET: "192.168.100.0"
|
||||||
|
OVPN_SERVER_MASK: "255.255.255.0"
|
||||||
|
OVPN_PASSWD_AUTH: "true"
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
ports:
|
ports:
|
||||||
- 7777:1194 # for openvpn
|
- 7777:1194 # for openvpn
|
||||||
- 8080:8080 # for openvpn-admin because of network_mode
|
- 8080:8080 # for ovpn-admin because of network_mode
|
||||||
volumes:
|
volumes:
|
||||||
- ./easyrsa_master:/etc/openvpn/easyrsa
|
- ./easyrsa_master:/etc/openvpn/easyrsa
|
||||||
- ./ccd_master:/etc/openvpn/ccd
|
- ./ccd_master:/etc/openvpn/ccd
|
||||||
openvpn-admin:
|
ovpn-admin:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
image: openvpn-admin:local
|
image: ovpn-admin:local
|
||||||
command: /app/openvpn-admin --debug --ovpn.network="172.16.100.0/22" --master.sync-token="TOKEN"
|
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
|
network_mode: service:openvpn
|
||||||
volumes:
|
volumes:
|
||||||
- ./easyrsa_master:/mnt/easyrsa
|
- ./easyrsa_master:/mnt/easyrsa
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
image="node:14.2-alpine3.11"
|
image="node:16.13.0-alpine3.12"
|
||||||
uid="$(id -u $USER)"
|
uid="$(id -u $USER)"
|
||||||
|
|
||||||
docker run -u $uid -w /app -v $(pwd):/app $image npm i && \
|
docker run -u $uid -w /app -v $(pwd):/app $image npm i && \
|
||||||
|
|
8561
frontend/package-lock.json
generated
8561
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "openvpn-admin",
|
"name": "ovpn-admin",
|
||||||
"description": "Vue.js admin ui for openvpn and easyrsa",
|
"description": "Vue.js admin ui for openvpn and easyrsa",
|
||||||
"version": "1.0.1a",
|
"version": "1.0.1a",
|
||||||
"author": "vitaliy.snurnitsin@gmail.com",
|
"author": "vitaliy.snurnitsin@gmail.com",
|
||||||
|
@ -7,14 +7,18 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cross-env NODE_ENV=development webpack-dev-server --hot",
|
"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": {
|
"dependencies": {
|
||||||
"axios": "^0.19.2",
|
"axios": "^0.27.1",
|
||||||
"vue": "^2.6.12",
|
"bootstrap-vue": "^2.22.0",
|
||||||
"vue-clipboard2": "^0.2.1",
|
"normalize.css": "^8.0.1",
|
||||||
|
"vue": "^2.6.14",
|
||||||
|
"vue-clipboard2": "^0.3.3",
|
||||||
"vue-cookies": "^1.7.4",
|
"vue-cookies": "^1.7.4",
|
||||||
"vue-good-table": "^2.21.1"
|
"vue-good-table": "^2.21.11",
|
||||||
|
"vue-notification": "^1.3.20",
|
||||||
|
"vue-style-loader": "^4.1.3"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
|
@ -22,23 +26,23 @@
|
||||||
"not ie <= 8"
|
"not ie <= 8"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.8.6",
|
"@babel/core": "^7.16.5",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.0.0",
|
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
||||||
"@babel/plugin-proposal-json-strings": "^7.0.0",
|
"@babel/plugin-proposal-json-strings": "^7.16.7",
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
|
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||||
"@babel/plugin-syntax-import-meta": "^7.0.0",
|
"@babel/plugin-syntax-import-meta": "^7.10.4",
|
||||||
"@babel/preset-env": "^7.0.0",
|
"@babel/preset-env": "^7.16.5",
|
||||||
"babel-loader": "^8.0.0",
|
"babel-loader": "^8.2.3",
|
||||||
"cross-env": "^7.0.0",
|
"cross-env": "^7.0.3",
|
||||||
"css-loader": "^3.4.2",
|
"css-loader": "^6.5.1",
|
||||||
"file-loader": "^5.1.0",
|
"file-loader": "^6.2.0",
|
||||||
"node-sass": "^4.13.1",
|
"node-sass": "^7.0.1",
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^12.4.0",
|
||||||
"terser-webpack-plugin": "^2.3.5",
|
"terser-webpack-plugin": "^5.3.0",
|
||||||
"vue-loader": "^15.9.0",
|
"vue-loader": "^17.0.0",
|
||||||
"vue-template-compiler": "^2.6.11",
|
"vue-template-compiler": "^2.6.14",
|
||||||
"webpack": "^4.42.0",
|
"webpack": "^5.65.0",
|
||||||
"webpack-cli": "^3.3.11",
|
"webpack-cli": "^4.9.1",
|
||||||
"webpack-dev-server": "^3.10.3"
|
"webpack-dev-server": "^4.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import VueCookies from 'vue-cookies'
|
import VueCookies from 'vue-cookies'
|
||||||
|
import BootstrapVue from 'bootstrap-vue'
|
||||||
import VueClipboard from 'vue-clipboard2'
|
import VueClipboard from 'vue-clipboard2'
|
||||||
|
import Notifications from 'vue-notification'
|
||||||
import VueGoodTablePlugin from 'vue-good-table'
|
import VueGoodTablePlugin from 'vue-good-table'
|
||||||
|
|
||||||
import 'vue-good-table/dist/vue-good-table.css'
|
|
||||||
|
|
||||||
Vue.use(VueClipboard)
|
|
||||||
Vue.use(VueGoodTablePlugin)
|
|
||||||
Vue.use(VueCookies)
|
Vue.use(VueCookies)
|
||||||
|
Vue.use(VueClipboard)
|
||||||
|
Vue.use(BootstrapVue)
|
||||||
|
Vue.use(Notifications)
|
||||||
|
Vue.use(VueGoodTablePlugin)
|
||||||
|
|
||||||
var axios_cfg = function(url, data='', type='form') {
|
var axios_cfg = function(url, data='', type='form') {
|
||||||
if (data == '') {
|
if (data == '') {
|
||||||
|
@ -54,6 +56,11 @@ new Vue({
|
||||||
field: 'AccountStatus',
|
field: 'AccountStatus',
|
||||||
filterable: true,
|
filterable: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Active Connections',
|
||||||
|
field: 'Connections',
|
||||||
|
filterable: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Expiration Date',
|
label: 'Expiration Date',
|
||||||
field: 'ExpirationDate',
|
field: 'ExpirationDate',
|
||||||
|
@ -84,41 +91,93 @@ new Vue({
|
||||||
],
|
],
|
||||||
rows: [],
|
rows: [],
|
||||||
actions: [
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'u-change-password',
|
||||||
|
label: 'Change password',
|
||||||
|
class: 'btn-warning',
|
||||||
|
showWhenStatus: 'Active',
|
||||||
|
showForServerRole: ['master'],
|
||||||
|
showForModule: ['passwdAuth'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'u-revoke',
|
name: 'u-revoke',
|
||||||
label: 'Revoke',
|
label: 'Revoke',
|
||||||
|
class: 'btn-warning',
|
||||||
showWhenStatus: 'Active',
|
showWhenStatus: 'Active',
|
||||||
showForServerRole: ['master']
|
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',
|
name: 'u-unrevoke',
|
||||||
label: 'Unrevoke',
|
label: 'Unrevoke',
|
||||||
|
class: 'btn-primary',
|
||||||
showWhenStatus: 'Revoked',
|
showWhenStatus: 'Revoked',
|
||||||
showForServerRole: ['master']
|
showForServerRole: ['master'],
|
||||||
},
|
showForModule: ["core"],
|
||||||
{
|
|
||||||
name: 'u-show-config',
|
|
||||||
label: 'Show config',
|
|
||||||
showWhenStatus: 'Active',
|
|
||||||
showForServerRole: ['master', 'slave']
|
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// name: 'u-show-config',
|
||||||
|
// label: 'Show config',
|
||||||
|
// class: 'btn-primary',
|
||||||
|
// showWhenStatus: 'Active',
|
||||||
|
// showForServerRole: ['master', 'slave'],
|
||||||
|
// showForModule: ["core"],
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
name: 'u-download-config',
|
name: 'u-download-config',
|
||||||
label: 'Download config',
|
label: 'Download config',
|
||||||
|
class: 'btn-info',
|
||||||
showWhenStatus: 'Active',
|
showWhenStatus: 'Active',
|
||||||
showForServerRole: ['master', 'slave']
|
showForServerRole: ['master', 'slave'],
|
||||||
|
showForModule: ["core"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'u-edit-ccd',
|
name: 'u-edit-ccd',
|
||||||
label: 'Edit routes',
|
label: 'Edit routes',
|
||||||
|
class: 'btn-primary',
|
||||||
showWhenStatus: 'Active',
|
showWhenStatus: 'Active',
|
||||||
showForServerRole: ['master']
|
showForServerRole: ['master'],
|
||||||
|
showForModule: ["ccd"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'u-edit-ccd',
|
name: 'u-edit-ccd',
|
||||||
label: 'Show routes',
|
label: 'Show routes',
|
||||||
|
class: 'btn-primary',
|
||||||
showWhenStatus: 'Active',
|
showWhenStatus: 'Active',
|
||||||
showForServerRole: ['slave']
|
showForServerRole: ['slave'],
|
||||||
|
showForModule: ["ccd"],
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
filters: {
|
filters: {
|
||||||
|
@ -126,13 +185,22 @@ new Vue({
|
||||||
},
|
},
|
||||||
serverRole: "master",
|
serverRole: "master",
|
||||||
lastSync: "unknown",
|
lastSync: "unknown",
|
||||||
|
modulesEnabled: [],
|
||||||
u: {
|
u: {
|
||||||
newUserName: '',
|
newUserName: '',
|
||||||
// newUserPassword: 'nopass',
|
newUserPassword: '',
|
||||||
newUserCreateError: '',
|
newUserCreateError: '',
|
||||||
|
newPassword: '',
|
||||||
|
passwordChangeStatus: '',
|
||||||
|
passwordChangeMessage: '',
|
||||||
|
rotateUserMessage: '',
|
||||||
|
deleteUserMessage: '',
|
||||||
modalNewUserVisible: false,
|
modalNewUserVisible: false,
|
||||||
modalShowConfigVisible: false,
|
modalShowConfigVisible: false,
|
||||||
modalShowCcdVisible: false,
|
modalShowCcdVisible: false,
|
||||||
|
modalChangePasswordVisible: false,
|
||||||
|
modalRotateUserVisible: false,
|
||||||
|
modalDeleteUserVisible: false,
|
||||||
openvpnConfig: '',
|
openvpnConfig: '',
|
||||||
ccd: {
|
ccd: {
|
||||||
Name: '',
|
Name: '',
|
||||||
|
@ -148,7 +216,7 @@ new Vue({
|
||||||
},
|
},
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
this.getUserData();
|
this.getUserData();
|
||||||
this.getServerRole();
|
this.getServerSetting();
|
||||||
this.filters.hideRevoked = this.$cookies.isKey('hideRevoked') ? (this.$cookies.get('hideRevoked') == "true") : false
|
this.filters.hideRevoked = this.$cookies.isKey('hideRevoked') ? (this.$cookies.get('hideRevoked') == "true") : false
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
@ -160,6 +228,7 @@ new Vue({
|
||||||
axios.request(axios_cfg('api/user/revoke', data, 'form'))
|
axios.request(axios_cfg('api/user/revoke', data, 'form'))
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
_this.getUserData();
|
_this.getUserData();
|
||||||
|
_this.$notify({title: 'User ' + _this.username + ' revoked!', type: 'warn'})
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
_this.$root.$on('u-unrevoke', function () {
|
_this.$root.$on('u-unrevoke', function () {
|
||||||
|
@ -168,8 +237,19 @@ new Vue({
|
||||||
axios.request(axios_cfg('api/user/unrevoke', data, 'form'))
|
axios.request(axios_cfg('api/user/unrevoke', data, 'form'))
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
_this.getUserData();
|
_this.getUserData();
|
||||||
|
_this.$notify({title: 'User ' + _this.username + ' unrevoked!', type: 'success'})
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
_this.$root.$on('u-rotate', function () {
|
||||||
|
_this.u.modalRotateUserVisible = true;
|
||||||
|
var data = new URLSearchParams();
|
||||||
|
data.append('username', _this.username);
|
||||||
|
})
|
||||||
|
_this.$root.$on('u-delete', function () {
|
||||||
|
_this.u.modalDeleteUserVisible = true;
|
||||||
|
var data = new URLSearchParams();
|
||||||
|
data.append('username', _this.username);
|
||||||
|
})
|
||||||
_this.$root.$on('u-show-config', function () {
|
_this.$root.$on('u-show-config', function () {
|
||||||
_this.u.modalShowConfigVisible = true;
|
_this.u.modalShowConfigVisible = true;
|
||||||
var data = new URLSearchParams();
|
var data = new URLSearchParams();
|
||||||
|
@ -207,17 +287,31 @@ new Vue({
|
||||||
data.append('username', _this.username);
|
data.append('username', _this.username);
|
||||||
axios.request(axios_cfg('api/user/disconnect', data, 'form'))
|
axios.request(axios_cfg('api/user/disconnect', data, 'form'))
|
||||||
.then(function(response) {
|
.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: {
|
computed: {
|
||||||
customAddressDisabled: function () {
|
customAddressDynamic: function () {
|
||||||
return this.serverRole == "master" ? this.u.ccd.ClientAddress == "dynamic" : true
|
return this.u.ccd.ClientAddress == "dynamic"
|
||||||
},
|
},
|
||||||
ccdApplyStatusCssClass: function () {
|
ccdApplyStatusCssClass: function () {
|
||||||
return this.u.ccdApplyStatus == 200 ? "alert-success" : "alert-danger"
|
return this.u.ccdApplyStatus == 200 ? "alert-success" : "alert-danger"
|
||||||
},
|
},
|
||||||
|
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 () {
|
modalNewUserDisplay: function () {
|
||||||
return this.u.modalNewUserVisible ? {display: 'flex'} : {}
|
return this.u.modalNewUserVisible ? {display: 'flex'} : {}
|
||||||
},
|
},
|
||||||
|
@ -227,6 +321,15 @@ new Vue({
|
||||||
modalShowCcdDisplay: function () {
|
modalShowCcdDisplay: function () {
|
||||||
return this.u.modalShowCcdVisible ? {display: 'flex'} : {}
|
return this.u.modalShowCcdVisible ? {display: 'flex'} : {}
|
||||||
},
|
},
|
||||||
|
modalChangePasswordDisplay: function () {
|
||||||
|
return this.u.modalChangePasswordVisible ? {display: 'flex'} : {}
|
||||||
|
},
|
||||||
|
modalRotateUserDisplay: function () {
|
||||||
|
return this.u.modalRotateUserVisible ? {display: 'flex'} : {}
|
||||||
|
},
|
||||||
|
modalDeleteUserDisplay: function () {
|
||||||
|
return this.u.modalDeleteUserVisible ? {display: 'flex'} : {}
|
||||||
|
},
|
||||||
revokeFilterText: function() {
|
revokeFilterText: function() {
|
||||||
return this.filters.hideRevoked ? "Show revoked" : "Hide revoked"
|
return this.filters.hideRevoked ? "Show revoked" : "Hide revoked"
|
||||||
},
|
},
|
||||||
|
@ -243,7 +346,16 @@ new Vue({
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
rowStyleClassFn: function(row) {
|
rowStyleClassFn: function(row) {
|
||||||
return row.ConnectionStatus == 'Connected' ? 'connected-user' : ''
|
if (row.ConnectionStatus == 'Connected') {
|
||||||
|
return 'connected-user'
|
||||||
|
}
|
||||||
|
if (row.AccountStatus == 'Revoked') {
|
||||||
|
return 'revoked-user'
|
||||||
|
}
|
||||||
|
if (row.AccountStatus == 'Expired') {
|
||||||
|
return 'expired-user'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
},
|
},
|
||||||
rowActionFn: function(e) {
|
rowActionFn: function(e) {
|
||||||
this.username = e.target.dataset.username;
|
this.username = e.target.dataset.username;
|
||||||
|
@ -253,14 +365,17 @@ new Vue({
|
||||||
var _this = this;
|
var _this = this;
|
||||||
axios.request(axios_cfg('api/users/list'))
|
axios.request(axios_cfg('api/users/list'))
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
_this.rows = response.data;
|
_this.rows = Array.isArray(response.data) ? response.data : [];
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getServerRole: function() {
|
|
||||||
|
getServerSetting: function() {
|
||||||
var _this = this;
|
var _this = this;
|
||||||
axios.request(axios_cfg('api/server/role'))
|
axios.request(axios_cfg('api/server/settings'))
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
_this.serverRole = response.data.serverRole;
|
_this.serverRole = response.data.serverRole;
|
||||||
|
_this.modulesEnabled = response.data.modules;
|
||||||
|
|
||||||
if (_this.serverRole == "slave") {
|
if (_this.serverRole == "slave") {
|
||||||
axios.request(axios_cfg('api/sync/last/successful'))
|
axios.request(axios_cfg('api/sync/last/successful'))
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
|
@ -269,6 +384,7 @@ new Vue({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
createUser: function() {
|
createUser: function() {
|
||||||
var _this = this;
|
var _this = this;
|
||||||
|
|
||||||
|
@ -276,19 +392,25 @@ new Vue({
|
||||||
|
|
||||||
var data = new URLSearchParams();
|
var data = new URLSearchParams();
|
||||||
data.append('username', _this.u.newUserName);
|
data.append('username', _this.u.newUserName);
|
||||||
// data.append('password', this.u.newUserPassword);
|
data.append('password', _this.u.newUserPassword);
|
||||||
|
|
||||||
|
_this.username = _this.u.newUserName;
|
||||||
|
|
||||||
axios.request(axios_cfg('api/user/create', data, 'form'))
|
axios.request(axios_cfg('api/user/create', data, 'form'))
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
_this.getUserData();
|
_this.$notify({title: 'New user ' + _this.username + ' created', type: 'success'})
|
||||||
_this.u.modalNewUserVisible = false;
|
_this.u.modalNewUserVisible = false;
|
||||||
_this.u.newUserName = '';
|
_this.u.newUserName = '';
|
||||||
// _this.u.newUserPassword = 'nopass';
|
_this.u.newUserPassword = '';
|
||||||
|
_this.getUserData();
|
||||||
})
|
})
|
||||||
.catch(function(error) {
|
.catch(function(error) {
|
||||||
_this.u.newUserCreateError = error.response.data;
|
_this.u.newUserCreateError = error.response.data;
|
||||||
|
_this.$notify({title: 'New user ' + _this.username + ' creation failed.', type: 'error'})
|
||||||
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
ccdApply: function() {
|
ccdApply: function() {
|
||||||
var _this = this;
|
var _this = this;
|
||||||
|
|
||||||
|
@ -299,11 +421,84 @@ new Vue({
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
_this.u.ccdApplyStatus = 200;
|
_this.u.ccdApplyStatus = 200;
|
||||||
_this.u.ccdApplyStatusMessage = response.data;
|
_this.u.ccdApplyStatusMessage = response.data;
|
||||||
|
_this.$notify({title: 'Ccd for user ' + _this.username + ' applied', type: 'success'})
|
||||||
})
|
})
|
||||||
.catch(function(error) {
|
.catch(function(error) {
|
||||||
_this.u.ccdApplyStatus = error.response.status;
|
_this.u.ccdApplyStatus = error.response.status;
|
||||||
_this.u.ccdApplyStatusMessage = error.response.data;
|
_this.u.ccdApplyStatusMessage = error.response.data;
|
||||||
|
_this.$notify({title: 'Ccd for user ' + _this.username + ' apply failed ', type: 'error'})
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
}
|
|
||||||
|
changeUserPassword: function(user) {
|
||||||
|
var _this = this;
|
||||||
|
|
||||||
|
_this.u.passwordChangeMessage = "";
|
||||||
|
|
||||||
|
var data = new URLSearchParams();
|
||||||
|
data.append('username', user);
|
||||||
|
data.append('password', _this.u.newPassword);
|
||||||
|
|
||||||
|
axios.request(axios_cfg('api/user/change-password', data, 'form'))
|
||||||
|
.then(function(response) {
|
||||||
|
_this.u.passwordChangeStatus = 200;
|
||||||
|
_this.u.newPassword = '';
|
||||||
|
_this.getUserData();
|
||||||
|
_this.u.modalChangePasswordVisible = false;
|
||||||
|
_this.$notify({title: 'Password for user ' + _this.username + ' changed!', type: 'success'})
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
_this.u.passwordChangeStatus = error.response.status;
|
||||||
|
_this.u.passwordChangeMessage = error.response.data.message;
|
||||||
|
_this.$notify({title: 'Changing password for user ' + _this.username + ' failed!', type: 'error'})
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
rotateUser: function(user) {
|
||||||
|
var _this = this;
|
||||||
|
|
||||||
|
_this.u.rotateUserMessage = "";
|
||||||
|
|
||||||
|
var data = new URLSearchParams();
|
||||||
|
data.append('username', user);
|
||||||
|
data.append('password', _this.u.newPassword);
|
||||||
|
|
||||||
|
axios.request(axios_cfg('api/user/rotate', data, 'form'))
|
||||||
|
.then(function(response) {
|
||||||
|
_this.u.roatateUserStatus = 200;
|
||||||
|
_this.u.newPassword = '';
|
||||||
|
_this.getUserData();
|
||||||
|
_this.u.modalRotateUserVisible = false;
|
||||||
|
_this.$notify({title: 'Certificates for user ' + _this.username + ' rotated!', type: 'success'})
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
_this.u.roatateUserStatus = error.response.status;
|
||||||
|
_this.u.rotateUserMessage = error.response.data.message;
|
||||||
|
_this.$notify({title: 'Rotate certificates for user ' + _this.username + ' failed!', type: 'error'})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteUser: function(user) {
|
||||||
|
var _this = this;
|
||||||
|
|
||||||
|
_this.u.deleteUserMessage = "";
|
||||||
|
|
||||||
|
var data = new URLSearchParams();
|
||||||
|
data.append('username', user);
|
||||||
|
|
||||||
|
axios.request(axios_cfg('api/user/delete', data, 'form'))
|
||||||
|
.then(function(response) {
|
||||||
|
_this.u.deleteUserStatus = 200;
|
||||||
|
_this.u.newPassword = '';
|
||||||
|
_this.getUserData();
|
||||||
|
_this.u.modalDeleteUserVisible = false;
|
||||||
|
_this.$notify({title: 'User ' + _this.username + ' deleted!', type: 'success'})
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
_this.u.deleteUserStatus = error.response.status;
|
||||||
|
_this.u.deleteUserMessage = error.response.data.message;
|
||||||
|
_this.$notify({title: 'Deleting user ' + _this.username + ' failed!', type: 'error'})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -48,6 +48,14 @@ body {
|
||||||
background-color: rgba(162, 245, 169, 0.5);
|
background-color: rgba(162, 245, 169, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.revoked-user {
|
||||||
|
background-color: rgba(198, 186, 186, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired-user {
|
||||||
|
background-color: rgba(255, 220, 127, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
.new-user-btn {
|
.new-user-btn {
|
||||||
margin-right: 2rem;
|
margin-right: 2rem;
|
||||||
}
|
}
|
||||||
|
@ -58,3 +66,7 @@ body {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ccd-routes {
|
||||||
|
|
||||||
|
}
|
5
frontend/src/style.js
Normal file
5
frontend/src/style.js
Normal 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'
|
7
frontend/static/css/bootstrap.min.css
vendored
7
frontend/static/css/bootstrap.min.css
vendored
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
461
frontend/static/css/normalize.css
vendored
461
frontend/static/css/normalize.css
vendored
|
@ -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;
|
|
||||||
}
|
|
|
@ -2,12 +2,10 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>openvpn-admin</title>
|
<title>ovpn-admin</title>
|
||||||
<link rel="stylesheet" href="css/normalize.css">
|
|
||||||
<link rel="stylesheet" href="css/bootstrap.min.css">
|
|
||||||
<link rel="stylesheet" href="css/style.css">
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<script src="dist/style.min.js"></script>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<vue-good-table
|
<vue-good-table
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
|
@ -16,7 +14,7 @@
|
||||||
:row-style-class="rowStyleClassFn"
|
:row-style-class="rowStyleClassFn"
|
||||||
:search-options="{ enabled: true}" >
|
:search-options="{ enabled: true}" >
|
||||||
<div slot="table-actions">
|
<div slot="table-actions">
|
||||||
<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>
|
<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>
|
<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>
|
<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>
|
||||||
|
@ -28,7 +26,7 @@
|
||||||
<template slot="table-row" slot-scope="props">
|
<template slot="table-row" slot-scope="props">
|
||||||
<span v-if="props.column.field == 'actions'">
|
<span v-if="props.column.field == 'actions'">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-success el-square modal-el-margin"
|
class="btn btn-sm el-square modal-el-margin"
|
||||||
type="button"
|
type="button"
|
||||||
:title="action.label"
|
:title="action.label"
|
||||||
:data-username="props.row.Identity"
|
:data-username="props.row.Identity"
|
||||||
|
@ -36,17 +34,14 @@
|
||||||
:data-text="action.label"
|
:data-text="action.label"
|
||||||
@click.left.stop="rowActionFn"
|
@click.left.stop="rowActionFn"
|
||||||
v-for="action in actions"
|
v-for="action in actions"
|
||||||
v-if="action.showWhenStatus == props.row.AccountStatus && action.showForServerRole.includes(serverRole)">
|
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 }}
|
{{ action.label }}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</vue-good-table>
|
</vue-good-table>
|
||||||
|
|
||||||
<!-- <div class="d-flex justify-content-md-end">-->
|
|
||||||
<!-- <button type="button" class="btn btn-sm btn-success el-square new-user-btn" v-on:click.stop="u.ctxVisible=false;u.modalNewUserVisible=true">Add user</button>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
|
|
||||||
<div class="modal-wrapper" v-if="u.modalNewUserVisible" v-bind:style="modalNewUserDisplay">
|
<div class="modal-wrapper" v-if="u.modalNewUserVisible" v-bind:style="modalNewUserDisplay">
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
@ -55,7 +50,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<input type="text" class="form-control el-square modal-el-margin" placeholder="Username [_a-zA-Z0-9\.-]" v-model="u.newUserName">
|
<input type="text" class="form-control el-square modal-el-margin" placeholder="Username [_a-zA-Z0-9\.-]" v-model="u.newUserName">
|
||||||
<!-- <input type="password" class="form-control el-square modal-el-margin" minlength="6" autocomplete="off" placeholder="Password [_a-zA-Z0-9\.-]" v-model="u.newUserPassword">-->
|
<input type="password" class="form-control el-square modal-el-margin" minlength="6" autocomplete="off" placeholder="Password [_a-zA-Z0-9\.-]" v-model="u.newUserPassword" v-if="modulesEnabled.includes('passwdAuth')">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-footer justify-content-center" v-if="u.newUserCreateError.length > 0">
|
<div class="modal-footer justify-content-center" v-if="u.newUserCreateError.length > 0">
|
||||||
|
@ -64,18 +59,41 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="createUser();">Create</button>
|
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="createUser()">Create</button>
|
||||||
<button type="button" class="btn btn-primary el-square d-flex justify-content-sm-end modal-el-margin" v-on:click.stop="u.newUserName='';u.newUserPassword='nopass';u.modalNewUserVisible=false">Close</button>
|
<button type="button" class="btn btn-primary el-square d-flex justify-content-sm-end modal-el-margin" v-on:click.stop="u.newUserName='';u.newUserPassword='nopass';u.modalNewUserVisible=false">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-wrapper" v-if="u.modalChangePasswordVisible" v-bind:style="modalChangePasswordDisplay">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4>Change password for: <strong>{{ username }}</strong></h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="password" class="form-control el-square modal-el-margin" minlength="6" autocomplete="off" placeholder="Password [_a-zA-Z0-9\.-]" v-model="u.newPassword">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer justify-content-center" v-if="u.passwordChangeMessage.length > 0">
|
||||||
|
<div class="alert" v-bind:class="passwordChangeStatusCssClass" role="alert" >
|
||||||
|
{{ u.passwordChangeMessage }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="changeUserPassword(username)">Change password</button>
|
||||||
|
<button type="button" class="btn btn-primary el-square d-flex justify-content-sm-end modal-el-margin" v-on:click.stop="u.newPassword='';u.passwordChangeMessage='';u.modalChangePasswordVisible=false">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="modal-wrapper" v-if="u.modalShowConfigVisible" v-bind:style="modalShowConfigDisplay">
|
<div class="modal-wrapper" v-if="u.modalShowConfigVisible" v-bind:style="modalShowConfigDisplay">
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4>ovpn config for {{ username }}</h4>
|
<h4>ovpn config for: <strong>{{ username }}</strong></h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
|
@ -96,42 +114,57 @@
|
||||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
|
<h3 class="static-address-label ">Routes table for: <strong>{{ username }}</strong></h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<h4 class="static-address-label ">Client "{{ username }}" static address</h4>
|
<h5 class="static-address-label ">Static address:</h5>
|
||||||
<div class="input-group-prepend">
|
<input id="static-address" type="text" class="form-control" v-model="u.ccd.ClientAddress" placeholder="127.0.0.1">
|
||||||
<div class="input-group-text">
|
<div class="input-group-append">
|
||||||
<input id="enable-static" type="checkbox" onchange="document.getElementById('staticAddress').disabled=!this.checked;" v-if="serverRole == 'master'" v-bind:checked="customAddressDisabled">
|
<button id="static-address-clear" class="btn btn-warning" type="button" v-on:click="u.ccd.ClientAddress = 'dynamic'" v-if="serverRole == 'master'" v-bind:disabled="customAddressDynamic">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input id="staticAddress" type="text" class="form-control" v-model="u.ccd.ClientAddress" placeholder="127.0.0.1" v-bind:disabled="customAddressDisabled">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="d-flex ">
|
<div class="d-flex ">
|
||||||
<table class="table table-bordered table-hover">
|
<table class="table table-bordered table-hover ccd-routes" >
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Address</th>
|
<th scope="col">Address</th>
|
||||||
<th scope="col">Mask</th>
|
<th scope="col">Mask</th>
|
||||||
<th scope="col">Description</th>
|
<th scope="col">Description</th>
|
||||||
<th scope="col">Action</th>
|
<th scope="col" v-if="serverRole == 'master'">Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(customRoute, index) in u.ccd.CustomRoutes">
|
<tr v-for="(customRoute, index) in u.ccd.CustomRoutes">
|
||||||
<td>{{ customRoute.Address }}</td>
|
|
||||||
<td>{{ customRoute.Mask }}</td>
|
|
||||||
<td>{{ customRoute.Description }}</td>
|
|
||||||
<td>
|
<td>
|
||||||
<button type="button" class="btn btn-primary btn-sm el-square modal-el-margin" v-if="serverRole == 'master'" 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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="serverRole == 'master'">
|
<tr v-if="serverRole == 'master'">
|
||||||
<td><input type="text" v-model="u.newRoute.Address"/></td>
|
<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.Mask"/></td>
|
||||||
<td><input type="text" v-model="u.newRoute.Description"/></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>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -152,7 +185,52 @@
|
||||||
</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>
|
||||||
<script src="dist/build.js"></script>
|
<div class="modal-body" v-if="modulesEnabled.includes('passwdAuth')">
|
||||||
|
<h4>Enter new password:</h4>
|
||||||
|
<input type="password" class="form-control el-square modal-el-margin" minlength="6" autocomplete="off" placeholder="Password [_a-zA-Z0-9\.-]" v-model="u.newPassword">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer justify-content-center" v-if="u.rotateUserMessage.length > 0">
|
||||||
|
<div class="alert" v-bind:class="userRotateStatusCssClass" role="alert" >
|
||||||
|
{{ u.rotateUserMessage }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-danger el-square modal-el-margin" v-on:click.stop="rotateUser(username)">Rotate</button>
|
||||||
|
<button type="button" class="btn btn-primary el-square d-flex justify-content-sm-end modal-el-margin" v-on:click.stop="u.newPassword='';u.rotateUserMessage='';u.modalRotateUserVisible=false">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-wrapper" v-if="u.modalDeleteUserVisible" v-bind:style="modalDeleteUserDisplay">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4>Confirm deleting user: <strong>{{ username }}</strong></h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer justify-content-center" v-if="u.deleteUserMessage.length > 0">
|
||||||
|
<div class="alert" v-bind:class="deleteUserStatusCssClass" role="alert" >
|
||||||
|
{{ u.deleteUserMessage }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-danger el-square modal-el-margin" v-on:click.stop="deleteUser(username)">Delete</button>
|
||||||
|
<button type="button" class="btn btn-primary el-square d-flex justify-content-sm-end modal-el-margin" v-on:click.stop="u.deleteUserMessage='';u.modalDeleteUserVisible=false">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<notifications position="bottom left" :speed="900" />
|
||||||
|
</div>
|
||||||
|
<script src="dist/bundle.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,14 +1,24 @@
|
||||||
var path = require('path')
|
const path = require('path');
|
||||||
var webpack = require('webpack')
|
//const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||||
const TerserPlugin = require('terser-webpack-plugin');
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: './src/main.js',
|
mode: 'production',
|
||||||
|
entry: {
|
||||||
|
bundle: [
|
||||||
|
'./src/main.js',
|
||||||
|
],
|
||||||
|
style: [
|
||||||
|
'./src/style.js',
|
||||||
|
]
|
||||||
|
},
|
||||||
output: {
|
output: {
|
||||||
path: path.resolve(__dirname, './static/dist'),
|
path: path.resolve(__dirname, './static/dist'),
|
||||||
publicPath: '/dist/',
|
publicPath: '/dist/',
|
||||||
filename: 'build.js'
|
filename: '[name].min.js'
|
||||||
},
|
},
|
||||||
|
plugins: [
|
||||||
|
//new BundleAnalyzerPlugin(),
|
||||||
|
],
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
|
@ -18,89 +28,23 @@ module.exports = {
|
||||||
'css-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$/,
|
test: /\.js$/,
|
||||||
|
//exclude: /node_modules\/(?!bootstrap-vue\/src\/)/,
|
||||||
|
exclude: /node_modules/,
|
||||||
loader: 'babel-loader',
|
loader: 'babel-loader',
|
||||||
exclude: /node_modules/
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(png|jpg|gif|svg)$/,
|
|
||||||
loader: 'file-loader',
|
|
||||||
options: {
|
options: {
|
||||||
name: '[name].[ext]?[hash]'
|
presets: ['@babel/preset-env']
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'vue$': 'vue/dist/vue.esm.js'
|
'vue$': 'vue/dist/vue.esm.js',
|
||||||
|
//'bootstrap-vue$': 'bootstrap-vue/src/index.js'
|
||||||
},
|
},
|
||||||
extensions: ['*', '.js', '.vue', '.json']
|
extensions: ['*', '.js', '.vue', '.json']
|
||||||
},
|
},
|
||||||
devServer: {
|
|
||||||
historyApiFallback: true,
|
|
||||||
noInfo: true,
|
|
||||||
overlay: true
|
|
||||||
},
|
|
||||||
performance: {
|
|
||||||
hints: false
|
|
||||||
},
|
|
||||||
devtool: '#eval-source-map'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
})
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
63
go.mod
63
go.mod
|
@ -1,12 +1,61 @@
|
||||||
module openvpn-web-ui
|
module ovpn-admin
|
||||||
|
|
||||||
go 1.14
|
go 1.17
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Electronn/openvpn_exporter v0.0.0-20181005212047-37f639dc9c7d
|
github.com/gobuffalo/packr/v2 v2.8.3
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
github.com/prometheus/client_golang v1.11.0
|
||||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
|
|
||||||
github.com/prometheus/client_golang v1.8.0
|
|
||||||
github.com/prometheus/common v0.15.0 // indirect
|
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||||
|
k8s.io/apimachinery v0.23.1
|
||||||
|
k8s.io/client-go v0.23.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||||
|
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
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
|
||||||
)
|
)
|
||||||
|
|
2
helm/Chart.yaml
Normal file
2
helm/Chart.yaml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
name: ovpn-admin
|
||||||
|
version: 1.0.0
|
1
helm/README.md
Normal file
1
helm/README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
helm chart example
|
88
helm/templates/configmap.yaml
Normal file
88
helm/templates/configmap.yaml
Normal 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
|
117
helm/templates/deployment.yaml
Normal file
117
helm/templates/deployment.yaml
Normal 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
|
39
helm/templates/ingress.yaml
Normal file
39
helm/templates/ingress.yaml
Normal 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
36
helm/templates/rbac.yaml
Normal 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
|
8
helm/templates/secret.yaml
Normal file
8
helm/templates/secret.yaml
Normal 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 }}
|
57
helm/templates/service.yaml
Normal file
57
helm/templates/service.yaml
Normal 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
26
helm/values.yaml
Normal 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
|
231
helpers.go
231
helpers.go
|
@ -1,30 +1,43 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/gzip"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func indexTxtDateToHumanReadable(datetime string) string {
|
func parseDate(layout, datetime string) time.Time {
|
||||||
layout := "060102150405Z"
|
|
||||||
t, err := time.Parse(layout, datetime)
|
t, err := time.Parse(layout, datetime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Errorln(err)
|
||||||
}
|
}
|
||||||
return t.Format("2006-01-02 15:04:05")
|
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 {
|
func runBash(script string) string {
|
||||||
fmt.Println(script)
|
log.Debugln(script)
|
||||||
cmd := exec.Command("bash", "-c", script)
|
cmd := exec.Command("bash", "-c", script)
|
||||||
stdout, err := cmd.CombinedOutput()
|
stdout, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return (fmt.Sprint(err) + " : " + string(stdout))
|
return fmt.Sprint(err) + " : " + string(stdout)
|
||||||
}
|
}
|
||||||
return string(stdout)
|
return string(stdout)
|
||||||
}
|
}
|
||||||
|
@ -35,7 +48,7 @@ func fExist(path string) bool {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return false
|
return false
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatalf("fExist: %s", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,37 +58,102 @@ func fExist(path string) bool {
|
||||||
func fRead(path string) string {
|
func fRead(path string) string {
|
||||||
content, err := ioutil.ReadFile(path)
|
content, err := ioutil.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Warning(err)
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(content)
|
return string(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fCreate(path string) bool {
|
func fCreate(path string) error {
|
||||||
var _, err = os.Stat(path)
|
var _, err = os.Stat(path)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
var file, err = os.Create(path)
|
var file, err = os.Create(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Errorln(err)
|
||||||
return false
|
return err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
}
|
}
|
||||||
return true
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fWrite(path, content string) {
|
func fWrite(path, content string) error {
|
||||||
err := ioutil.WriteFile(path, []byte(content), 0644)
|
err := ioutil.WriteFile(path, []byte(content), 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fDelete(path string) {
|
func fDelete(path string) error {
|
||||||
err := os.Remove(path)
|
err := os.Remove(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fCopy(src, dst string) error {
|
||||||
|
sfi, err := os.Stat(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !sfi.Mode().IsRegular() {
|
||||||
|
// cannot copy non-regular files (e.g., directories, symlinks, devices, etc.)
|
||||||
|
return fmt.Errorf("fCopy: non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String())
|
||||||
|
}
|
||||||
|
dfi, err := os.Stat(dst)
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !(dfi.Mode().IsRegular()) {
|
||||||
|
return fmt.Errorf("fCopy: non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String())
|
||||||
|
}
|
||||||
|
if os.SameFile(sfi, dfi) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = os.Link(src, dst); err == nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
in, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
out, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
cerr := out.Close()
|
||||||
|
if err == nil {
|
||||||
|
err = cerr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if _, err = io.Copy(out, in); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = out.Sync()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func fMove(src, dst string) error {
|
||||||
|
err := fCopy(src, dst)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = fDelete(src)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fDownload(path, url string, basicAuth bool) error {
|
func fDownload(path, url string, basicAuth bool) error {
|
||||||
|
@ -91,7 +169,7 @@ func fDownload(path, url string, basicAuth bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
log.Printf("WARNING: Download file operation for url %s finished with status code %d", url, resp.StatusCode )
|
log.Warnf("WARNING: Download file operation for url %s finished with status code %d\n", url, resp.StatusCode)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
@ -105,3 +183,124 @@ func fDownload(path, url string, basicAuth bool) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createArchiveFromDir(dir, path string) error {
|
||||||
|
|
||||||
|
var files []string
|
||||||
|
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
log.Warn(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
files = append(files, path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Warn(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error writing archive %s: %s", path, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
gw := gzip.NewWriter(out)
|
||||||
|
defer gw.Close()
|
||||||
|
tw := tar.NewWriter(gw)
|
||||||
|
defer tw.Close()
|
||||||
|
|
||||||
|
// Iterate over files and add them to the tar archive
|
||||||
|
for _, filePath := range files {
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Error writing archive %s: %s", path, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get FileInfo about our file providing file size, mode, etc.
|
||||||
|
info, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
file.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a tar Header from the FileInfo data
|
||||||
|
header, err := tar.FileInfoHeader(info, info.Name())
|
||||||
|
if err != nil {
|
||||||
|
file.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
header.Name = strings.Replace(filePath, dir+"/", "", 1)
|
||||||
|
|
||||||
|
// Write file header to the tar archive
|
||||||
|
err = tw.WriteHeader(header)
|
||||||
|
if err != nil {
|
||||||
|
file.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy file content to tar archive
|
||||||
|
_, err = io.Copy(tw, file)
|
||||||
|
if err != nil {
|
||||||
|
file.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractFromArchive(archive, path string) error {
|
||||||
|
// Open the file which will be written into the archive
|
||||||
|
file, err := os.Open(archive)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Write file header to the tar archive
|
||||||
|
uncompressedStream, err := gzip.NewReader(file)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("extractFromArchive(): NewReader failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
tarReader := tar.NewReader(uncompressedStream)
|
||||||
|
|
||||||
|
for true {
|
||||||
|
header, err := tarReader.Next()
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("extractFromArchive: Next() failed: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
switch header.Typeflag {
|
||||||
|
case tar.TypeDir:
|
||||||
|
if err := os.Mkdir(path+"/"+header.Name, 0755); err != nil {
|
||||||
|
log.Fatalf("extractFromArchive: Mkdir() failed: %s", err.Error())
|
||||||
|
}
|
||||||
|
case tar.TypeReg:
|
||||||
|
outFile, err := os.Create(path + "/" + header.Name)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("extractFromArchive: Create() failed: %s", err.Error())
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(outFile, tarReader); err != nil {
|
||||||
|
log.Fatalf("extractFromArchive: Copy() failed: %s", err.Error())
|
||||||
|
}
|
||||||
|
outFile.Close()
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Fatalf(
|
||||||
|
"extractFromArchive: uknown type: %s in %s", header.Typeflag, header.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
BIN
img/ovpn-admin-metrics.png
Normal file
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
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
12
install-deps-arm.sh
Executable 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
12
install-deps.sh
Executable 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
776
kubernetes.go
Normal 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
|
||||||
|
}
|
16
setup/auth.sh
Normal file
16
setup/auth.sh
Normal 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
|
|
@ -1,13 +1,19 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -x
|
set -ex
|
||||||
|
|
||||||
EASY_RSA_LOC="/etc/openvpn/easyrsa"
|
EASY_RSA_LOC="/etc/openvpn/easyrsa"
|
||||||
SERVER_CERT="${EASY_RSA_LOC}/pki/issued/server.crt"
|
SERVER_CERT="${EASY_RSA_LOC}/pki/issued/server.crt"
|
||||||
|
|
||||||
|
OVPN_SRV_NET=${OVPN_SERVER_NET:-172.16.100.0}
|
||||||
|
OVPN_SRV_MASK=${OVPN_SERVER_MASK:-255.255.255.0}
|
||||||
|
|
||||||
|
|
||||||
cd $EASY_RSA_LOC
|
cd $EASY_RSA_LOC
|
||||||
|
|
||||||
if [ -e "$SERVER_CERT" ]; then
|
if [ -e "$SERVER_CERT" ]; then
|
||||||
echo "Found existing certs - reusing"
|
echo "Found existing certs - reusing"
|
||||||
else
|
else
|
||||||
if [ ${OPVN_ROLE:-"master"} = "slave" ]; then
|
if [ ${OVPN_ROLE:-"master"} = "slave" ]; then
|
||||||
echo "Waiting for initial sync data from master"
|
echo "Waiting for initial sync data from master"
|
||||||
while [ $(wget -q localhost/api/sync/last/try -O - | wc -m) -lt 1 ]
|
while [ $(wget -q localhost/api/sync/last/try -O - | wc -m) -lt 1 ]
|
||||||
do
|
do
|
||||||
|
@ -25,7 +31,8 @@ else
|
||||||
fi
|
fi
|
||||||
easyrsa gen-crl
|
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
|
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
|
mkdir -p /dev/net
|
||||||
if [ ! -c /dev/net/tun ]; then
|
if [ ! -c /dev/net/tun ]; then
|
||||||
|
@ -34,10 +41,19 @@ fi
|
||||||
|
|
||||||
cp -f /etc/openvpn/setup/openvpn.conf /etc/openvpn/openvpn.conf
|
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
|
[ -d $EASY_RSA_LOC/pki ] && chmod 755 $EASY_RSA_LOC/pki
|
||||||
[ -f $EASY_RSA_LOC/pki/crl.pem ] && chmod 644 $EASY_RSA_LOC/pki/crl.pem
|
[ -f $EASY_RSA_LOC/pki/crl.pem ] && chmod 644 $EASY_RSA_LOC/pki/crl.pem
|
||||||
|
|
||||||
mkdir -p /etc/openvpn/ccd
|
mkdir -p /etc/openvpn/ccd
|
||||||
|
|
||||||
openvpn --config /etc/openvpn/openvpn.conf --client-config-dir /etc/openvpn/ccd
|
openvpn --config /etc/openvpn/openvpn.conf --client-config-dir /etc/openvpn/ccd --port 1194 --proto tcp --management 127.0.0.1 8989 --dev tun0 --server ${OVPN_SRV_NET} ${OVPN_SRV_MASK}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
server 172.16.100.0 255.255.255.0
|
# server 172.16.100.0 255.255.255.0
|
||||||
verb 3
|
verb 3
|
||||||
tls-server
|
tls-server
|
||||||
ca /etc/openvpn/easyrsa/pki/ca.crt
|
ca /etc/openvpn/easyrsa/pki/ca.crt
|
||||||
|
@ -8,16 +8,16 @@ dh /etc/openvpn/easyrsa/pki/dh.pem
|
||||||
crl-verify /etc/openvpn/easyrsa/pki/crl.pem
|
crl-verify /etc/openvpn/easyrsa/pki/crl.pem
|
||||||
tls-auth /etc/openvpn/easyrsa/pki/ta.key
|
tls-auth /etc/openvpn/easyrsa/pki/ta.key
|
||||||
key-direction 0
|
key-direction 0
|
||||||
duplicate-cn
|
|
||||||
cipher AES-128-CBC
|
cipher AES-128-CBC
|
||||||
management 127.0.0.1 8989
|
#management 127.0.0.1 8989
|
||||||
keepalive 10 60
|
keepalive 10 60
|
||||||
persist-key
|
persist-key
|
||||||
persist-tun
|
persist-tun
|
||||||
topology subnet
|
topology subnet
|
||||||
proto tcp
|
#duplicate-cn
|
||||||
port 1194
|
#proto tcp
|
||||||
dev tun0
|
#port 1194
|
||||||
|
#dev tun0
|
||||||
status /tmp/openvpn-status.log
|
status /tmp/openvpn-status.log
|
||||||
user nobody
|
user nobody
|
||||||
group nogroup
|
group nogroup
|
2
start.sh
2
start.sh
|
@ -1,3 +1,3 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
docker-compose -p openvpn-master up -d --build
|
docker compose -p openvpn-master up -d --build
|
||||||
|
|
6
templates/ccd.tpl
Normal file
6
templates/ccd.tpl
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{{- if (ne .ClientAddress "dynamic") }}
|
||||||
|
ifconfig-push {{ .ClientAddress }} 255.255.255.0
|
||||||
|
{{- end }}
|
||||||
|
{{- range $route := .CustomRoutes }}
|
||||||
|
push "route {{ $route.Address }} {{ $route.Mask }}" # {{ $route.Description }}
|
||||||
|
{{- end }}
|
38
templates/client.conf.tpl
Normal file
38
templates/client.conf.tpl
Normal 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
118
werf.yaml
|
@ -1,120 +1,10 @@
|
||||||
project: openvpn-web-ui
|
project: ovpn-admin
|
||||||
configVersion: 1
|
configVersion: 1
|
||||||
deploy:
|
|
||||||
helmRelease: "[[ project ]]-[[ env ]]"
|
|
||||||
namespace: "[[ project ]]-[[ env ]]"
|
|
||||||
|
|
||||||
---
|
---
|
||||||
artifact: backend-builder
|
image: ovpn-admin
|
||||||
from: golang:1.14.2-alpine3.11
|
dockerfile: Dockerfile
|
||||||
git:
|
|
||||||
- add: /
|
|
||||||
to: /app
|
|
||||||
stageDependencies:
|
|
||||||
install:
|
|
||||||
- "*.go"
|
|
||||||
excludePaths:
|
|
||||||
- .helm
|
|
||||||
- .werf
|
|
||||||
- frontend
|
|
||||||
- werf.yaml
|
|
||||||
- Dockerfile
|
|
||||||
ansible:
|
|
||||||
install:
|
|
||||||
- name: Install packages
|
|
||||||
apk:
|
|
||||||
name:
|
|
||||||
- build-base
|
|
||||||
- gcc
|
|
||||||
- name: Build backend
|
|
||||||
command: go build -ldflags='-extldflags "-static" -s -w' -o openvpn-admin
|
|
||||||
environment:
|
|
||||||
CGO_ENABLED: 0
|
|
||||||
GOOS: linux
|
|
||||||
GOARCH: amd64
|
|
||||||
args:
|
|
||||||
chdir: /app
|
|
||||||
|
|
||||||
---
|
|
||||||
artifact: frontend-builder
|
|
||||||
from: node:14.2-alpine3.11
|
|
||||||
git:
|
|
||||||
- add: /frontend
|
|
||||||
to: /app
|
|
||||||
stageDependencies:
|
|
||||||
install:
|
|
||||||
- "**/*"
|
|
||||||
excludePaths:
|
|
||||||
- Dockerfile
|
|
||||||
- build.sh
|
|
||||||
- werf.yaml
|
|
||||||
ansible:
|
|
||||||
setup:
|
|
||||||
- name: install deps
|
|
||||||
command: npm install
|
|
||||||
args:
|
|
||||||
chdir: /app
|
|
||||||
- name: Build app
|
|
||||||
command: npm run build
|
|
||||||
args:
|
|
||||||
chdir: /app
|
|
||||||
|
|
||||||
---
|
|
||||||
image: openvpn-admin
|
|
||||||
from: alpine:3.11
|
|
||||||
import:
|
|
||||||
- artifact: backend-builder
|
|
||||||
add: /app/openvpn-admin
|
|
||||||
to: /usr/bin/openvpn-admin
|
|
||||||
before: setup
|
|
||||||
- artifact: frontend-builder
|
|
||||||
add: /app/static
|
|
||||||
to: /app/static
|
|
||||||
before: setup
|
|
||||||
git:
|
|
||||||
- add: /client.conf.tpl
|
|
||||||
to: /app/client.conf.tpl
|
|
||||||
stageDependencies:
|
|
||||||
setup:
|
|
||||||
- "*"
|
|
||||||
- add: /ccd.tpl
|
|
||||||
to: /app/ccd.tpl
|
|
||||||
stageDependencies:
|
|
||||||
setup:
|
|
||||||
- "*"
|
|
||||||
ansible:
|
|
||||||
install:
|
|
||||||
- name: Install packages
|
|
||||||
apk:
|
|
||||||
name:
|
|
||||||
- easy-rsa
|
|
||||||
- bash
|
|
||||||
- name: Create symbolic link for easy-rsa
|
|
||||||
file:
|
|
||||||
src: "/usr/share/easy-rsa/easyrsa"
|
|
||||||
dest: "/usr/local/bin/easyrsa"
|
|
||||||
state: link
|
|
||||||
|
|
||||||
---
|
---
|
||||||
image: openvpn
|
image: openvpn
|
||||||
from: alpine:3.11
|
dockerfile: Dockerfile.openvpn
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue