Compare commits
114 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 | ||
|
bf37066475 | ||
|
9f8098ab03 | ||
|
f3a7f1f869 |
57 changed files with 9230 additions and 6852 deletions
|
@ -3,12 +3,24 @@
|
|||
*.iml
|
||||
out
|
||||
gen
|
||||
|
||||
.github
|
||||
|
||||
easyrsa
|
||||
easyrsa_master
|
||||
easyrsa_slave
|
||||
ccd
|
||||
ccd_master
|
||||
ccd_slave
|
||||
werf.yaml
|
||||
frontend/node_modules
|
||||
frontend/static/dist
|
||||
openvpn-web-ui
|
||||
openvpn-ui
|
||||
openvpn-admin
|
||||
ovpn-admin
|
||||
|
||||
docker-compose.yaml
|
||||
docker-compose-slave.yaml
|
||||
img
|
||||
dashboard
|
||||
.helm
|
||||
|
|
24
.editorconfig
Normal file
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 }}
|
23
.gitignore
vendored
23
.gitignore
vendored
|
@ -1,6 +1,21 @@
|
|||
easyrsa
|
||||
easyrsa_master
|
||||
easyrsa_slave
|
||||
ccd
|
||||
openvpn-web-ui
|
||||
openvpn-ui
|
||||
openvpn-admin
|
||||
frontend/node_modules
|
||||
ccd_master
|
||||
ccd_slave
|
||||
openvpn-web-ui*
|
||||
openvpn-ui*
|
||||
openvpn-admin*
|
||||
ovpn-admin*
|
||||
frontend/node_modules
|
||||
|
||||
main-packr.go
|
||||
packrd/
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -x
|
||||
EASY_RSA_LOC="/etc/openvpn/easyrsa"
|
||||
SERVER_CERT="${EASY_RSA_LOC}/pki/issued/server.crt"
|
||||
cd $EASY_RSA_LOC
|
||||
if [ -e "$SERVER_CERT" ]; then
|
||||
echo "found existing certs - reusing"
|
||||
else
|
||||
easyrsa init-pki
|
||||
cp -R /usr/share/easy-rsa/* $EASY_RSA_LOC/pki
|
||||
echo "ca" | easyrsa build-ca nopass
|
||||
easyrsa build-server-full server nopass
|
||||
easyrsa gen-dh
|
||||
openvpn --genkey --secret ./pki/ta.key
|
||||
fi
|
||||
easyrsa gen-crl
|
||||
|
||||
iptables -t nat -A POSTROUTING -s 172.16.100.0/255.255.255.0 ! -d 172.16.100.0/255.255.255.0 -j MASQUERADE
|
||||
|
||||
mkdir -p /dev/net
|
||||
if [ ! -c /dev/net/tun ]; then
|
||||
mknod /dev/net/tun c 10 200
|
||||
fi
|
||||
|
||||
cp -f /etc/openvpn/setup/openvpn.conf /etc/openvpn/openvpn.conf
|
||||
|
||||
[ -d $EASY_RSA_LOC/pki ] && chmod 755 $EASY_RSA_LOC/pki
|
||||
[ -f $EASY_RSA_LOC/pki/crl.pem ] && chmod 644 $EASY_RSA_LOC/pki/crl.pem
|
||||
|
||||
mkdir -p /etc/openvpn/ccd
|
||||
|
||||
openvpn --config /etc/openvpn/openvpn.conf --client-config-dir /etc/openvpn/ccd
|
||||
|
29
Dockerfile
29
Dockerfile
|
@ -1,19 +1,20 @@
|
|||
FROM golang:1.14.2-alpine3.11 AS backend-builder
|
||||
COPY . /app
|
||||
#RUN apk --no-cache add build-base git gcc
|
||||
RUN cd /app && env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags='-extldflags "-static" -s -w' -o openvpn-admin
|
||||
|
||||
FROM node:14.2-alpine3.11 AS frontend-builder
|
||||
FROM node:16-alpine3.15 AS frontend-builder
|
||||
COPY frontend/ /app
|
||||
RUN cd /app && npm install && npm run build
|
||||
RUN apk add --update python3 make g++ && cd /app && npm install && npm run build
|
||||
|
||||
FROM alpine:3.11
|
||||
FROM golang:1.17.3-buster AS backend-builder
|
||||
RUN go install github.com/gobuffalo/packr/v2/packr2@latest
|
||||
COPY --from=frontend-builder /app/static /app/frontend/static
|
||||
COPY . /app
|
||||
ARG TARGETARCH
|
||||
RUN cd /app && packr2 && env CGO_ENABLED=1 GOOS=linux GOARCH=${TARGETARCH} go build -a -tags netgo -ldflags '-linkmode external -extldflags -static -s -w' -o ovpn-admin && packr2 clean
|
||||
|
||||
FROM alpine:3.16
|
||||
WORKDIR /app
|
||||
COPY --from=backend-builder /app/openvpn-admin /app
|
||||
COPY --from=frontend-builder /app/static /app/static
|
||||
COPY client.conf.tpl /app/client.conf.tpl
|
||||
COPY ccd.tpl /app/ccd.tpl
|
||||
RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \
|
||||
apk add --update bash easy-rsa && \
|
||||
COPY --from=backend-builder /app/ovpn-admin /app
|
||||
ARG TARGETARCH
|
||||
RUN apk add --update bash easy-rsa openssl openvpn coreutils && \
|
||||
ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \
|
||||
wget https://github.com/pashcovich/openvpn-user/releases/download/v1.0.4/openvpn-user-linux-${TARGETARCH}.tar.gz -O - | tar xz -C /usr/local/bin && \
|
||||
rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*
|
||||
RUN if [ -f "/usr/local/bin/openvpn-user-${TARGETARCH}" ]; then ln -s /usr/local/bin/openvpn-user-${TARGETARCH} /usr/local/bin/openvpn-user; fi
|
|
@ -1,7 +1,9 @@
|
|||
FROM alpine:3.11
|
||||
RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \
|
||||
apk add --update bash openvpn easy-rsa && \
|
||||
FROM alpine:3.16
|
||||
ARG TARGETARCH
|
||||
RUN apk add --update bash openvpn easy-rsa iptables && \
|
||||
ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \
|
||||
wget https://github.com/pashcovich/openvpn-user/releases/download/v1.0.4/openvpn-user-linux-${TARGETARCH}.tar.gz -O - | tar xz -C /usr/local/bin && \
|
||||
rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*
|
||||
COPY .werffiles /etc/openvpn/setup
|
||||
RUN chmod +x /etc/openvpn/setup/configure.sh
|
||||
RUN if [ -f "/usr/local/bin/openvpn-user-${TARGETARCH}" ]; then ln -s /usr/local/bin/openvpn-user-${TARGETARCH} /usr/local/bin/openvpn-user; fi
|
||||
COPY setup/ /etc/openvpn/setup
|
||||
RUN chmod +x /etc/openvpn/setup/configure.sh
|
178
README.md
178
README.md
|
@ -1 +1,177 @@
|
|||
# openvpn-web-ui
|
||||
# ovpn-admin
|
||||
|
||||
Simple web UI to manage OpenVPN users, their certificates & routes in Linux. While backend is written in Go, frontend is based on Vue.js.
|
||||
|
||||
Originally created in [Flant](https://flant.com/) for internal needs & used for years, then updated to be more modern and [publicly released](https://medium.com/flant-com/introducing-ovpn-admin-a-web-interface-to-manage-openvpn-users-d81705ad8f23) in March'21. Please note that the project is currently on pause, no new Issues or PRs are accepted.
|
||||
|
||||
***DISCLAIMER!** This project was created for experienced users (system administrators) and private (e.g., protected by network policies) environments only. Thus, it is not implemented with security in mind (e.g., it doesn't strictly check all parameters passed by users, etc.). It also relies heavily on files and fails if required files aren't available.*
|
||||
|
||||
## Features
|
||||
|
||||
* Adding, deleting OpenVPN users (generating certificates for them);
|
||||
* Revoking/restoring/rotating users certificates;
|
||||
* Generating ready-to-user config files;
|
||||
* Providing metrics for Prometheus, including certificates expiration date, number of (connected/total) users, information about connected users;
|
||||
* (optionally) Specifying CCD (`client-config-dir`) for each user;
|
||||
* (optionally) Operating in a master/slave mode (syncing certs & CCD with other server);
|
||||
* (optionally) Specifying/changing password for additional authorization in OpenVPN;
|
||||
* (optionally) Specifying the Kubernetes LoadBalancer if it's used in front of the OpenVPN server (to get an automatically defined `remote` in the `client.conf.tpl` template).
|
||||
* (optionally) Storing certificates and other files in Kubernetes Secrets (**Attention, this feature is experimental!**).
|
||||
|
||||
### Screenshots
|
||||
|
||||
Managing users in ovpn-admin:
|
||||
![ovpn-admin UI](https://raw.githubusercontent.com/flant/ovpn-admin/master/img/ovpn-admin-users.png)
|
||||
|
||||
An example of dashboard made using ovpn-admin metrics:
|
||||
![ovpn-admin metrics](https://raw.githubusercontent.com/flant/ovpn-admin/master/img/ovpn-admin-metrics.png)
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Docker
|
||||
|
||||
There is a ready-to-use [docker-compose.yaml](https://github.com/flant/ovpn-admin/blob/master/docker-compose.yaml), so you can just change/add values you need and start it with [start.sh](https://github.com/flant/ovpn-admin/blob/master/start.sh).
|
||||
|
||||
Requirements:
|
||||
You need [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/) installed.
|
||||
|
||||
Commands to execute:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/flant/ovpn-admin.git
|
||||
cd ovpn-admin
|
||||
./start.sh
|
||||
```
|
||||
#### 1.1
|
||||
Ready docker images available on [Docker Hub](https://hub.docker.com/r/flant/ovpn-admin/tags)
|
||||
. Tags are simple: `$VERSION` or `latest` for ovpn-admin and `openvpn-$VERSION` or `openvpn-latest` for openvpn-server
|
||||
|
||||
### 2. Building from source
|
||||
|
||||
Requirements. You need Linux with the following components installed:
|
||||
- [golang](https://golang.org/doc/install)
|
||||
- [packr2](https://github.com/gobuffalo/packr#installation)
|
||||
- [nodejs/npm](https://nodejs.org/en/download/package-manager/)
|
||||
|
||||
Commands to execute:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/flant/ovpn-admin.git
|
||||
cd ovpn-admin
|
||||
./bootstrap.sh
|
||||
./build.sh
|
||||
./ovpn-admin
|
||||
```
|
||||
|
||||
(Please don't forget to configure all needed params in advance.)
|
||||
|
||||
### 3. Prebuilt binary
|
||||
|
||||
You can also download and use prebuilt binaries from the [releases](https://github.com/flant/ovpn-admin/releases/latest) page — just choose a relevant tar.gz file.
|
||||
|
||||
|
||||
## Notes
|
||||
* this tool uses external calls for `bash`, `coreutils` and `easy-rsa`, thus **Linux systems only are supported** at the moment.
|
||||
* to enable additional password authentication provide `--auth` and `--auth.db="/etc/easyrsa/pki/users.db`" flags and install [openvpn-user](https://github.com/pashcovich/openvpn-user/releases/latest). This tool should be available in your `$PATH` and its binary should be executable (`+x`).
|
||||
* master-replica synchronization does not work with `--storage.backend=kubernetes.secrets` - **WIP**
|
||||
* additional password authentication does not work with `--storage.backend=kubernetes.secrets` - **WIP**
|
||||
* if you use `--ccd` and `--ccd.path="/etc/openvpn/ccd"` abd plan to use static address setup for users do not forget to provide `--ovpn.network="172.16.100.0/24"` with valid openvpn-server network
|
||||
* tested only with Openvpn-server versions 2.4 and 2.5 with only tls-auth mode
|
||||
* not tested with EasyRsa version > 3.0.8
|
||||
* status of users connections update every 28 second(*no need to ask why =)*)
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
usage: ovpn-admin [<flags>]
|
||||
|
||||
Flags:
|
||||
--help show context-sensitive help (try also --help-long and --help-man)
|
||||
|
||||
--listen.host="0.0.0.0" host for ovpn-admin
|
||||
(or OVPN_LISTEN_HOST)
|
||||
|
||||
--listen.port="8080" port for ovpn-admin
|
||||
(or OVPN_LISTEN_PORT)
|
||||
|
||||
--listen.base-url="/" base URL for ovpn-admin web files
|
||||
(or $OVPN_LISTEN_BASE_URL)
|
||||
|
||||
--role="master" server role, master or slave
|
||||
(or OVPN_ROLE)
|
||||
|
||||
--master.host="http://127.0.0.1"
|
||||
(or OVPN_MASTER_HOST) URL for the master server
|
||||
|
||||
--master.basic-auth.user="" user for master server's Basic Auth
|
||||
(or OVPN_MASTER_USER)
|
||||
|
||||
--master.basic-auth.password=""
|
||||
(or OVPN_MASTER_PASSWORD) password for master server's Basic Auth
|
||||
|
||||
--master.sync-frequency=600 master host data sync frequency in seconds
|
||||
(or OVPN_MASTER_SYNC_FREQUENCY)
|
||||
|
||||
--master.sync-token=TOKEN master host data sync security token
|
||||
(or OVPN_MASTER_TOKEN)
|
||||
|
||||
--ovpn.network="172.16.100.0/24"
|
||||
(or OVPN_NETWORK) NETWORK/MASK_PREFIX for OpenVPN server
|
||||
|
||||
--ovpn.server=HOST:PORT:PROTOCOL ...
|
||||
(or OVPN_SERVER) HOST:PORT:PROTOCOL for OpenVPN server
|
||||
can have multiple values
|
||||
|
||||
--ovpn.server.behindLB enable if your OpenVPN server is behind Kubernetes
|
||||
(or OVPN_LB) Service having the LoadBalancer type
|
||||
|
||||
--ovpn.service="openvpn-external"
|
||||
(or OVPN_LB_SERVICE) the name of Kubernetes Service having the LoadBalancer
|
||||
type if your OpenVPN server is behind it
|
||||
|
||||
--mgmt=main=127.0.0.1:8989 ...
|
||||
(or OVPN_MGMT) ALIAS=HOST:PORT for OpenVPN server mgmt interface;
|
||||
can have multiple values
|
||||
|
||||
--metrics.path="/metrics" URL path for exposing collected metrics
|
||||
(or OVPN_METRICS_PATH)
|
||||
|
||||
--easyrsa.path="./easyrsa/" path to easyrsa dir
|
||||
(or EASYRSA_PATH)
|
||||
|
||||
--easyrsa.index-path="./easyrsa/pki/index.txt"
|
||||
(or OVPN_INDEX_PATH) path to easyrsa index file
|
||||
|
||||
--ccd enable client-config-dir
|
||||
(or OVPN_CCD)
|
||||
|
||||
--ccd.path="./ccd" path to client-config-dir
|
||||
(or OVPN_CCD_PATH)
|
||||
|
||||
--templates.clientconfig-path=""
|
||||
(or OVPN_TEMPLATES_CC_PATH) path to custom client.conf.tpl
|
||||
|
||||
--templates.ccd-path="" path to custom ccd.tpl
|
||||
(or OVPN_TEMPLATES_CCD_PATH)
|
||||
|
||||
--auth.password enable additional password authorization
|
||||
(or OVPN_AUTH)
|
||||
|
||||
--auth.db="./easyrsa/pki/users.db"
|
||||
(or OVPN_AUTH_DB_PATH) database path for password authorization
|
||||
|
||||
--log.level set log level: trace, debug, info, warn, error (default info)
|
||||
(or LOG_LEVEL)
|
||||
|
||||
--log.format set log format: text, json (default text)
|
||||
(or LOG_FORMAT)
|
||||
|
||||
--storage.backend storage backend: filesystem, kubernetes.secrets (default filesystem)
|
||||
(or STORAGE_BACKEND)
|
||||
|
||||
--version show application version
|
||||
```
|
||||
|
||||
## Further information
|
||||
|
||||
Please feel free to use [issues](https://github.com/flant/ovpn-admin/issues) and [discussions](https://github.com/flant/ovpn-admin/discussions) to get help from maintainers & community.
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [ ! -d easyrsa ]; then
|
||||
mkdir easyrsa
|
||||
fi
|
||||
mkdir -p {easyrsa,ccd}
|
||||
|
||||
cd easyrsa
|
||||
|
||||
|
@ -15,6 +13,6 @@ if [ -d pki ]; then
|
|||
fi
|
||||
|
||||
./easyrsa init-pki
|
||||
echo "ca\n" | ./easyrsa build-ca nopass
|
||||
echo "ca" | ./easyrsa build-ca nopass
|
||||
./easyrsa build-server-full server nopass
|
||||
./easyrsa build-client-full client nopass
|
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
|
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,26 +0,0 @@
|
|||
remote {{ .Host }} {{ .Port }} tcp
|
||||
verb 4
|
||||
client
|
||||
nobind
|
||||
dev tun
|
||||
cipher AES-128-CBC
|
||||
key-direction 1
|
||||
#redirect-gateway def1
|
||||
tls-client
|
||||
remote-cert-tls server
|
||||
# for update resolv.conf on ubuntu
|
||||
#script-security 2 system
|
||||
#up /etc/openvpn/update-resolv-conf
|
||||
#down /etc/openvpn/update-resolv-conf
|
||||
<cert>
|
||||
{{ .Cert -}}
|
||||
</cert>
|
||||
<key>
|
||||
{{ .Key -}}
|
||||
</key>
|
||||
<ca>
|
||||
{{ .CA -}}
|
||||
</ca>
|
||||
<tls-auth>
|
||||
{{ .TLS -}}
|
||||
</tls-auth>
|
974
dashboard/ovpn-admin.json
Normal file
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": ""
|
||||
}
|
30
docker-compose-slave.yaml
Normal file
30
docker-compose-slave.yaml
Normal file
|
@ -0,0 +1,30 @@
|
|||
version: '3'
|
||||
|
||||
services:
|
||||
openvpn:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.openvpn
|
||||
image: openvpn:local
|
||||
command: /etc/openvpn/setup/configure.sh
|
||||
environment:
|
||||
- OVPN_ROLE=slave
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
ports:
|
||||
- 7778:1194 # for openvpn
|
||||
- 8081:8080 # for ovpn-admin because of network_mode
|
||||
volumes:
|
||||
- ./easyrsa_slave:/etc/openvpn/easyrsa
|
||||
- ./ccd_slave:/etc/openvpn/ccd
|
||||
ovpn-admin:
|
||||
build:
|
||||
context: .
|
||||
image: ovpn-admin:local
|
||||
command: /app/ovpn-admin --debug --ovpn.network="172.16.100.0/22" --master.sync-token="TOKEN" --master.host="http://172.20.0.1:8080" --role="slave" --ovpn.server="127.0.0.1:7777:tcp" --ovpn.server="127.0.0.1:7778:tcp" --easyrsa.path="/mnt/easyrsa" --easyrsa.index-path="/mnt/easyrsa/pki/index.txt"
|
||||
environment:
|
||||
- OVPN_SLAVE=1
|
||||
network_mode: service:openvpn
|
||||
volumes:
|
||||
- ./easyrsa_slave:/mnt/easyrsa
|
||||
- ./ccd_slave:/mnt/ccd
|
|
@ -7,20 +7,36 @@ services:
|
|||
dockerfile: Dockerfile.openvpn
|
||||
image: openvpn:local
|
||||
command: /etc/openvpn/setup/configure.sh
|
||||
environment:
|
||||
OVPN_SERVER_NET: "192.168.100.0"
|
||||
OVPN_SERVER_MASK: "255.255.255.0"
|
||||
OVPN_PASSWD_AUTH: "true"
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
ports:
|
||||
- 7777:1194
|
||||
- 7777:1194 # for openvpn
|
||||
- 8080:8080 # for ovpn-admin because of network_mode
|
||||
volumes:
|
||||
- ./easyrsa:/etc/openvpn/easyrsa
|
||||
- ./ccd:/etc/openvpn/ccd
|
||||
openvpn-admin:
|
||||
- ./easyrsa_master:/etc/openvpn/easyrsa
|
||||
- ./ccd_master:/etc/openvpn/ccd
|
||||
ovpn-admin:
|
||||
build:
|
||||
context: .
|
||||
image: openvpn-admin:local
|
||||
command: /app/openvpn-admin --debug --ovpn.network="172.16.100.0/22" --mgmt.host="openvpn"
|
||||
ports:
|
||||
- 8080:8080
|
||||
image: ovpn-admin:local
|
||||
command: /app/ovpn-admin
|
||||
environment:
|
||||
OVPN_DEBUG: "true"
|
||||
OVPN_VERBOSE: "true"
|
||||
OVPN_NETWORK: "192.168.100.0/24"
|
||||
OVPN_CCD: "true"
|
||||
OVPN_CCD_PATH: "/mnt/ccd"
|
||||
EASYRSA_PATH: "/mnt/easyrsa"
|
||||
OVPN_SERVER: "127.0.0.1:7777:tcp"
|
||||
OVPN_INDEX_PATH: "/mnt/easyrsa/pki/index.txt"
|
||||
OVPN_AUTH: "true"
|
||||
OVPN_AUTH_DB_PATH: "/mnt/easyrsa/pki/users.db"
|
||||
LOG_LEVEL: "debug"
|
||||
network_mode: service:openvpn
|
||||
volumes:
|
||||
- ./easyrsa:/mnt/easyrsa
|
||||
- ./ccd:/mnt/ccd
|
||||
- ./easyrsa_master:/mnt/easyrsa
|
||||
- ./ccd_master:/mnt/ccd
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
|
||||
image="node:14.2-alpine3.11"
|
||||
image="node:16.13.0-alpine3.12"
|
||||
uid="$(id -u $USER)"
|
||||
|
||||
docker run -u $uid -w /app -v $(pwd):/app $image npm i && \
|
||||
|
|
8626
frontend/package-lock.json
generated
8626
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",
|
||||
"version": "1.0.1a",
|
||||
"author": "vitaliy.snurnitsin@gmail.com",
|
||||
|
@ -7,13 +7,18 @@
|
|||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development webpack-dev-server --hot",
|
||||
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
|
||||
"build": "cross-env NODE_ENV=production webpack --progress"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.19.2",
|
||||
"vue": "^2.6.12",
|
||||
"vue-clipboard2": "^0.2.1",
|
||||
"vue-good-table": "^2.21.1"
|
||||
"axios": "^0.27.1",
|
||||
"bootstrap-vue": "^2.22.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"vue": "^2.6.14",
|
||||
"vue-clipboard2": "^0.3.3",
|
||||
"vue-cookies": "^1.7.4",
|
||||
"vue-good-table": "^2.21.11",
|
||||
"vue-notification": "^1.3.20",
|
||||
"vue-style-loader": "^4.1.3"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
@ -21,23 +26,23 @@
|
|||
"not ie <= 8"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.8.6",
|
||||
"@babel/plugin-proposal-class-properties": "^7.0.0",
|
||||
"@babel/plugin-proposal-json-strings": "^7.0.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
|
||||
"@babel/plugin-syntax-import-meta": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-loader": "^8.0.0",
|
||||
"cross-env": "^7.0.0",
|
||||
"css-loader": "^3.4.2",
|
||||
"file-loader": "^5.1.0",
|
||||
"node-sass": "^4.13.1",
|
||||
"sass-loader": "^8.0.2",
|
||||
"terser-webpack-plugin": "^2.3.5",
|
||||
"vue-loader": "^15.9.0",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"webpack": "^4.42.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-dev-server": "^3.10.3"
|
||||
"@babel/core": "^7.16.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
||||
"@babel/plugin-proposal-json-strings": "^7.16.7",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-syntax-import-meta": "^7.10.4",
|
||||
"@babel/preset-env": "^7.16.5",
|
||||
"babel-loader": "^8.2.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^6.5.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"node-sass": "^7.0.1",
|
||||
"sass-loader": "^12.4.0",
|
||||
"terser-webpack-plugin": "^5.3.0",
|
||||
"vue-loader": "^17.0.0",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"webpack": "^5.65.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"webpack-dev-server": "^4.7.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import Vue from 'vue';
|
||||
import axios from 'axios';
|
||||
import VueCookies from 'vue-cookies'
|
||||
import BootstrapVue from 'bootstrap-vue'
|
||||
import VueClipboard from 'vue-clipboard2'
|
||||
import Notifications from 'vue-notification'
|
||||
import VueGoodTablePlugin from 'vue-good-table'
|
||||
|
||||
import 'vue-good-table/dist/vue-good-table.css'
|
||||
|
||||
Vue.use(VueCookies)
|
||||
Vue.use(VueClipboard)
|
||||
Vue.use(BootstrapVue)
|
||||
Vue.use(Notifications)
|
||||
Vue.use(VueGoodTablePlugin)
|
||||
|
||||
var axios_cfg = function(url, data='', type='form') {
|
||||
|
@ -52,6 +56,11 @@ new Vue({
|
|||
field: 'AccountStatus',
|
||||
filterable: true,
|
||||
},
|
||||
{
|
||||
label: 'Active Connections',
|
||||
field: 'Connections',
|
||||
filterable: true,
|
||||
},
|
||||
{
|
||||
label: 'Expiration Date',
|
||||
field: 'ExpirationDate',
|
||||
|
@ -82,42 +91,116 @@ new Vue({
|
|||
],
|
||||
rows: [],
|
||||
actions: [
|
||||
{
|
||||
name: 'u-change-password',
|
||||
label: 'Change password',
|
||||
class: 'btn-warning',
|
||||
showWhenStatus: 'Active',
|
||||
showForServerRole: ['master'],
|
||||
showForModule: ['passwdAuth'],
|
||||
},
|
||||
{
|
||||
name: 'u-revoke',
|
||||
label: 'Revoke',
|
||||
showWhenStatus: 'Active'
|
||||
class: 'btn-warning',
|
||||
showWhenStatus: 'Active',
|
||||
showForServerRole: ['master'],
|
||||
showForModule: ["core"],
|
||||
},
|
||||
{
|
||||
name: 'u-delete',
|
||||
label: 'Delete',
|
||||
class: 'btn-danger',
|
||||
showWhenStatus: 'Revoked',
|
||||
showForServerRole: ['master'],
|
||||
showForModule: ["core"],
|
||||
},
|
||||
{
|
||||
name: 'u-delete',
|
||||
label: 'Delete',
|
||||
class: 'btn-danger',
|
||||
showWhenStatus: 'Expired',
|
||||
showForServerRole: ['master'],
|
||||
showForModule: ["core"],
|
||||
},
|
||||
{
|
||||
name: 'u-rotate',
|
||||
label: 'Rotate',
|
||||
class: 'btn-warning',
|
||||
showWhenStatus: 'Revoked',
|
||||
showForServerRole: ['master'],
|
||||
showForModule: ["core"],
|
||||
},
|
||||
{
|
||||
name: 'u-rotate',
|
||||
label: 'Rotate',
|
||||
class: 'btn-warning',
|
||||
showWhenStatus: 'Expired',
|
||||
showForServerRole: ['master'],
|
||||
showForModule: ["core"],
|
||||
},
|
||||
{
|
||||
name: 'u-unrevoke',
|
||||
label: 'Unrevoke',
|
||||
showWhenStatus: 'Revoked'
|
||||
},
|
||||
{
|
||||
name: 'u-show-config',
|
||||
label: 'Show config',
|
||||
showWhenStatus: 'Active'
|
||||
class: 'btn-primary',
|
||||
showWhenStatus: 'Revoked',
|
||||
showForServerRole: ['master'],
|
||||
showForModule: ["core"],
|
||||
},
|
||||
// {
|
||||
// name: 'u-show-config',
|
||||
// label: 'Show config',
|
||||
// class: 'btn-primary',
|
||||
// showWhenStatus: 'Active',
|
||||
// showForServerRole: ['master', 'slave'],
|
||||
// showForModule: ["core"],
|
||||
// },
|
||||
{
|
||||
name: 'u-download-config',
|
||||
label: 'Download config',
|
||||
showWhenStatus: 'Active'
|
||||
class: 'btn-info',
|
||||
showWhenStatus: 'Active',
|
||||
showForServerRole: ['master', 'slave'],
|
||||
showForModule: ["core"],
|
||||
},
|
||||
{
|
||||
name: 'u-edit-ccd',
|
||||
label: 'Edit routes',
|
||||
showWhenStatus: 'Active'
|
||||
class: 'btn-primary',
|
||||
showWhenStatus: 'Active',
|
||||
showForServerRole: ['master'],
|
||||
showForModule: ["ccd"],
|
||||
},
|
||||
{
|
||||
name: 'u-edit-ccd',
|
||||
label: 'Show routes',
|
||||
class: 'btn-primary',
|
||||
showWhenStatus: 'Active',
|
||||
showForServerRole: ['slave'],
|
||||
showForModule: ["ccd"],
|
||||
}
|
||||
],
|
||||
filters: {
|
||||
hide_revoked: true
|
||||
hideRevoked: true,
|
||||
},
|
||||
serverRole: "master",
|
||||
lastSync: "unknown",
|
||||
modulesEnabled: [],
|
||||
u: {
|
||||
newUserName: '',
|
||||
// newUserPassword: 'nopass',
|
||||
newUserPassword: '',
|
||||
newUserCreateError: '',
|
||||
newPassword: '',
|
||||
passwordChangeStatus: '',
|
||||
passwordChangeMessage: '',
|
||||
rotateUserMessage: '',
|
||||
deleteUserMessage: '',
|
||||
modalNewUserVisible: false,
|
||||
modalShowConfigVisible: false,
|
||||
modalShowCcdVisible: false,
|
||||
modalChangePasswordVisible: false,
|
||||
modalRotateUserVisible: false,
|
||||
modalDeleteUserVisible: false,
|
||||
openvpnConfig: '',
|
||||
ccd: {
|
||||
Name: '',
|
||||
|
@ -132,16 +215,20 @@ new Vue({
|
|||
watch: {
|
||||
},
|
||||
mounted: function () {
|
||||
this.u_get_data()
|
||||
this.getUserData();
|
||||
this.getServerSetting();
|
||||
this.filters.hideRevoked = this.$cookies.isKey('hideRevoked') ? (this.$cookies.get('hideRevoked') == "true") : false
|
||||
},
|
||||
created() {
|
||||
var _this = this
|
||||
var _this = this;
|
||||
|
||||
_this.$root.$on('u-revoke', function (msg) {
|
||||
var data = new URLSearchParams();
|
||||
data.append('username', _this.username);
|
||||
axios.request(axios_cfg('api/user/revoke', data, 'form'))
|
||||
.then(function(response) {
|
||||
_this.u_get_data();
|
||||
_this.getUserData();
|
||||
_this.$notify({title: 'User ' + _this.username + ' revoked!', type: 'warn'})
|
||||
});
|
||||
})
|
||||
_this.$root.$on('u-unrevoke', function () {
|
||||
|
@ -149,9 +236,20 @@ new Vue({
|
|||
data.append('username', _this.username);
|
||||
axios.request(axios_cfg('api/user/unrevoke', data, 'form'))
|
||||
.then(function(response) {
|
||||
_this.u_get_data();
|
||||
_this.getUserData();
|
||||
_this.$notify({title: 'User ' + _this.username + ' unrevoked!', type: 'success'})
|
||||
});
|
||||
})
|
||||
_this.$root.$on('u-rotate', function () {
|
||||
_this.u.modalRotateUserVisible = true;
|
||||
var data = new URLSearchParams();
|
||||
data.append('username', _this.username);
|
||||
})
|
||||
_this.$root.$on('u-delete', function () {
|
||||
_this.u.modalDeleteUserVisible = true;
|
||||
var data = new URLSearchParams();
|
||||
data.append('username', _this.username);
|
||||
})
|
||||
_this.$root.$on('u-show-config', function () {
|
||||
_this.u.modalShowConfigVisible = true;
|
||||
var data = new URLSearchParams();
|
||||
|
@ -183,23 +281,37 @@ new Vue({
|
|||
_this.u.ccd = response.data;
|
||||
});
|
||||
})
|
||||
_this.$root.$on('u-disconnect-use', function () {
|
||||
_this.$root.$on('u-disconnect-user', function () {
|
||||
_this.u.modalShowCcdVisible = true;
|
||||
var data = new URLSearchParams();
|
||||
data.append('username', _this.username);
|
||||
axios.request(axios_cfg('api/user/disconnect', data, 'form'))
|
||||
.then(function(response) {
|
||||
_this.u.ccd = response.data;
|
||||
console.log(response.data);
|
||||
});
|
||||
})
|
||||
_this.$root.$on('u-change-password', function () {
|
||||
_this.u.modalChangePasswordVisible = true;
|
||||
var data = new URLSearchParams();
|
||||
data.append('username', _this.username);
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
customAddressEnabled: function () {
|
||||
customAddressDynamic: function () {
|
||||
return this.u.ccd.ClientAddress == "dynamic"
|
||||
},
|
||||
ccdApplyStatusCssClass: function () {
|
||||
return this.u.ccdApplyStatus == 200 ? "alert-success" : "alert-danger"
|
||||
},
|
||||
passwordChangeStatusCssClass: function () {
|
||||
return this.u.passwordChangeStatus == 200 ? "alert-success" : "alert-danger"
|
||||
},
|
||||
userRotateStatusCssClass: function () {
|
||||
return this.u.roatateUserStatus == 200 ? "alert-success" : "alert-danger"
|
||||
},
|
||||
deleteUserStatusCssClass: function () {
|
||||
return this.u.deleteUserStatus == 200 ? "alert-success" : "alert-danger"
|
||||
},
|
||||
modalNewUserDisplay: function () {
|
||||
return this.u.modalNewUserVisible ? {display: 'flex'} : {}
|
||||
},
|
||||
|
@ -209,55 +321,97 @@ new Vue({
|
|||
modalShowCcdDisplay: function () {
|
||||
return this.u.modalShowCcdVisible ? {display: 'flex'} : {}
|
||||
},
|
||||
modalChangePasswordDisplay: function () {
|
||||
return this.u.modalChangePasswordVisible ? {display: 'flex'} : {}
|
||||
},
|
||||
modalRotateUserDisplay: function () {
|
||||
return this.u.modalRotateUserVisible ? {display: 'flex'} : {}
|
||||
},
|
||||
modalDeleteUserDisplay: function () {
|
||||
return this.u.modalDeleteUserVisible ? {display: 'flex'} : {}
|
||||
},
|
||||
revokeFilterText: function() {
|
||||
return this.filters.hideRevoked ? "Show revoked" : "Hide revoked"
|
||||
},
|
||||
filteredRows: function() {
|
||||
var _this = this;
|
||||
|
||||
if(_this.filters.hide_revoked) {
|
||||
return _this.rows.filter(function(account) {
|
||||
return account.AccountStatus === "Active";
|
||||
if (this.filters.hideRevoked) {
|
||||
return this.rows.filter(function(account) {
|
||||
return account.AccountStatus == "Active"
|
||||
});
|
||||
} else {
|
||||
return _this.rows;
|
||||
return this.rows
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
methods: {
|
||||
rowStyleClassFn: function(row) {
|
||||
return row.ConnectionStatus == 'Connected' ? 'connected-user' : '' ;
|
||||
if (row.ConnectionStatus == 'Connected') {
|
||||
return 'connected-user'
|
||||
}
|
||||
if (row.AccountStatus == 'Revoked') {
|
||||
return 'revoked-user'
|
||||
}
|
||||
if (row.AccountStatus == 'Expired') {
|
||||
return 'expired-user'
|
||||
}
|
||||
return ''
|
||||
},
|
||||
rowActionFn: function(e) {
|
||||
this.username = e.target.dataset.username;
|
||||
this.$root.$emit(e.target.dataset.name);
|
||||
},
|
||||
u_get_data: function() {
|
||||
getUserData: function() {
|
||||
var _this = this;
|
||||
axios.request(axios_cfg('api/users/list'))
|
||||
.then(function(response) {
|
||||
_this.rows = Array.isArray(response.data) ? response.data : [];
|
||||
});
|
||||
},
|
||||
|
||||
getServerSetting: function() {
|
||||
var _this = this;
|
||||
axios.request(axios_cfg('api/server/settings'))
|
||||
.then(function(response) {
|
||||
_this.rows = response.data;
|
||||
_this.serverRole = response.data.serverRole;
|
||||
_this.modulesEnabled = response.data.modules;
|
||||
|
||||
if (_this.serverRole == "slave") {
|
||||
axios.request(axios_cfg('api/sync/last/successful'))
|
||||
.then(function(response) {
|
||||
_this.lastSync = response.data;
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
create_user: function() {
|
||||
|
||||
createUser: function() {
|
||||
var _this = this;
|
||||
|
||||
_this.u.newUserCreateError = "";
|
||||
|
||||
var data = new URLSearchParams();
|
||||
data.append('username', _this.u.newUserName);
|
||||
// data.append('password', this.u.newUserPassword);
|
||||
data.append('password', _this.u.newUserPassword);
|
||||
|
||||
_this.username = _this.u.newUserName;
|
||||
|
||||
axios.request(axios_cfg('api/user/create', data, 'form'))
|
||||
.then(function(response) {
|
||||
_this.u_get_data();
|
||||
_this.$notify({title: 'New user ' + _this.username + ' created', type: 'success'})
|
||||
_this.u.modalNewUserVisible = false;
|
||||
_this.u.newUserName = '';
|
||||
// _this.u.newUserPassword = 'nopass';
|
||||
_this.u.newUserPassword = '';
|
||||
_this.getUserData();
|
||||
})
|
||||
.catch(function(error) {
|
||||
_this.u.newUserCreateError = error.response.data;
|
||||
_this.$notify({title: 'New user ' + _this.username + ' creation failed.', type: 'error'})
|
||||
|
||||
});
|
||||
},
|
||||
ccd_apply: function() {
|
||||
|
||||
ccdApply: function() {
|
||||
var _this = this;
|
||||
|
||||
_this.u.ccdApplyStatus = "";
|
||||
|
@ -267,11 +421,84 @@ new Vue({
|
|||
.then(function(response) {
|
||||
_this.u.ccdApplyStatus = 200;
|
||||
_this.u.ccdApplyStatusMessage = response.data;
|
||||
_this.$notify({title: 'Ccd for user ' + _this.username + ' applied', type: 'success'})
|
||||
})
|
||||
.catch(function(error) {
|
||||
_this.u.ccdApplyStatus = error.response.status;
|
||||
_this.u.ccdApplyStatusMessage = error.response.data;
|
||||
_this.$notify({title: 'Ccd for user ' + _this.username + ' apply failed ', type: 'error'})
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
changeUserPassword: function(user) {
|
||||
var _this = this;
|
||||
|
||||
_this.u.passwordChangeMessage = "";
|
||||
|
||||
var data = new URLSearchParams();
|
||||
data.append('username', user);
|
||||
data.append('password', _this.u.newPassword);
|
||||
|
||||
axios.request(axios_cfg('api/user/change-password', data, 'form'))
|
||||
.then(function(response) {
|
||||
_this.u.passwordChangeStatus = 200;
|
||||
_this.u.newPassword = '';
|
||||
_this.getUserData();
|
||||
_this.u.modalChangePasswordVisible = false;
|
||||
_this.$notify({title: 'Password for user ' + _this.username + ' changed!', type: 'success'})
|
||||
})
|
||||
.catch(function(error) {
|
||||
_this.u.passwordChangeStatus = error.response.status;
|
||||
_this.u.passwordChangeMessage = error.response.data.message;
|
||||
_this.$notify({title: 'Changing password for user ' + _this.username + ' failed!', type: 'error'})
|
||||
});
|
||||
},
|
||||
|
||||
rotateUser: function(user) {
|
||||
var _this = this;
|
||||
|
||||
_this.u.rotateUserMessage = "";
|
||||
|
||||
var data = new URLSearchParams();
|
||||
data.append('username', user);
|
||||
data.append('password', _this.u.newPassword);
|
||||
|
||||
axios.request(axios_cfg('api/user/rotate', data, 'form'))
|
||||
.then(function(response) {
|
||||
_this.u.roatateUserStatus = 200;
|
||||
_this.u.newPassword = '';
|
||||
_this.getUserData();
|
||||
_this.u.modalRotateUserVisible = false;
|
||||
_this.$notify({title: 'Certificates for user ' + _this.username + ' rotated!', type: 'success'})
|
||||
})
|
||||
.catch(function(error) {
|
||||
_this.u.roatateUserStatus = error.response.status;
|
||||
_this.u.rotateUserMessage = error.response.data.message;
|
||||
_this.$notify({title: 'Rotate certificates for user ' + _this.username + ' failed!', type: 'error'})
|
||||
})
|
||||
},
|
||||
deleteUser: function(user) {
|
||||
var _this = this;
|
||||
|
||||
_this.u.deleteUserMessage = "";
|
||||
|
||||
var data = new URLSearchParams();
|
||||
data.append('username', user);
|
||||
|
||||
axios.request(axios_cfg('api/user/delete', data, 'form'))
|
||||
.then(function(response) {
|
||||
_this.u.deleteUserStatus = 200;
|
||||
_this.u.newPassword = '';
|
||||
_this.getUserData();
|
||||
_this.u.modalDeleteUserVisible = false;
|
||||
_this.$notify({title: 'User ' + _this.username + ' deleted!', type: 'success'})
|
||||
})
|
||||
.catch(function(error) {
|
||||
_this.u.deleteUserStatus = error.response.status;
|
||||
_this.u.deleteUserMessage = error.response.data.message;
|
||||
_this.$notify({title: 'Deleting user ' + _this.username + ' failed!', type: 'error'})
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
})
|
||||
|
|
|
@ -48,6 +48,14 @@ body {
|
|||
background-color: rgba(162, 245, 169, 0.5);
|
||||
}
|
||||
|
||||
.revoked-user {
|
||||
background-color: rgba(198, 186, 186, 0.5);
|
||||
}
|
||||
|
||||
.expired-user {
|
||||
background-color: rgba(255, 220, 127, 0.5);
|
||||
}
|
||||
|
||||
.new-user-btn {
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
@ -58,3 +66,7 @@ body {
|
|||
background-color: #ffffff;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.ccd-routes {
|
||||
|
||||
}
|
5
frontend/src/style.js
Normal file
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">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>openvpn-admin</title>
|
||||
<link rel="stylesheet" href="css/normalize.css">
|
||||
<link rel="stylesheet" href="css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<title>ovpn-admin</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="dist/style.min.js"></script>
|
||||
<div id="app">
|
||||
<vue-good-table
|
||||
:columns="columns"
|
||||
|
@ -16,18 +14,19 @@
|
|||
:row-style-class="rowStyleClassFn"
|
||||
:search-options="{ enabled: true}" >
|
||||
<div slot="table-actions">
|
||||
<button type="button" class="btn btn-sm btn-success el-square" v-on:click.stop="u.modalNewUserVisible=true">Add user</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary el-square" v-on:click.stop="filters.hide_revoked=!filters.hide_revoked" v-show="filters.hide_revoked">Show revoked</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary el-square" v-on:click.stop="filters.hide_revoked=!filters.hide_revoked" v-show="!filters.hide_revoked">Hide revoked</button>
|
||||
<button type="button" class="btn btn-sm btn-success el-square" v-show="serverRole == 'master'" v-on:click.stop="u.modalNewUserVisible=true">Add user</button>
|
||||
<b-badget class="btn btn-sm btn-info el-square" v-if="serverRole == 'slave'">Slave - last sync: {{ lastSync }}</b-badget>
|
||||
<button type="button" class="btn btn-sm btn-secondary el-square" v-on:click.stop="filters.hideRevoked=!filters.hideRevoked;this.$cookies.set('hideRevoked',!(this.$cookies.get('hideRevoked') == 'true'), -1);">{{ revokeFilterText }}</button>
|
||||
</div>
|
||||
<div slot="emptystate" class="d-flex justify-content-center">
|
||||
<h4>No users have been created yet.</h4>
|
||||
<button type="button" class="btn btn-sm btn-success el-square" v-on:click.stop="u.modalNewUserVisible=true">Add user</button>
|
||||
<br>
|
||||
<button type="button" class="btn btn-sm btn-success el-square" v-if="serverRole == 'master'" v-on:click.stop="u.modalNewUserVisible=true">Add user</button>
|
||||
</div>
|
||||
<template slot="table-row" slot-scope="props">
|
||||
<span v-if="props.column.field == 'actions'">
|
||||
<button
|
||||
class="btn btn-sm btn-success el-square modal-el-margin"
|
||||
class="btn btn-sm el-square modal-el-margin"
|
||||
type="button"
|
||||
:title="action.label"
|
||||
:data-username="props.row.Identity"
|
||||
|
@ -35,26 +34,23 @@
|
|||
:data-text="action.label"
|
||||
@click.left.stop="rowActionFn"
|
||||
v-for="action in actions"
|
||||
v-if="action.showWhenStatus == props.row.AccountStatus">
|
||||
v-bind:class="action.class"
|
||||
v-if="action.showWhenStatus == props.row.AccountStatus && action.showForServerRole.includes(serverRole) && action.showForModule.some(p=> modulesEnabled.includes(p))">
|
||||
{{ action.label }}
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
</vue-good-table>
|
||||
|
||||
<!-- <div class="d-flex justify-content-md-end">-->
|
||||
<!-- <button type="button" class="btn btn-sm btn-success el-square new-user-btn" v-on:click.stop="u.ctxVisible=false;u.modalNewUserVisible=true">Add user</button>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<div class="modal-wrapper" v-if="u.modalNewUserVisible" v-bind:style="modalNewUserDisplay">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4>Add new user </h4>
|
||||
<h4>Add new user</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="text" class="form-control el-square modal-el-margin" placeholder="Username [_a-zA-Z0-9\.-]" v-model="u.newUserName">
|
||||
<!-- <input type="password" class="form-control el-square modal-el-margin" minlength="6" autocomplete="off" placeholder="Password [_a-zA-Z0-9\.-]" v-model="u.newUserPassword">-->
|
||||
<input type="password" class="form-control el-square modal-el-margin" minlength="6" autocomplete="off" placeholder="Password [_a-zA-Z0-9\.-]" v-model="u.newUserPassword" v-if="modulesEnabled.includes('passwdAuth')">
|
||||
</div>
|
||||
|
||||
<div class="modal-footer justify-content-center" v-if="u.newUserCreateError.length > 0">
|
||||
|
@ -63,18 +59,41 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="create_user();">Create</button>
|
||||
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="createUser()">Create</button>
|
||||
<button type="button" class="btn btn-primary el-square d-flex justify-content-sm-end modal-el-margin" v-on:click.stop="u.newUserName='';u.newUserPassword='nopass';u.modalNewUserVisible=false">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-wrapper" v-if="u.modalChangePasswordVisible" v-bind:style="modalChangePasswordDisplay">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4>Change password for: <strong>{{ username }}</strong></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="password" class="form-control el-square modal-el-margin" minlength="6" autocomplete="off" placeholder="Password [_a-zA-Z0-9\.-]" v-model="u.newPassword">
|
||||
</div>
|
||||
|
||||
<div class="modal-footer justify-content-center" v-if="u.passwordChangeMessage.length > 0">
|
||||
<div class="alert" v-bind:class="passwordChangeStatusCssClass" role="alert" >
|
||||
{{ u.passwordChangeMessage }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="changeUserPassword(username)">Change password</button>
|
||||
<button type="button" class="btn btn-primary el-square d-flex justify-content-sm-end modal-el-margin" v-on:click.stop="u.newPassword='';u.passwordChangeMessage='';u.modalChangePasswordVisible=false">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-wrapper" v-if="u.modalShowConfigVisible" v-bind:style="modalShowConfigDisplay">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4>ovpn config for {{ username }}</h4>
|
||||
<h4>ovpn config for: <strong>{{ username }}</strong></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="d-flex">
|
||||
|
@ -95,42 +114,57 @@
|
|||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="static-address-label ">Routes table for: <strong>{{ username }}</strong></h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="input-group">
|
||||
<h4 class="static-address-label ">Client "{{ username }}" static address</h4>
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<input id="enable-static" type="checkbox" onchange="document.getElementById('staticAddress').disabled=!this.checked;" v-bind:checked="!customAddressEnabled">
|
||||
</div>
|
||||
<h5 class="static-address-label ">Static address:</h5>
|
||||
<input id="static-address" type="text" class="form-control" v-model="u.ccd.ClientAddress" placeholder="127.0.0.1">
|
||||
<div class="input-group-append">
|
||||
<button id="static-address-clear" class="btn btn-warning" type="button" v-on:click="u.ccd.ClientAddress = 'dynamic'" v-if="serverRole == 'master'" v-bind:disabled="customAddressDynamic">Clear</button>
|
||||
</div>
|
||||
<input id="staticAddress" type="text" class="form-control" v-model="u.ccd.ClientAddress" placeholder="127.0.0.1" v-bind:disabled="customAddressEnabled">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="d-flex ">
|
||||
<table class="table table-bordered table-hover">
|
||||
<table class="table table-bordered table-hover ccd-routes" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Address</th>
|
||||
<th scope="col">Mask</th>
|
||||
<th scope="col">Description</th>
|
||||
<th scope="col">Action</th>
|
||||
<th scope="col" v-if="serverRole == 'master'">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(customRoute, index) in u.ccd.CustomRoutes">
|
||||
<td>{{ customRoute.Address }}</td>
|
||||
<td>{{ customRoute.Mask }}</td>
|
||||
<td>{{ customRoute.Description }}</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-primary btn-sm el-square modal-el-margin" v-on:click.stop="u.ccd.CustomRoutes.splice(index, 1)">Delete</button>
|
||||
<div v-if="serverRole == 'slave'">
|
||||
{{ customRoute.Address }}
|
||||
</div>
|
||||
<input v-if="serverRole == 'master'" v-model="customRoute.Address">
|
||||
</td>
|
||||
<td>
|
||||
<div v-if="serverRole == 'slave'">
|
||||
{{ customRoute.Mask }}
|
||||
</div>
|
||||
<input v-if="serverRole == 'master'" v-model="customRoute.Mask">
|
||||
</td>
|
||||
<td>
|
||||
<div v-if="serverRole == 'slave'">
|
||||
{{ customRoute.Description }}
|
||||
</div>
|
||||
<input v-if="serverRole == 'master'" v-model="customRoute.Description">
|
||||
</td>
|
||||
<td class="text-right" v-if="serverRole == 'master'">
|
||||
<button type="button" class="btn btn-danger btn-sm el-square modal-el-margin" v-if="serverRole == 'master'" v-on:click.stop="u.ccd.CustomRoutes.splice(index, 1)">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr v-if="serverRole == 'master'">
|
||||
<td><input type="text" v-model="u.newRoute.Address"/></td>
|
||||
<td><input type="text" v-model="u.newRoute.Mask"/></td>
|
||||
<td><input type="text" v-model="u.newRoute.Description"/></td>
|
||||
<td>
|
||||
<td class="text-right" v-if="serverRole == 'master'">
|
||||
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="u.ccd.CustomRoutes.push(u.newRoute);u.newRoute={};">Add</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -144,14 +178,59 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="ccd_apply()">Save</button>
|
||||
<button type="button" class="btn btn-success el-square modal-el-margin" v-if="serverRole == 'master'" v-on:click.stop="ccdApply()">Save</button>
|
||||
<button type="button" class="btn btn-primary el-square modal-el-margin" v-on:click.stop="u.ccd={Name:'',ClientAddress:'',CustomRoutes:[]};u.ccdApplyStatusMessage='';u.ccdApplyStatus='';u.modalShowCcdVisible=false">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-wrapper" v-if="u.modalRotateUserVisible" v-bind:style="modalRotateUserDisplay">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4>Confirm rotating certificates for user: <strong>{{ username }}</strong></h4>
|
||||
</div>
|
||||
<div class="modal-body" v-if="modulesEnabled.includes('passwdAuth')">
|
||||
<h4>Enter new password:</h4>
|
||||
<input type="password" class="form-control el-square modal-el-margin" minlength="6" autocomplete="off" placeholder="Password [_a-zA-Z0-9\.-]" v-model="u.newPassword">
|
||||
</div>
|
||||
|
||||
<div class="modal-footer justify-content-center" v-if="u.rotateUserMessage.length > 0">
|
||||
<div class="alert" v-bind:class="userRotateStatusCssClass" role="alert" >
|
||||
{{ u.rotateUserMessage }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger el-square modal-el-margin" v-on:click.stop="rotateUser(username)">Rotate</button>
|
||||
<button type="button" class="btn btn-primary el-square d-flex justify-content-sm-end modal-el-margin" v-on:click.stop="u.newPassword='';u.rotateUserMessage='';u.modalRotateUserVisible=false">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-wrapper" v-if="u.modalDeleteUserVisible" v-bind:style="modalDeleteUserDisplay">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4>Confirm deleting user: <strong>{{ username }}</strong></h4>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer justify-content-center" v-if="u.deleteUserMessage.length > 0">
|
||||
<div class="alert" v-bind:class="deleteUserStatusCssClass" role="alert" >
|
||||
{{ u.deleteUserMessage }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger el-square modal-el-margin" v-on:click.stop="deleteUser(username)">Delete</button>
|
||||
<button type="button" class="btn btn-primary el-square d-flex justify-content-sm-end modal-el-margin" v-on:click.stop="u.deleteUserMessage='';u.modalDeleteUserVisible=false">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<notifications position="bottom left" :speed="900" />
|
||||
</div>
|
||||
<script src="dist/build.js"></script>
|
||||
<script src="dist/bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,106 +1,50 @@
|
|||
var path = require('path')
|
||||
var webpack = require('webpack')
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const path = require('path');
|
||||
//const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
|
||||
module.exports = {
|
||||
entry: './src/main.js',
|
||||
output: {
|
||||
path: path.resolve(__dirname, './static/dist'),
|
||||
publicPath: '/dist/',
|
||||
filename: 'build.js'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
'vue-style-loader',
|
||||
'css-loader'
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
'vue-style-loader',
|
||||
'css-loader',
|
||||
'sass-loader'
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.sass$/,
|
||||
use: [
|
||||
'vue-style-loader',
|
||||
'css-loader',
|
||||
'sass-loader?indentedSyntax'
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
loaders: {
|
||||
// Since sass-loader (weirdly) has SCSS as its default parse mode, we map
|
||||
// the "scss" and "sass" values for the lang attribute to the right configs here.
|
||||
// other preprocessors should work out of the box, no loader config like this necessary.
|
||||
'scss': [
|
||||
'vue-style-loader',
|
||||
'css-loader',
|
||||
'sass-loader'
|
||||
],
|
||||
'sass': [
|
||||
'vue-style-loader',
|
||||
'css-loader',
|
||||
'sass-loader?indentedSyntax'
|
||||
]
|
||||
}
|
||||
// other vue-loader options go here
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|gif|svg)$/,
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]?[hash]'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'vue$': 'vue/dist/vue.esm.js'
|
||||
mode: 'production',
|
||||
entry: {
|
||||
bundle: [
|
||||
'./src/main.js',
|
||||
],
|
||||
style: [
|
||||
'./src/style.js',
|
||||
]
|
||||
},
|
||||
extensions: ['*', '.js', '.vue', '.json']
|
||||
},
|
||||
devServer: {
|
||||
historyApiFallback: true,
|
||||
noInfo: true,
|
||||
overlay: true
|
||||
},
|
||||
performance: {
|
||||
hints: false
|
||||
},
|
||||
devtool: '#eval-source-map'
|
||||
}
|
||||
output: {
|
||||
path: path.resolve(__dirname, './static/dist'),
|
||||
publicPath: '/dist/',
|
||||
filename: '[name].min.js'
|
||||
},
|
||||
plugins: [
|
||||
//new BundleAnalyzerPlugin(),
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
'vue-style-loader',
|
||||
'css-loader'
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
//exclude: /node_modules\/(?!bootstrap-vue\/src\/)/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-env']
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'vue$': 'vue/dist/vue.esm.js',
|
||||
//'bootstrap-vue$': 'bootstrap-vue/src/index.js'
|
||||
},
|
||||
extensions: ['*', '.js', '.vue', '.json']
|
||||
},
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports.devtool = '#source-map'
|
||||
// http://vue-loader.vuejs.org/en/workflow/production.html
|
||||
module.exports.plugins = (module.exports.plugins || []).concat([
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
NODE_ENV: '"production"'
|
||||
}
|
||||
}),
|
||||
new TerserPlugin({
|
||||
sourceMap: true
|
||||
}),
|
||||
new webpack.LoaderOptionsPlugin({
|
||||
minimize: true
|
||||
})
|
||||
])
|
||||
}
|
||||
|
|
60
go.mod
60
go.mod
|
@ -1,9 +1,61 @@
|
|||
module openvpn-web-ui
|
||||
module ovpn-admin
|
||||
|
||||
go 1.14
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/gobuffalo/packr/v2 v2.8.3
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
k8s.io/apimachinery v0.23.1
|
||||
k8s.io/client-go v0.23.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-logr/logr v1.2.2 // indirect
|
||||
github.com/gobuffalo/envy v1.10.1 // indirect
|
||||
github.com/gobuffalo/logger v1.0.6 // indirect
|
||||
github.com/gobuffalo/packd v1.0.1 // indirect
|
||||
github.com/gobuffalo/packr v1.30.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-cmp v0.5.6 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/googleapis/gnostic v0.5.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/karrick/godirwalk v1.16.1 // indirect
|
||||
github.com/markbates/errx v1.1.0 // indirect
|
||||
github.com/markbates/oncer v1.0.0 // indirect
|
||||
github.com/markbates/safe v1.0.1 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.32.1 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 // indirect
|
||||
golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
k8s.io/api v0.23.1 // indirect
|
||||
k8s.io/klog/v2 v2.40.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20220114203427-a0453230fd26 // indirect
|
||||
k8s.io/utils v0.0.0-20211208161948-7d6a63dca704 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
|
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
|
306
helpers.go
Normal file
306
helpers.go
Normal file
|
@ -0,0 +1,306 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func parseDate(layout, datetime string) time.Time {
|
||||
t, err := time.Parse(layout, datetime)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func parseDateToString(layout, datetime, format string) string {
|
||||
return parseDate(layout, datetime).Format(format)
|
||||
}
|
||||
|
||||
func parseDateToUnix(layout, datetime string) int64 {
|
||||
return parseDate(layout, datetime).Unix()
|
||||
}
|
||||
|
||||
func runBash(script string) string {
|
||||
log.Debugln(script)
|
||||
cmd := exec.Command("bash", "-c", script)
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Sprint(err) + " : " + string(stdout)
|
||||
}
|
||||
return string(stdout)
|
||||
}
|
||||
|
||||
func fExist(path string) bool {
|
||||
var _, err = os.Stat(path)
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
} else if err != nil {
|
||||
log.Fatalf("fExist: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func fRead(path string) string {
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Warning(err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(content)
|
||||
}
|
||||
|
||||
func fCreate(path string) error {
|
||||
var _, err = os.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
var file, err = os.Create(path)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fWrite(path, content string) error {
|
||||
err := ioutil.WriteFile(path, []byte(content), 0644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fDelete(path string) error {
|
||||
err := os.Remove(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fCopy(src, dst string) error {
|
||||
sfi, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !sfi.Mode().IsRegular() {
|
||||
// cannot copy non-regular files (e.g., directories, symlinks, devices, etc.)
|
||||
return fmt.Errorf("fCopy: non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String())
|
||||
}
|
||||
dfi, err := os.Stat(dst)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if !(dfi.Mode().IsRegular()) {
|
||||
return fmt.Errorf("fCopy: non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String())
|
||||
}
|
||||
if os.SameFile(sfi, dfi) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = os.Link(src, dst); err == nil {
|
||||
return err
|
||||
}
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
cerr := out.Close()
|
||||
if err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
if _, err = io.Copy(out, in); err != nil {
|
||||
return err
|
||||
}
|
||||
err = out.Sync()
|
||||
return err
|
||||
}
|
||||
|
||||
func fMove(src, dst string) error {
|
||||
err := fCopy(src, dst)
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
return err
|
||||
}
|
||||
err = fDelete(src)
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fDownload(path, url string, basicAuth bool) error {
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if basicAuth {
|
||||
req.SetBasicAuth(*masterBasicAuthUser, *masterBasicAuthPassword)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
log.Warnf("WARNING: Download file operation for url %s finished with status code %d\n", url, resp.StatusCode)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fCreate(path)
|
||||
fWrite(path, string(body))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createArchiveFromDir(dir, path string) error {
|
||||
|
||||
var files []string
|
||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
files = append(files, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
}
|
||||
|
||||
out, err := os.Create(path)
|
||||
if err != nil {
|
||||
log.Errorf("Error writing archive %s: %s", path, err)
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
gw := gzip.NewWriter(out)
|
||||
defer gw.Close()
|
||||
tw := tar.NewWriter(gw)
|
||||
defer tw.Close()
|
||||
|
||||
// Iterate over files and add them to the tar archive
|
||||
for _, filePath := range files {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
log.Warnf("Error writing archive %s: %s", path, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Get FileInfo about our file providing file size, mode, etc.
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a tar Header from the FileInfo data
|
||||
header, err := tar.FileInfoHeader(info, info.Name())
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
header.Name = strings.Replace(filePath, dir+"/", "", 1)
|
||||
|
||||
// Write file header to the tar archive
|
||||
err = tw.WriteHeader(header)
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy file content to tar archive
|
||||
_, err = io.Copy(tw, file)
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractFromArchive(archive, path string) error {
|
||||
// Open the file which will be written into the archive
|
||||
file, err := os.Open(archive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Write file header to the tar archive
|
||||
uncompressedStream, err := gzip.NewReader(file)
|
||||
if err != nil {
|
||||
log.Fatal("extractFromArchive(): NewReader failed")
|
||||
}
|
||||
|
||||
tarReader := tar.NewReader(uncompressedStream)
|
||||
|
||||
for true {
|
||||
header, err := tarReader.Next()
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("extractFromArchive: Next() failed: %s", err.Error())
|
||||
}
|
||||
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err := os.Mkdir(path+"/"+header.Name, 0755); err != nil {
|
||||
log.Fatalf("extractFromArchive: Mkdir() failed: %s", err.Error())
|
||||
}
|
||||
case tar.TypeReg:
|
||||
outFile, err := os.Create(path + "/" + header.Name)
|
||||
if err != nil {
|
||||
log.Fatalf("extractFromArchive: Create() failed: %s", err.Error())
|
||||
}
|
||||
if _, err := io.Copy(outFile, tarReader); err != nil {
|
||||
log.Fatalf("extractFromArchive: Copy() failed: %s", err.Error())
|
||||
}
|
||||
outFile.Close()
|
||||
|
||||
default:
|
||||
log.Fatalf(
|
||||
"extractFromArchive: uknown type: %s in %s", header.Typeflag, header.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
BIN
img/ovpn-admin-metrics.png
Normal file
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
|
59
setup/configure.sh
Normal file
59
setup/configure.sh
Normal file
|
@ -0,0 +1,59 @@
|
|||
#!/usr/bin/env bash
|
||||
set -ex
|
||||
|
||||
EASY_RSA_LOC="/etc/openvpn/easyrsa"
|
||||
SERVER_CERT="${EASY_RSA_LOC}/pki/issued/server.crt"
|
||||
|
||||
OVPN_SRV_NET=${OVPN_SERVER_NET:-172.16.100.0}
|
||||
OVPN_SRV_MASK=${OVPN_SERVER_MASK:-255.255.255.0}
|
||||
|
||||
|
||||
cd $EASY_RSA_LOC
|
||||
|
||||
if [ -e "$SERVER_CERT" ]; then
|
||||
echo "Found existing certs - reusing"
|
||||
else
|
||||
if [ ${OVPN_ROLE:-"master"} = "slave" ]; then
|
||||
echo "Waiting for initial sync data from master"
|
||||
while [ $(wget -q localhost/api/sync/last/try -O - | wc -m) -lt 1 ]
|
||||
do
|
||||
sleep 5
|
||||
done
|
||||
else
|
||||
echo "Generating new certs"
|
||||
easyrsa init-pki
|
||||
cp -R /usr/share/easy-rsa/* $EASY_RSA_LOC/pki
|
||||
echo "ca" | easyrsa build-ca nopass
|
||||
easyrsa build-server-full server nopass
|
||||
easyrsa gen-dh
|
||||
openvpn --genkey --secret ./pki/ta.key
|
||||
fi
|
||||
fi
|
||||
easyrsa gen-crl
|
||||
|
||||
iptables -t nat -D POSTROUTING -s ${OVPN_SRV_NET}/${OVPN_SRV_MASK} ! -d ${OVPN_SRV_NET}/${OVPN_SRV_MASK} -j MASQUERADE || true
|
||||
iptables -t nat -A POSTROUTING -s ${OVPN_SRV_NET}/${OVPN_SRV_MASK} ! -d ${OVPN_SRV_NET}/${OVPN_SRV_MASK} -j MASQUERADE
|
||||
|
||||
mkdir -p /dev/net
|
||||
if [ ! -c /dev/net/tun ]; then
|
||||
mknod /dev/net/tun c 10 200
|
||||
fi
|
||||
|
||||
cp -f /etc/openvpn/setup/openvpn.conf /etc/openvpn/openvpn.conf
|
||||
|
||||
if [ ${OVPN_PASSWD_AUTH} = "true" ]; then
|
||||
mkdir -p /etc/openvpn/scripts/
|
||||
cp -f /etc/openvpn/setup/auth.sh /etc/openvpn/scripts/auth.sh
|
||||
chmod +x /etc/openvpn/scripts/auth.sh
|
||||
echo "auth-user-pass-verify /etc/openvpn/scripts/auth.sh via-file" | tee -a /etc/openvpn/openvpn.conf
|
||||
echo "script-security 2" | tee -a /etc/openvpn/openvpn.conf
|
||||
echo "verify-client-cert require" | tee -a /etc/openvpn/openvpn.conf
|
||||
openvpn-user db-init --db.path=$EASY_RSA_LOC/pki/users.db
|
||||
fi
|
||||
|
||||
[ -d $EASY_RSA_LOC/pki ] && chmod 755 $EASY_RSA_LOC/pki
|
||||
[ -f $EASY_RSA_LOC/pki/crl.pem ] && chmod 644 $EASY_RSA_LOC/pki/crl.pem
|
||||
|
||||
mkdir -p /etc/openvpn/ccd
|
||||
|
||||
openvpn --config /etc/openvpn/openvpn.conf --client-config-dir /etc/openvpn/ccd --port 1194 --proto tcp --management 127.0.0.1 8989 --dev tun0 --server ${OVPN_SRV_NET} ${OVPN_SRV_MASK}
|
|
@ -1,4 +1,4 @@
|
|||
server 172.16.100.0 255.255.255.0
|
||||
# server 172.16.100.0 255.255.255.0
|
||||
verb 3
|
||||
tls-server
|
||||
ca /etc/openvpn/easyrsa/pki/ca.crt
|
||||
|
@ -8,19 +8,19 @@ dh /etc/openvpn/easyrsa/pki/dh.pem
|
|||
crl-verify /etc/openvpn/easyrsa/pki/crl.pem
|
||||
tls-auth /etc/openvpn/easyrsa/pki/ta.key
|
||||
key-direction 0
|
||||
duplicate-cn
|
||||
cipher AES-128-CBC
|
||||
management 127.0.0.1 8989
|
||||
#management 127.0.0.1 8989
|
||||
keepalive 10 60
|
||||
persist-key
|
||||
persist-tun
|
||||
topology subnet
|
||||
proto tcp
|
||||
port 1194
|
||||
dev tun0
|
||||
#duplicate-cn
|
||||
#proto tcp
|
||||
#port 1194
|
||||
#dev tun0
|
||||
status /tmp/openvpn-status.log
|
||||
user nobody
|
||||
group nogroup
|
||||
push "topology subnet"
|
||||
push "route-metric 9999"
|
||||
push "dhcp-option DNS 1.1.1.1"
|
||||
push "dhcp-option DNS 1.1.1.1"
|
4
start-with-slave.sh
Executable file
4
start-with-slave.sh
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
./start.sh
|
||||
docker-compose -p openvpn-slave -f docker-compose-slave.yaml up -d
|
3
start.sh
Executable file
3
start.sh
Executable file
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
docker compose -p openvpn-master up -d --build
|
|
@ -1,5 +1,5 @@
|
|||
{{- if (ne .ClientAddress "dynamic") }}
|
||||
ifconfig-push {{ .ClientAddress }} 255.255.255.255
|
||||
ifconfig-push {{ .ClientAddress }} 255.255.255.0
|
||||
{{- end }}
|
||||
{{- range $route := .CustomRoutes }}
|
||||
push "route {{ $route.Address }} {{ $route.Mask }}" # {{ $route.Description }}
|
38
templates/client.conf.tpl
Normal file
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
|
||||
deploy:
|
||||
helmRelease: "[[ project ]]-[[ env ]]"
|
||||
namespace: "[[ project ]]-[[ env ]]"
|
||||
|
||||
---
|
||||
artifact: backend-builder
|
||||
from: golang:1.14.2-alpine3.11
|
||||
git:
|
||||
- add: /
|
||||
to: /app
|
||||
stageDependencies:
|
||||
install:
|
||||
- "*.go"
|
||||
excludePaths:
|
||||
- .helm
|
||||
- .werf
|
||||
- frontend
|
||||
- werf.yaml
|
||||
- Dockerfile
|
||||
ansible:
|
||||
install:
|
||||
- name: Install packages
|
||||
apk:
|
||||
name:
|
||||
- build-base
|
||||
- gcc
|
||||
- name: Build backend
|
||||
command: go build -ldflags='-extldflags "-static" -s -w' -o openvpn-admin
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: linux
|
||||
GOARCH: amd64
|
||||
args:
|
||||
chdir: /app
|
||||
|
||||
---
|
||||
artifact: frontend-builder
|
||||
from: node:14.2-alpine3.11
|
||||
git:
|
||||
- add: /frontend
|
||||
to: /app
|
||||
stageDependencies:
|
||||
install:
|
||||
- "**/*"
|
||||
excludePaths:
|
||||
- Dockerfile
|
||||
- build.sh
|
||||
- werf.yaml
|
||||
ansible:
|
||||
setup:
|
||||
- name: install deps
|
||||
command: npm install
|
||||
args:
|
||||
chdir: /app
|
||||
- name: Build app
|
||||
command: npm run build
|
||||
args:
|
||||
chdir: /app
|
||||
|
||||
---
|
||||
image: openvpn-admin
|
||||
from: alpine:3.11
|
||||
import:
|
||||
- artifact: backend-builder
|
||||
add: /app/openvpn-admin
|
||||
to: /usr/bin/openvpn-admin
|
||||
before: setup
|
||||
- artifact: frontend-builder
|
||||
add: /app/static
|
||||
to: /app/static
|
||||
before: setup
|
||||
git:
|
||||
- add: /client.conf.tpl
|
||||
to: /app/client.conf.tpl
|
||||
stageDependencies:
|
||||
setup:
|
||||
- "*"
|
||||
- add: /ccd.tpl
|
||||
to: /app/ccd.tpl
|
||||
stageDependencies:
|
||||
setup:
|
||||
- "*"
|
||||
ansible:
|
||||
install:
|
||||
- name: Install packages
|
||||
apk:
|
||||
name:
|
||||
- easy-rsa
|
||||
- bash
|
||||
- name: Create symbolic link for easy-rsa
|
||||
file:
|
||||
src: "/usr/share/easy-rsa/easyrsa"
|
||||
dest: "/usr/local/bin/easyrsa"
|
||||
state: link
|
||||
image: ovpn-admin
|
||||
dockerfile: Dockerfile
|
||||
|
||||
---
|
||||
image: openvpn
|
||||
from: alpine:3.11
|
||||
git:
|
||||
- add: /.werffiles/
|
||||
to: /etc/openvpn/setup/
|
||||
stageDependencies:
|
||||
install:
|
||||
- "*"
|
||||
ansible:
|
||||
install:
|
||||
- name: Install packages
|
||||
apk:
|
||||
name:
|
||||
- openvpn
|
||||
- easy-rsa
|
||||
- name: Create symbolic link for easy-rsa
|
||||
file:
|
||||
src: "/usr/share/easy-rsa/easyrsa"
|
||||
dest: "/usr/local/bin/easyrsa"
|
||||
state: link
|
||||
|
||||
|
||||
dockerfile: Dockerfile.openvpn
|
||||
|
|
Loading…
Reference in a new issue