Compare commits
No commits in common. "master" and "1.1" have entirely different histories.
57 changed files with 6831 additions and 9209 deletions
|
@ -3,24 +3,12 @@
|
|||
*.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
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
|
||||
; 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
35
.github/workflows/publish-latest.yaml
vendored
|
@ -1,35 +0,0 @@
|
|||
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
39
.github/workflows/publish-tag.yaml
vendored
|
@ -1,39 +0,0 @@
|
|||
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
29
.github/workflows/release.yaml
vendored
|
@ -1,29 +0,0 @@
|
|||
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
29
.github/workflows/release_arm.yaml
vendored
|
@ -1,29 +0,0 @@
|
|||
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 }}
|
21
.gitignore
vendored
21
.gitignore
vendored
|
@ -1,21 +1,6 @@
|
|||
easyrsa
|
||||
easyrsa_master
|
||||
easyrsa_slave
|
||||
ccd
|
||||
ccd_master
|
||||
ccd_slave
|
||||
openvpn-web-ui*
|
||||
openvpn-ui*
|
||||
openvpn-admin*
|
||||
ovpn-admin*
|
||||
openvpn-web-ui
|
||||
openvpn-ui
|
||||
openvpn-admin
|
||||
frontend/node_modules
|
||||
|
||||
main-packr.go
|
||||
packrd/
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
|
|
33
.werffiles/configure.sh
Normal file
33
.werffiles/configure.sh
Normal file
|
@ -0,0 +1,33 @@
|
|||
#!/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
|
||||
|
|
@ -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,16 +8,16 @@ 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
|
||||
#duplicate-cn
|
||||
#proto tcp
|
||||
#port 1194
|
||||
#dev tun0
|
||||
proto tcp
|
||||
port 1194
|
||||
dev tun0
|
||||
status /tmp/openvpn-status.log
|
||||
user nobody
|
||||
group nogroup
|
29
Dockerfile
29
Dockerfile
|
@ -1,20 +1,19 @@
|
|||
FROM node:16-alpine3.15 AS frontend-builder
|
||||
COPY frontend/ /app
|
||||
RUN apk add --update python3 make g++ && cd /app && npm install && npm run build
|
||||
|
||||
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
|
||||
FROM golang:1.14.2-alpine3.11 AS backend-builder
|
||||
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
|
||||
#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 alpine:3.16
|
||||
FROM node:14.2-alpine3.11 AS frontend-builder
|
||||
COPY frontend/ /app
|
||||
RUN cd /app && npm install && npm run build
|
||||
|
||||
FROM alpine:3.11
|
||||
WORKDIR /app
|
||||
COPY --from=backend-builder /app/ovpn-admin /app
|
||||
ARG TARGETARCH
|
||||
RUN apk add --update bash easy-rsa openssl openvpn coreutils && \
|
||||
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 && \
|
||||
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,9 +1,7 @@
|
|||
FROM alpine:3.16
|
||||
ARG TARGETARCH
|
||||
RUN apk add --update bash openvpn easy-rsa iptables && \
|
||||
FROM alpine:3.11
|
||||
RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \
|
||||
apk add --update bash openvpn easy-rsa && \
|
||||
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
|
||||
COPY setup/ /etc/openvpn/setup
|
||||
COPY .werffiles /etc/openvpn/setup
|
||||
RUN chmod +x /etc/openvpn/setup/configure.sh
|
178
README.md
178
README.md
|
@ -1,177 +1 @@
|
|||
# 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.
|
||||
# openvpn-web-ui
|
12
build.sh
12
build.sh
|
@ -1,11 +1,3 @@
|
|||
#!/usr/bin/env bash
|
||||
#!/bin/bash
|
||||
|
||||
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
|
||||
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags='-extldflags "-static" -s -w' -o openvpn-admin
|
||||
|
|
18
build_arm.sh
18
build_arm.sh
|
@ -1,18 +0,0 @@
|
|||
#!/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
|
|
@ -1,5 +1,5 @@
|
|||
{{- if (ne .ClientAddress "dynamic") }}
|
||||
ifconfig-push {{ .ClientAddress }} 255.255.255.0
|
||||
ifconfig-push {{ .ClientAddress }} 255.255.255.255
|
||||
{{- end }}
|
||||
{{- range $route := .CustomRoutes }}
|
||||
push "route {{ $route.Address }} {{ $route.Mask }}" # {{ $route.Description }}
|
195
certificates.go
195
certificates.go
|
@ -1,195 +0,0 @@
|
|||
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
|
||||
}
|
26
client.conf.tpl
Normal file
26
client.conf.tpl
Normal file
|
@ -0,0 +1,26 @@
|
|||
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>
|
|
@ -1,974 +0,0 @@
|
|||
{
|
||||
"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": ""
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
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,36 +7,20 @@ 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 # for openvpn
|
||||
- 8080:8080 # for ovpn-admin because of network_mode
|
||||
- 7777:1194
|
||||
volumes:
|
||||
- ./easyrsa_master:/etc/openvpn/easyrsa
|
||||
- ./ccd_master:/etc/openvpn/ccd
|
||||
ovpn-admin:
|
||||
- ./easyrsa:/etc/openvpn/easyrsa
|
||||
- ./ccd:/etc/openvpn/ccd
|
||||
openvpn-admin:
|
||||
build:
|
||||
context: .
|
||||
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
|
||||
image: openvpn-admin:local
|
||||
command: /app/openvpn-admin --debug --ovpn.network="172.16.100.0/22" --mgmt.host="openvpn"
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- ./easyrsa_master:/mnt/easyrsa
|
||||
- ./ccd_master:/mnt/ccd
|
||||
- ./easyrsa:/mnt/easyrsa
|
||||
- ./ccd:/mnt/ccd
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
|
||||
image="node:16.13.0-alpine3.12"
|
||||
image="node:14.2-alpine3.11"
|
||||
uid="$(id -u $USER)"
|
||||
|
||||
docker run -u $uid -w /app -v $(pwd):/app $image npm i && \
|
||||
|
|
8570
frontend/package-lock.json
generated
8570
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "ovpn-admin",
|
||||
"name": "openvpn-admin",
|
||||
"description": "Vue.js admin ui for openvpn and easyrsa",
|
||||
"version": "1.0.1a",
|
||||
"author": "vitaliy.snurnitsin@gmail.com",
|
||||
|
@ -7,18 +7,13 @@
|
|||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development webpack-dev-server --hot",
|
||||
"build": "cross-env NODE_ENV=production webpack --progress"
|
||||
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
|
||||
},
|
||||
"dependencies": {
|
||||
"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"
|
||||
"axios": "^0.19.2",
|
||||
"vue": "^2.6.12",
|
||||
"vue-clipboard2": "^0.2.1",
|
||||
"vue-good-table": "^2.21.1"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
@ -26,23 +21,23 @@
|
|||
"not ie <= 8"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@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"
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
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'
|
||||
|
||||
Vue.use(VueCookies)
|
||||
import 'vue-good-table/dist/vue-good-table.css'
|
||||
|
||||
Vue.use(VueClipboard)
|
||||
Vue.use(BootstrapVue)
|
||||
Vue.use(Notifications)
|
||||
Vue.use(VueGoodTablePlugin)
|
||||
|
||||
var axios_cfg = function(url, data='', type='form') {
|
||||
|
@ -56,11 +52,6 @@ new Vue({
|
|||
field: 'AccountStatus',
|
||||
filterable: true,
|
||||
},
|
||||
{
|
||||
label: 'Active Connections',
|
||||
field: 'Connections',
|
||||
filterable: true,
|
||||
},
|
||||
{
|
||||
label: 'Expiration Date',
|
||||
field: 'ExpirationDate',
|
||||
|
@ -91,116 +82,42 @@ 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',
|
||||
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"],
|
||||
showWhenStatus: 'Active'
|
||||
},
|
||||
{
|
||||
name: 'u-unrevoke',
|
||||
label: 'Unrevoke',
|
||||
class: 'btn-primary',
|
||||
showWhenStatus: 'Revoked',
|
||||
showForServerRole: ['master'],
|
||||
showForModule: ["core"],
|
||||
showWhenStatus: 'Revoked'
|
||||
},
|
||||
{
|
||||
name: 'u-show-config',
|
||||
label: 'Show config',
|
||||
showWhenStatus: 'Active'
|
||||
},
|
||||
// {
|
||||
// name: 'u-show-config',
|
||||
// label: 'Show config',
|
||||
// class: 'btn-primary',
|
||||
// showWhenStatus: 'Active',
|
||||
// showForServerRole: ['master', 'slave'],
|
||||
// showForModule: ["core"],
|
||||
// },
|
||||
{
|
||||
name: 'u-download-config',
|
||||
label: 'Download config',
|
||||
class: 'btn-info',
|
||||
showWhenStatus: 'Active',
|
||||
showForServerRole: ['master', 'slave'],
|
||||
showForModule: ["core"],
|
||||
showWhenStatus: 'Active'
|
||||
},
|
||||
{
|
||||
name: 'u-edit-ccd',
|
||||
label: 'Edit routes',
|
||||
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"],
|
||||
showWhenStatus: 'Active'
|
||||
}
|
||||
],
|
||||
filters: {
|
||||
hideRevoked: true,
|
||||
hide_revoked: true
|
||||
},
|
||||
serverRole: "master",
|
||||
lastSync: "unknown",
|
||||
modulesEnabled: [],
|
||||
u: {
|
||||
newUserName: '',
|
||||
newUserPassword: '',
|
||||
// newUserPassword: 'nopass',
|
||||
newUserCreateError: '',
|
||||
newPassword: '',
|
||||
passwordChangeStatus: '',
|
||||
passwordChangeMessage: '',
|
||||
rotateUserMessage: '',
|
||||
deleteUserMessage: '',
|
||||
modalNewUserVisible: false,
|
||||
modalShowConfigVisible: false,
|
||||
modalShowCcdVisible: false,
|
||||
modalChangePasswordVisible: false,
|
||||
modalRotateUserVisible: false,
|
||||
modalDeleteUserVisible: false,
|
||||
openvpnConfig: '',
|
||||
ccd: {
|
||||
Name: '',
|
||||
|
@ -215,20 +132,16 @@ new Vue({
|
|||
watch: {
|
||||
},
|
||||
mounted: function () {
|
||||
this.getUserData();
|
||||
this.getServerSetting();
|
||||
this.filters.hideRevoked = this.$cookies.isKey('hideRevoked') ? (this.$cookies.get('hideRevoked') == "true") : false
|
||||
this.u_get_data()
|
||||
},
|
||||
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.getUserData();
|
||||
_this.$notify({title: 'User ' + _this.username + ' revoked!', type: 'warn'})
|
||||
_this.u_get_data();
|
||||
});
|
||||
})
|
||||
_this.$root.$on('u-unrevoke', function () {
|
||||
|
@ -236,20 +149,9 @@ new Vue({
|
|||
data.append('username', _this.username);
|
||||
axios.request(axios_cfg('api/user/unrevoke', data, 'form'))
|
||||
.then(function(response) {
|
||||
_this.getUserData();
|
||||
_this.$notify({title: 'User ' + _this.username + ' unrevoked!', type: 'success'})
|
||||
_this.u_get_data();
|
||||
});
|
||||
})
|
||||
_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();
|
||||
|
@ -281,37 +183,23 @@ new Vue({
|
|||
_this.u.ccd = response.data;
|
||||
});
|
||||
})
|
||||
_this.$root.$on('u-disconnect-user', function () {
|
||||
_this.$root.$on('u-disconnect-use', 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) {
|
||||
console.log(response.data);
|
||||
_this.u.ccd = response.data;
|
||||
});
|
||||
})
|
||||
_this.$root.$on('u-change-password', function () {
|
||||
_this.u.modalChangePasswordVisible = true;
|
||||
var data = new URLSearchParams();
|
||||
data.append('username', _this.username);
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
customAddressDynamic: function () {
|
||||
customAddressEnabled: 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'} : {}
|
||||
},
|
||||
|
@ -321,97 +209,55 @@ 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() {
|
||||
if (this.filters.hideRevoked) {
|
||||
return this.rows.filter(function(account) {
|
||||
return account.AccountStatus == "Active"
|
||||
var _this = this;
|
||||
|
||||
if(_this.filters.hide_revoked) {
|
||||
return _this.rows.filter(function(account) {
|
||||
return account.AccountStatus === "Active";
|
||||
});
|
||||
} else {
|
||||
return this.rows
|
||||
return _this.rows;
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
methods: {
|
||||
rowStyleClassFn: function(row) {
|
||||
if (row.ConnectionStatus == 'Connected') {
|
||||
return 'connected-user'
|
||||
}
|
||||
if (row.AccountStatus == 'Revoked') {
|
||||
return 'revoked-user'
|
||||
}
|
||||
if (row.AccountStatus == 'Expired') {
|
||||
return 'expired-user'
|
||||
}
|
||||
return ''
|
||||
return row.ConnectionStatus == 'Connected' ? 'connected-user' : '' ;
|
||||
},
|
||||
rowActionFn: function(e) {
|
||||
this.username = e.target.dataset.username;
|
||||
this.$root.$emit(e.target.dataset.name);
|
||||
},
|
||||
getUserData: function() {
|
||||
u_get_data: function() {
|
||||
var _this = this;
|
||||
axios.request(axios_cfg('api/users/list'))
|
||||
.then(function(response) {
|
||||
_this.rows = Array.isArray(response.data) ? response.data : [];
|
||||
_this.rows = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
getServerSetting: function() {
|
||||
var _this = this;
|
||||
axios.request(axios_cfg('api/server/settings'))
|
||||
.then(function(response) {
|
||||
_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;
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
createUser: function() {
|
||||
create_user: function() {
|
||||
var _this = this;
|
||||
|
||||
_this.u.newUserCreateError = "";
|
||||
|
||||
var data = new URLSearchParams();
|
||||
data.append('username', _this.u.newUserName);
|
||||
data.append('password', _this.u.newUserPassword);
|
||||
|
||||
_this.username = _this.u.newUserName;
|
||||
// data.append('password', this.u.newUserPassword);
|
||||
|
||||
axios.request(axios_cfg('api/user/create', data, 'form'))
|
||||
.then(function(response) {
|
||||
_this.$notify({title: 'New user ' + _this.username + ' created', type: 'success'})
|
||||
_this.u_get_data();
|
||||
_this.u.modalNewUserVisible = false;
|
||||
_this.u.newUserName = '';
|
||||
_this.u.newUserPassword = '';
|
||||
_this.getUserData();
|
||||
// _this.u.newUserPassword = 'nopass';
|
||||
})
|
||||
.catch(function(error) {
|
||||
_this.u.newUserCreateError = error.response.data;
|
||||
_this.$notify({title: 'New user ' + _this.username + ' creation failed.', type: 'error'})
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
ccdApply: function() {
|
||||
ccd_apply: function() {
|
||||
var _this = this;
|
||||
|
||||
_this.u.ccdApplyStatus = "";
|
||||
|
@ -421,84 +267,11 @@ 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'})
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
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
Normal file
7
frontend/static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/static/css/bootstrap.min.css.map
Normal file
1
frontend/static/css/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
7
frontend/static/css/bootstrap.min.css_
Normal file
7
frontend/static/css/bootstrap.min.css_
Normal file
File diff suppressed because one or more lines are too long
461
frontend/static/css/normalize.css
vendored
Normal file
461
frontend/static/css/normalize.css
vendored
Normal file
|
@ -0,0 +1,461 @@
|
|||
/*! 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;
|
||||
}
|
|
@ -48,14 +48,6 @@ 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;
|
||||
}
|
||||
|
@ -66,7 +58,3 @@ body {
|
|||
background-color: #ffffff;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.ccd-routes {
|
||||
|
||||
}
|
|
@ -2,10 +2,12 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>ovpn-admin</title>
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
<script src="dist/style.min.js"></script>
|
||||
<div id="app">
|
||||
<vue-good-table
|
||||
:columns="columns"
|
||||
|
@ -14,19 +16,18 @@
|
|||
:row-style-class="rowStyleClassFn"
|
||||
:search-options="{ enabled: true}" >
|
||||
<div slot="table-actions">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<div slot="emptystate" class="d-flex justify-content-center">
|
||||
<h4>No users have been created yet.</h4>
|
||||
<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>
|
||||
<button type="button" class="btn btn-sm btn-success el-square" 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 el-square modal-el-margin"
|
||||
class="btn btn-sm btn-success el-square modal-el-margin"
|
||||
type="button"
|
||||
:title="action.label"
|
||||
:data-username="props.row.Identity"
|
||||
|
@ -34,14 +35,17 @@
|
|||
:data-text="action.label"
|
||||
@click.left.stop="rowActionFn"
|
||||
v-for="action in actions"
|
||||
v-bind:class="action.class"
|
||||
v-if="action.showWhenStatus == props.row.AccountStatus && action.showForServerRole.includes(serverRole) && action.showForModule.some(p=> modulesEnabled.includes(p))">
|
||||
v-if="action.showWhenStatus == props.row.AccountStatus">
|
||||
{{ 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">
|
||||
|
@ -50,7 +54,7 @@
|
|||
</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" v-if="modulesEnabled.includes('passwdAuth')">
|
||||
<!-- <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">-->
|
||||
</div>
|
||||
|
||||
<div class="modal-footer justify-content-center" v-if="u.newUserCreateError.length > 0">
|
||||
|
@ -59,41 +63,18 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="createUser()">Create</button>
|
||||
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="create_user();">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: <strong>{{ username }}</strong></h4>
|
||||
<h4>ovpn config for {{ username }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="d-flex">
|
||||
|
@ -114,57 +95,42 @@
|
|||
<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">
|
||||
<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>
|
||||
<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>
|
||||
</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 ccd-routes" >
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Address</th>
|
||||
<th scope="col">Mask</th>
|
||||
<th scope="col">Description</th>
|
||||
<th scope="col" v-if="serverRole == 'master'">Action</th>
|
||||
<th scope="col">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>
|
||||
<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>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="serverRole == 'master'">
|
||||
<tr>
|
||||
<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 class="text-right" v-if="serverRole == 'master'">
|
||||
<td>
|
||||
<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>
|
||||
|
@ -178,59 +144,14 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<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-success el-square modal-el-margin" v-on:click.stop="ccd_apply()">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/bundle.min.js"></script>
|
||||
<script src="dist/build.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,24 +1,14 @@
|
|||
const path = require('path');
|
||||
//const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
var path = require('path')
|
||||
var webpack = require('webpack')
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
mode: 'production',
|
||||
entry: {
|
||||
bundle: [
|
||||
'./src/main.js',
|
||||
],
|
||||
style: [
|
||||
'./src/style.js',
|
||||
]
|
||||
},
|
||||
entry: './src/main.js',
|
||||
output: {
|
||||
path: path.resolve(__dirname, './static/dist'),
|
||||
publicPath: '/dist/',
|
||||
filename: '[name].min.js'
|
||||
filename: 'build.js'
|
||||
},
|
||||
plugins: [
|
||||
//new BundleAnalyzerPlugin(),
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
|
@ -29,22 +19,88 @@ module.exports = {
|
|||
],
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
//exclude: /node_modules\/(?!bootstrap-vue\/src\/)/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-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: {
|
||||
presets: ['@babel/preset-env']
|
||||
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',
|
||||
//'bootstrap-vue$': 'bootstrap-vue/src/index.js'
|
||||
'vue$': 'vue/dist/vue.esm.js'
|
||||
},
|
||||
extensions: ['*', '.js', '.vue', '.json']
|
||||
},
|
||||
devServer: {
|
||||
historyApiFallback: true,
|
||||
noInfo: true,
|
||||
overlay: true
|
||||
},
|
||||
performance: {
|
||||
hints: false
|
||||
},
|
||||
devtool: '#eval-source-map'
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports.devtool = '#source-map'
|
||||
// http://vue-loader.vuejs.org/en/workflow/production.html
|
||||
module.exports.plugins = (module.exports.plugins || []).concat([
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
NODE_ENV: '"production"'
|
||||
}
|
||||
}),
|
||||
new TerserPlugin({
|
||||
sourceMap: true
|
||||
}),
|
||||
new webpack.LoaderOptionsPlugin({
|
||||
minimize: true
|
||||
})
|
||||
])
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
#!/bin/bash
|
||||
|
||||
mkdir -p {easyrsa,ccd}
|
||||
if [ ! -d easyrsa ]; then
|
||||
mkdir easyrsa
|
||||
fi
|
||||
|
||||
cd easyrsa
|
||||
|
||||
|
@ -13,6 +15,6 @@ if [ -d pki ]; then
|
|||
fi
|
||||
|
||||
./easyrsa init-pki
|
||||
echo "ca" | ./easyrsa build-ca nopass
|
||||
echo "ca\n" | ./easyrsa build-ca nopass
|
||||
./easyrsa build-server-full server nopass
|
||||
./easyrsa build-client-full client nopass
|
60
go.mod
60
go.mod
|
@ -1,61 +1,9 @@
|
|||
module ovpn-admin
|
||||
module openvpn-web-ui
|
||||
|
||||
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
|
||||
)
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-logr/logr v1.2.2 // indirect
|
||||
github.com/gobuffalo/envy v1.10.1 // indirect
|
||||
github.com/gobuffalo/logger v1.0.6 // indirect
|
||||
github.com/gobuffalo/packd v1.0.1 // indirect
|
||||
github.com/gobuffalo/packr v1.30.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-cmp v0.5.6 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/googleapis/gnostic v0.5.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/karrick/godirwalk v1.16.1 // indirect
|
||||
github.com/markbates/errx v1.1.0 // indirect
|
||||
github.com/markbates/oncer v1.0.0 // indirect
|
||||
github.com/markbates/safe v1.0.1 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.32.1 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 // indirect
|
||||
golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
k8s.io/api v0.23.1 // indirect
|
||||
k8s.io/klog/v2 v2.40.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20220114203427-a0453230fd26 // indirect
|
||||
k8s.io/utils v0.0.0-20211208161948-7d6a63dca704 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
)
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
name: ovpn-admin
|
||||
version: 1.0.0
|
|
@ -1 +0,0 @@
|
|||
helm chart example
|
|
@ -1,88 +0,0 @@
|
|||
{{ $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
|
|
@ -1,117 +0,0 @@
|
|||
---
|
||||
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
|
|
@ -1,39 +0,0 @@
|
|||
---
|
||||
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
|
|
@ -1,36 +0,0 @@
|
|||
---
|
||||
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
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: basic-auth
|
||||
type: Opaque
|
||||
data:
|
||||
auth: {{ print .Values.ovpnAdmin.basicAuth.user ":{PLAIN}" .Values.ovpnAdmin.basicAuth.password | b64enc | quote }}
|
|
@ -1,57 +0,0 @@
|
|||
---
|
||||
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 }}
|
|
@ -1,26 +0,0 @@
|
|||
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
306
helpers.go
|
@ -1,306 +0,0 @@
|
|||
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
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 179 KiB |
Binary file not shown.
Before Width: | Height: | Size: 54 KiB |
|
@ -1,12 +0,0 @@
|
|||
#!/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
|
|
@ -1,12 +0,0 @@
|
|||
#!/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
776
kubernetes.go
|
@ -1,776 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
PATH=$PATH:/usr/local/bin
|
||||
set -e
|
||||
|
||||
env
|
||||
|
||||
auth_usr=$(head -1 $1)
|
||||
auth_passwd=$(tail -1 $1)
|
||||
|
||||
if [ $common_name = $auth_usr ]; then
|
||||
openvpn-user auth --db.path /etc/openvpn/easyrsa/pki/users.db --user ${auth_usr} --password ${auth_passwd}
|
||||
else
|
||||
echo "Authorization failed"
|
||||
exit 1
|
||||
fi
|
|
@ -1,59 +0,0 @@
|
|||
#!/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 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
./start.sh
|
||||
docker-compose -p openvpn-slave -f docker-compose-slave.yaml up -d
|
3
start.sh
3
start.sh
|
@ -1,3 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
docker compose -p openvpn-master up -d --build
|
|
@ -1,38 +0,0 @@
|
|||
{{- 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,10 +1,120 @@
|
|||
project: ovpn-admin
|
||||
project: openvpn-web-ui
|
||||
configVersion: 1
|
||||
deploy:
|
||||
helmRelease: "[[ project ]]-[[ env ]]"
|
||||
namespace: "[[ project ]]-[[ env ]]"
|
||||
|
||||
---
|
||||
image: ovpn-admin
|
||||
dockerfile: Dockerfile
|
||||
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: openvpn
|
||||
dockerfile: Dockerfile.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
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue