1
0
Fork 0
mirror of synced 2024-06-26 10:51:09 -04:00

Compare commits

..

No commits in common. "master" and "1.3" have entirely different histories.
master ... 1.3

56 changed files with 6890 additions and 8526 deletions

View file

@ -3,7 +3,7 @@
*.iml
out
gen
.github
easyrsa
easyrsa_master
@ -13,14 +13,6 @@ 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 }}

View file

@ -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 }}

12
.gitignore vendored
View file

@ -4,18 +4,14 @@ 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
*.sln

43
.werffiles/configure.sh Normal file
View file

@ -0,0 +1,43 @@
#!/usr/bin/env 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
if [ ${OPVN_ROLE:-"master"} = "slave" ]; then
echo "Waiting for syncing data from master"
while [ $(wget -q localhost/api/sync/last -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 -A POSTROUTING -s 172.16.100.0/255.255.255.0 ! -d 172.16.100.0/255.255.255.0 -j MASQUERADE
mkdir -p /dev/net
if [ ! -c /dev/net/tun ]; then
mknod /dev/net/tun c 10 200
fi
cp -f /etc/openvpn/setup/openvpn.conf /etc/openvpn/openvpn.conf
[ -d $EASY_RSA_LOC/pki ] && chmod 755 $EASY_RSA_LOC/pki
[ -f $EASY_RSA_LOC/pki/crl.pem ] && chmod 644 $EASY_RSA_LOC/pki/crl.pem
mkdir -p /etc/openvpn/ccd
openvpn --config /etc/openvpn/openvpn.conf --client-config-dir /etc/openvpn/ccd

View file

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

View file

@ -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

View file

@ -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
RUN chmod +x /etc/openvpn/setup/configure.sh
COPY .werffiles /etc/openvpn/setup
RUN chmod +x /etc/openvpn/setup/configure.sh

178
README.md
View file

@ -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. Your contributions are welcome!
***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

View file

@ -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

View file

@ -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

View file

@ -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 }}

View file

@ -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
}

29
client.conf.tpl Normal file
View file

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

View file

@ -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": ""
}

View file

@ -8,22 +8,22 @@ services:
image: openvpn:local
command: /etc/openvpn/setup/configure.sh
environment:
- OVPN_ROLE=slave
- OPVN_ROLE=slave
cap_add:
- NET_ADMIN
ports:
- 7778:1194 # for openvpn
- 8081:8080 # for ovpn-admin because of network_mode
- 8081:8080 # for openvpn-admin because of network_mode
volumes:
- ./easyrsa_slave:/etc/openvpn/easyrsa
- ./ccd_slave:/etc/openvpn/ccd
ovpn-admin:
openvpn-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"
image: openvpn-admin:local
command: /app/openvpn-admin --debug --ovpn.network="172.16.100.0/22" --master.sync-token="TOKEN" --master.host="http://172.20.0.1:8080" --role="slave" --ovpn.host="127.0.0.1:7744" --ovpn.host="127.0.0.1:7778"
environment:
- OVPN_SLAVE=1
- OPVN_SLAVE=1
network_mode: service:openvpn
volumes:
- ./easyrsa_slave:/mnt/easyrsa

View file

@ -7,35 +7,19 @@ 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
- 8080:8080 # for openvpn-admin because of network_mode
volumes:
- ./easyrsa_master:/etc/openvpn/easyrsa
- ./ccd_master:/etc/openvpn/ccd
ovpn-admin:
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"
image: openvpn-admin:local
command: /app/openvpn-admin --debug --ovpn.network="172.16.100.0/22" --master.sync-token="TOKEN"
network_mode: service:openvpn
volumes:
- ./easyrsa_master:/mnt/easyrsa

View file

@ -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 && \

File diff suppressed because it is too large Load diff

View file

@ -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,14 @@
"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",
"axios": "^0.19.2",
"vue": "^2.6.12",
"vue-clipboard2": "^0.2.1",
"vue-cookies": "^1.7.4",
"vue-good-table": "^2.21.11",
"vue-notification": "^1.3.20",
"vue-style-loader": "^4.1.3"
"vue-good-table": "^2.21.1"
},
"browserslist": [
"> 1%",
@ -26,23 +22,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"
}
}

View file

@ -1,16 +1,14 @@
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)
Vue.use(VueCookies)
var axios_cfg = function(url, data='', type='form') {
if (data == '') {
@ -56,11 +54,6 @@ new Vue({
field: 'AccountStatus',
filterable: true,
},
{
label: 'Active Connections',
field: 'Connections',
filterable: true,
},
{
label: 'Expiration Date',
field: 'ExpirationDate',
@ -91,93 +84,41 @@ 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"],
showForServerRole: ['master']
},
{
name: 'u-unrevoke',
label: 'Unrevoke',
class: 'btn-primary',
showWhenStatus: 'Revoked',
showForServerRole: ['master'],
showForModule: ["core"],
showForServerRole: ['master']
},
{
name: 'u-show-config',
label: 'Show config',
showWhenStatus: 'Active',
showForServerRole: ['master', 'slave']
},
// {
// name: 'u-show-config',
// label: 'Show config',
// class: 'btn-primary',
// showWhenStatus: 'Active',
// showForServerRole: ['master', 'slave'],
// showForModule: ["core"],
// },
{
name: 'u-download-config',
label: 'Download config',
class: 'btn-info',
showWhenStatus: 'Active',
showForServerRole: ['master', 'slave'],
showForModule: ["core"],
showForServerRole: ['master', 'slave']
},
{
name: 'u-edit-ccd',
label: 'Edit routes',
class: 'btn-primary',
showWhenStatus: 'Active',
showForServerRole: ['master'],
showForModule: ["ccd"],
showForServerRole: ['master']
},
{
name: 'u-edit-ccd',
label: 'Show routes',
class: 'btn-primary',
showWhenStatus: 'Active',
showForServerRole: ['slave'],
showForModule: ["ccd"],
showForServerRole: ['slave']
}
],
filters: {
@ -185,22 +126,13 @@ new Vue({
},
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: '',
@ -216,7 +148,7 @@ new Vue({
},
mounted: function () {
this.getUserData();
this.getServerSetting();
this.getServerRole();
this.filters.hideRevoked = this.$cookies.isKey('hideRevoked') ? (this.$cookies.get('hideRevoked') == "true") : false
},
created() {
@ -228,7 +160,6 @@ new Vue({
axios.request(axios_cfg('api/user/revoke', data, 'form'))
.then(function(response) {
_this.getUserData();
_this.$notify({title: 'User ' + _this.username + ' revoked!', type: 'warn'})
});
})
_this.$root.$on('u-unrevoke', function () {
@ -237,19 +168,8 @@ new Vue({
axios.request(axios_cfg('api/user/unrevoke', data, 'form'))
.then(function(response) {
_this.getUserData();
_this.$notify({title: 'User ' + _this.username + ' unrevoked!', type: 'success'})
});
})
_this.$root.$on('u-rotate', function () {
_this.u.modalRotateUserVisible = true;
var data = new URLSearchParams();
data.append('username', _this.username);
})
_this.$root.$on('u-delete', function () {
_this.u.modalDeleteUserVisible = true;
var data = new URLSearchParams();
data.append('username', _this.username);
})
_this.$root.$on('u-show-config', function () {
_this.u.modalShowConfigVisible = true;
var data = new URLSearchParams();
@ -287,31 +207,17 @@ new Vue({
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 () {
return this.u.ccd.ClientAddress == "dynamic"
customAddressDisabled: function () {
return this.serverRole == "master" ? this.u.ccd.ClientAddress == "dynamic" : true
},
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,15 +227,6 @@ 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"
},
@ -346,16 +243,7 @@ new Vue({
},
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;
@ -365,26 +253,22 @@ new Vue({
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() {
getServerRole: function() {
var _this = this;
axios.request(axios_cfg('api/server/settings'))
axios.request(axios_cfg('api/server/role'))
.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'))
axios.request(axios_cfg('api/sync/last'))
.then(function(response) {
_this.lastSync = response.data;
});
}
});
},
createUser: function() {
var _this = this;
@ -392,25 +276,19 @@ new Vue({
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.getUserData();
_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() {
var _this = this;
@ -421,84 +299,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'})
})
},
}
}
})

View file

@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

461
frontend/static/css/normalize.css vendored Normal file
View 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;
}

View file

@ -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 {
}

View file

@ -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,7 +16,7 @@
: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>
<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>
<b-badget class="btn btn-sm btn-info el-square" v-if="serverRole == 'slave'">Slave - last sync: {{ lastSync }}</b-badget>
<button type="button" class="btn btn-sm btn-secondary el-square" v-on:click.stop="filters.hideRevoked=!filters.hideRevoked;this.$cookies.set('hideRevoked',!(this.$cookies.get('hideRevoked') == 'true'), -1);">{{ revokeFilterText }}</button>
</div>
@ -26,7 +28,7 @@
<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,23 +36,26 @@
: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.showForServerRole.includes(serverRole)">
{{ action.label }}
</button>
</span>
</template>
</vue-good-table>
<!-- <div class="d-flex justify-content-md-end">-->
<!-- <button type="button" class="btn btn-sm btn-success el-square new-user-btn" v-on:click.stop="u.ctxVisible=false;u.modalNewUserVisible=true">Add user</button>-->
<!-- </div>-->
<div class="modal-wrapper" v-if="u.modalNewUserVisible" v-bind:style="modalNewUserDisplay">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4>Add new user</h4>
<h4>Add new user </h4>
</div>
<div class="modal-body">
<input type="text" class="form-control el-square modal-el-margin" placeholder="Username [_a-zA-Z0-9\.-]" v-model="u.newUserName">
<input type="password" class="form-control el-square modal-el-margin" minlength="6" autocomplete="off" placeholder="Password [_a-zA-Z0-9\.-]" v-model="u.newUserPassword" 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 +64,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="createUser();">Create</button>
<button type="button" class="btn btn-primary el-square d-flex justify-content-sm-end modal-el-margin" v-on:click.stop="u.newUserName='';u.newUserPassword='nopass';u.modalNewUserVisible=false">Close</button>
</div>
</div>
</div>
</div>
<div class="modal-wrapper" v-if="u.modalChangePasswordVisible" v-bind:style="modalChangePasswordDisplay">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4>Change password for: <strong>{{ username }}</strong></h4>
</div>
<div class="modal-body">
<input type="password" class="form-control el-square modal-el-margin" minlength="6" autocomplete="off" placeholder="Password [_a-zA-Z0-9\.-]" v-model="u.newPassword">
</div>
<div class="modal-footer justify-content-center" v-if="u.passwordChangeMessage.length > 0">
<div class="alert" v-bind:class="passwordChangeStatusCssClass" role="alert" >
{{ u.passwordChangeMessage }}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="changeUserPassword(username)">Change password</button>
<button type="button" class="btn btn-primary el-square d-flex justify-content-sm-end modal-el-margin" v-on:click.stop="u.newPassword='';u.passwordChangeMessage='';u.modalChangePasswordVisible=false">Close</button>
</div>
</div>
</div>
</div>
<div class="modal-wrapper" v-if="u.modalShowConfigVisible" v-bind:style="modalShowConfigDisplay">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4>ovpn config for: <strong>{{ username }}</strong></h4>
<h4>ovpn config for {{ username }}</h4>
</div>
<div class="modal-body">
<div class="d-flex">
@ -114,57 +96,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-if="serverRole == 'master'" v-bind:checked="customAddressDisabled">
</div>
</div>
<input id="staticAddress" type="text" class="form-control" v-model="u.ccd.ClientAddress" placeholder="127.0.0.1" v-bind:disabled="customAddressDisabled">
</div>
</div>
<div 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-if="serverRole == 'master'" v-on:click.stop="u.ccd.CustomRoutes.splice(index, 1)">Delete</button>
</td>
</tr>
<tr v-if="serverRole == 'master'">
<td><input type="text" v-model="u.newRoute.Address"/></td>
<td><input type="text" v-model="u.newRoute.Mask"/></td>
<td><input type="text" v-model="u.newRoute.Description"/></td>
<td 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>
@ -185,52 +152,7 @@
</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>

View file

@ -1,50 +1,106 @@
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',
]
},
output: {
path: path.resolve(__dirname, './static/dist'),
publicPath: '/dist/',
filename: '[name].min.js'
},
plugins: [
//new BundleAnalyzerPlugin(),
],
module: {
rules: [
{
test: /\.css$/,
use: [
'vue-style-loader',
'css-loader'
],
},
{
test: /\.js$/,
//exclude: /node_modules\/(?!bootstrap-vue\/src\/)/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
},
],
},
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js',
//'bootstrap-vue$': 'bootstrap-vue/src/index.js'
entry: './src/main.js',
output: {
path: path.resolve(__dirname, './static/dist'),
publicPath: '/dist/',
filename: 'build.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'vue-style-loader',
'css-loader'
],
},
extensions: ['*', '.js', '.vue', '.json']
{
test: /\.scss$/,
use: [
'vue-style-loader',
'css-loader',
'sass-loader'
],
},
{
test: /\.sass$/,
use: [
'vue-style-loader',
'css-loader',
'sass-loader?indentedSyntax'
],
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
// Since sass-loader (weirdly) has SCSS as its default parse mode, we map
// the "scss" and "sass" values for the lang attribute to the right configs here.
// other preprocessors should work out of the box, no loader config like this necessary.
'scss': [
'vue-style-loader',
'css-loader',
'sass-loader'
],
'sass': [
'vue-style-loader',
'css-loader',
'sass-loader?indentedSyntax'
]
}
// other vue-loader options go here
}
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: 'file-loader',
options: {
name: '[name].[ext]?[hash]'
}
}
]
},
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js'
},
}
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
})
])
}

View file

@ -1,6 +1,6 @@
#!/usr/bin/env bash
#!/bin/bash
mkdir -p {easyrsa,ccd}
mkdir -p easyrsa
cd easyrsa

63
go.mod
View file

@ -1,61 +1,12 @@
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/Electronn/openvpn_exporter v0.0.0-20181005212047-37f639dc9c7d
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
github.com/prometheus/client_golang v1.8.0
github.com/prometheus/common v0.15.0 // indirect
gopkg.in/alecthomas/kingpin.v2 v2.2.6
)

655
go.sum

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 }}

View file

@ -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 }}

View file

@ -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

View file

@ -1,43 +1,30 @@
package main
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
log "github.com/sirupsen/logrus"
)
func parseDate(layout, datetime string) time.Time {
func indexTxtDateToHumanReadable(datetime string) string {
layout := "060102150405Z"
t, err := time.Parse(layout, datetime)
if err != nil {
log.Errorln(err)
log.Println(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()
return t.Format("2006-01-02 15:04:05")
}
func runBash(script string) string {
log.Debugln(script)
fmt.Println(script)
cmd := exec.Command("bash", "-c", script)
stdout, err := cmd.CombinedOutput()
if err != nil {
return fmt.Sprint(err) + " : " + string(stdout)
return (fmt.Sprint(err) + " : " + string(stdout))
}
return string(stdout)
}
@ -48,7 +35,7 @@ func fExist(path string) bool {
if os.IsNotExist(err) {
return false
} else if err != nil {
log.Fatalf("fExist: %s", err)
log.Fatal(err)
return false
}
@ -58,102 +45,37 @@ func fExist(path string) bool {
func fRead(path string) string {
content, err := ioutil.ReadFile(path)
if err != nil {
log.Warning(err)
return ""
log.Fatal(err)
}
return string(content)
}
func fCreate(path string) error {
func fCreate(path string) bool {
var _, err = os.Stat(path)
if os.IsNotExist(err) {
var file, err = os.Create(path)
if err != nil {
log.Errorln(err)
return err
log.Println(err)
return false
}
defer file.Close()
}
return nil
return true
}
func fWrite(path, content string) error {
func fWrite(path, content string) {
err := ioutil.WriteFile(path, []byte(content), 0644)
if err != nil {
log.Fatal(err)
}
return nil
}
func fDelete(path string) error {
func fDelete(path string) {
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 {
@ -169,7 +91,7 @@ func fDownload(path, url string, basicAuth bool) error {
}
if resp.StatusCode != 200 {
log.Warnf("WARNING: Download file operation for url %s finished with status code %d\n", url, resp.StatusCode)
log.Printf("WARNING: Download file operation for url %s finished with status code %d", url, resp.StatusCode )
}
defer resp.Body.Close()
@ -183,124 +105,3 @@ func fDownload(path, url string, basicAuth bool) error {
return nil
}
func createArchiveFromDir(dir, path string) error {
var files []string
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Warn(err)
return err
}
if !info.IsDir() {
files = append(files, path)
}
return nil
})
if err != nil {
log.Warn(err)
}
out, err := os.Create(path)
if err != nil {
log.Errorf("Error writing archive %s: %s", path, err)
return err
}
defer out.Close()
gw := gzip.NewWriter(out)
defer gw.Close()
tw := tar.NewWriter(gw)
defer tw.Close()
// Iterate over files and add them to the tar archive
for _, filePath := range files {
file, err := os.Open(filePath)
if err != nil {
log.Warnf("Error writing archive %s: %s", path, err)
return err
}
// Get FileInfo about our file providing file size, mode, etc.
info, err := file.Stat()
if err != nil {
file.Close()
return err
}
// Create a tar Header from the FileInfo data
header, err := tar.FileInfoHeader(info, info.Name())
if err != nil {
file.Close()
return err
}
header.Name = strings.Replace(filePath, dir+"/", "", 1)
// Write file header to the tar archive
err = tw.WriteHeader(header)
if err != nil {
file.Close()
return err
}
// Copy file content to tar archive
_, err = io.Copy(tw, file)
if err != nil {
file.Close()
return err
}
file.Close()
}
return nil
}
func extractFromArchive(archive, path string) error {
// Open the file which will be written into the archive
file, err := os.Open(archive)
if err != nil {
return err
}
defer file.Close()
// Write file header to the tar archive
uncompressedStream, err := gzip.NewReader(file)
if err != nil {
log.Fatal("extractFromArchive(): NewReader failed")
}
tarReader := tar.NewReader(uncompressedStream)
for true {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("extractFromArchive: Next() failed: %s", err.Error())
}
switch header.Typeflag {
case tar.TypeDir:
if err := os.Mkdir(path+"/"+header.Name, 0755); err != nil {
log.Fatalf("extractFromArchive: Mkdir() failed: %s", err.Error())
}
case tar.TypeReg:
outFile, err := os.Create(path + "/" + header.Name)
if err != nil {
log.Fatalf("extractFromArchive: Create() failed: %s", err.Error())
}
if _, err := io.Copy(outFile, tarReader); err != nil {
log.Fatalf("extractFromArchive: Copy() failed: %s", err.Error())
}
outFile.Close()
default:
log.Fatalf(
"extractFromArchive: uknown type: %s in %s", header.Typeflag, header.Name)
}
}
return nil
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

View file

@ -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

View file

@ -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

View file

@ -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
}

1629
main.go

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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}

View file

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

View file

@ -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
View file

@ -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