mirror of
https://github.com/flant/ovpn-admin.git
synced 2026-02-04 01:10:22 -08:00
Compare commits
No commits in common. "master" and "1.7.2" have entirely different histories.
@ -3,7 +3,7 @@
|
||||
*.iml
|
||||
out
|
||||
gen
|
||||
.github
|
||||
|
||||
|
||||
easyrsa
|
||||
easyrsa_master
|
||||
@ -13,7 +13,6 @@ ccd_master
|
||||
ccd_slave
|
||||
werf.yaml
|
||||
frontend/node_modules
|
||||
frontend/static/dist
|
||||
openvpn-web-ui
|
||||
openvpn-ui
|
||||
openvpn-admin
|
||||
@ -22,5 +21,3 @@ ovpn-admin
|
||||
docker-compose.yaml
|
||||
docker-compose-slave.yaml
|
||||
img
|
||||
dashboard
|
||||
.helm
|
||||
|
||||
25
.github/dependabot.yml
vendored
25
.github/dependabot.yml
vendored
@ -1,25 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Dependencies listed in go.mod
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
# Dependencies listed in .github/workflows/*.yml
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
# Dependencies listed in Dockerfile
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
# Dependencies listed in frontend/package.json
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
17
.github/release.yml
vendored
17
.github/release.yml
vendored
@ -1,17 +0,0 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- ignore
|
||||
categories:
|
||||
- title: Enhancements 🚀
|
||||
labels:
|
||||
- enhancement
|
||||
- title: Bug Fixes 🐛
|
||||
labels:
|
||||
- bug
|
||||
- title: Dependency Updates ⬆️
|
||||
labels:
|
||||
- dependencies
|
||||
- title: Other Changes
|
||||
labels:
|
||||
- "*"
|
||||
35
.github/workflows/chart-release.yml
vendored
35
.github/workflows/chart-release.yml
vendored
@ -1,35 +0,0 @@
|
||||
name: Release Charts
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'charts/**'
|
||||
|
||||
jobs:
|
||||
chart-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "$GITHUB_ACTOR"
|
||||
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: v3.7.1
|
||||
|
||||
- name: Run chart-releaser
|
||||
uses: helm/chart-releaser-action@v1.7.0
|
||||
with:
|
||||
charts_dir: charts
|
||||
config: charts/cr.yaml
|
||||
env:
|
||||
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
107
.github/workflows/chart-test.yaml
vendored
107
.github/workflows/chart-test.yaml
vendored
@ -1,107 +0,0 @@
|
||||
name: Chart Test
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
chart:
|
||||
name: Chart
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
changed: ${{ steps.changes.outputs.changed }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: v3.10.3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Set up chart-testing
|
||||
uses: helm/chart-testing-action@v2.6.1
|
||||
|
||||
- name: Lint
|
||||
run: ct lint
|
||||
|
||||
- name: Check generated docs
|
||||
run: |
|
||||
make docs
|
||||
test "$(git diff --name-only)" == "" \
|
||||
|| ( printf >&2 "\nREADME files are not up to date (run 'make docs'), differences:\n\n%s\n\n" "$(git diff)" ; exit 1 ; )
|
||||
|
||||
- name: Detect changes
|
||||
id: changes
|
||||
run: |
|
||||
changed=$(ct list-changed)
|
||||
if [[ -n "$changed" ]]; then
|
||||
echo "changed=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
chart-test:
|
||||
name: Chart Test
|
||||
runs-on: ubuntu-latest
|
||||
needs: chart
|
||||
if: needs.chart.outputs.changed == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
kube: ["1.25", "1.29", "1.31"]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: v3.10.3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Set up chart-testing
|
||||
uses: helm/chart-testing-action@v2.6.1
|
||||
|
||||
# See https://github.com/kubernetes-sigs/kind/releases/tag/v0.17.0
|
||||
- name: Determine KinD node image version
|
||||
id: node_image
|
||||
run: |
|
||||
case ${{ matrix.kube }} in
|
||||
1.25)
|
||||
NODE_IMAGE=kindest/node:v1.25.3@sha256:f52781bc0d7a19fb6c405c2af83abfeb311f130707a0e219175677e366cc45d1 ;;
|
||||
1.29)
|
||||
NODE_IMAGE=kindest/node:v1.29.12@sha256:62c0672ba99a4afd7396512848d6fc382906b8f33349ae68fb1dbfe549f70dec ;;
|
||||
1.31)
|
||||
NODE_IMAGE=kindest/node:v1.31.2@sha256:0526eb5cd8d892ed79b56feb48d17eeee1f719f55d5c35cef468f053caffad32 ;;
|
||||
esac
|
||||
|
||||
echo "image=$NODE_IMAGE" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create KinD cluster
|
||||
uses: helm/kind-action@v1.13.0
|
||||
with:
|
||||
version: v0.17.0
|
||||
node_image: ${{ steps.node_image.outputs.image }}
|
||||
|
||||
- name: Test
|
||||
run: ct install
|
||||
30
.github/workflows/publish-latest.yaml
vendored
Normal file
30
.github/workflows/publish-latest.yaml
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
name: Build and publish to Docker Hub (releases only)
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build latest images for relase
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Push openvpn image to Docker Hub
|
||||
uses: docker/build-push-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASS }}
|
||||
repository: flant/ovpn-admin
|
||||
tags: openvpn-latest
|
||||
dockerfile: Dockerfile.openvpn
|
||||
- name: Push ovpn-admin image to Docker Hub
|
||||
uses: docker/build-push-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASS }}
|
||||
repository: flant/ovpn-admin
|
||||
tags: latest
|
||||
dockerfile: Dockerfile
|
||||
67
.github/workflows/publish-tag.yaml
vendored
67
.github/workflows/publish-tag.yaml
vendored
@ -1,59 +1,34 @@
|
||||
name: Build and publish tags to ghcr.io
|
||||
name: Build and publish to Docker Hub (tags only)
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
WERF_STAGED_DOCKERFILE_VERSION: v2
|
||||
WERF_BUILDAH_MODE: auto
|
||||
WERF_ENV: ${{ (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/')) && 'release' || 'pr' }}
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build images for tag
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install werf
|
||||
uses: werf/actions/install@v2
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Get the version
|
||||
id: get_version
|
||||
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
|
||||
- name: Push openvpn image to Docker Hub
|
||||
uses: docker/build-push-action@v1
|
||||
with:
|
||||
platforms: linux/amd64, linux/arm64, linux/arm/v7, linux/arm/v8
|
||||
|
||||
- name: Login into ghcr.io
|
||||
shell: bash
|
||||
run: werf cr login -u ${{ github.actor }} -p ${{ github.token }} ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
uses: docker/metadata-action@v5.10.0
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASS }}
|
||||
repository: flant/ovpn-admin
|
||||
tags: openvpn-${{ steps.get_version.outputs.VERSION }}
|
||||
dockerfile: Dockerfile.openvpn
|
||||
- name: Push ovpn-admin image to Docker Hub
|
||||
uses: docker/build-push-action@v1
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}/${{ matrix.name }}
|
||||
|
||||
- name: Build Image
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
run: |
|
||||
source "$(werf ci-env github --as-file)"
|
||||
source <(jq -r '.labels | to_entries | to_entries[] | "export WERF_EXPORT_ADD_LABEL_\(.key)=\"\(.value.key)=\(.value.value)\""' <<< $DOCKER_METADATA_OUTPUT_JSON)
|
||||
|
||||
werf build --repo='' --final-repo='' --secondary-repo "$WERF_REPO" --env "$WERF_ENV"
|
||||
|
||||
- name: Build and Push Image
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
run: |
|
||||
source "$(werf ci-env github --as-file)"
|
||||
source <(jq -r '.labels | to_entries | to_entries[] | "export WERF_EXPORT_ADD_LABEL_\(.key)=\"\(.value.key)=\(.value.value)\""' <<< $DOCKER_METADATA_OUTPUT_JSON)
|
||||
|
||||
werf export --tag ghcr.io/${{ github.repository }}/%image%:${{ github.ref_name }} --env "$WERF_ENV"
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASS }}
|
||||
repository: flant/ovpn-admin
|
||||
tags: ${{ steps.get_version.outputs.VERSION }}
|
||||
dockerfile: Dockerfile
|
||||
|
||||
17
.github/workflows/release.yaml
vendored
17
.github/workflows/release.yaml
vendored
@ -1,4 +1,3 @@
|
||||
name: Build and publish binaries (releases only)
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
@ -7,23 +6,21 @@ jobs:
|
||||
releases-matrix:
|
||||
name: Release Go Binary
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CGO_ENABLED: 1
|
||||
strategy:
|
||||
matrix:
|
||||
# build and publish in parallel: linux/386, linux/amd64, windows/386, windows/amd64, darwin/386, darwin/amd64
|
||||
goos: [linux]
|
||||
goarch: ["386", "amd64"]
|
||||
goarch: ["386", amd64]
|
||||
steps:
|
||||
- name: checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v2
|
||||
- name: build binaries
|
||||
uses: wangyoucao577/go-release-action@v1.55
|
||||
uses: wangyoucao577/go-release-action@v1.20
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
goversion: 1.23
|
||||
goos: ${{ matrix.goos }}
|
||||
goarch: ${{ matrix.goarch }}
|
||||
build_command: bash -ex ./build.sh
|
||||
pre_command: bash -ex ./install-deps.sh
|
||||
ldflags: '-linkmode external -extldflags "-static" -s -w'
|
||||
pre_command: bash ./install-deps.sh
|
||||
binary_name: "ovpn-admin"
|
||||
asset_name: ovpn-admin-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
asset_name: ovpn-admin-${{ matrix.goos }}-${{ matrix.goarch }}.
|
||||
29
.github/workflows/release_arm.yaml
vendored
29
.github/workflows/release_arm.yaml
vendored
@ -1,29 +0,0 @@
|
||||
name: Build and publish arm binaries (releases only)
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
releases-matrix:
|
||||
name: Release Go Binary
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CGO_ENABLED: 1
|
||||
strategy:
|
||||
matrix:
|
||||
goos: [linux]
|
||||
goarch: ["arm", "arm64"]
|
||||
steps:
|
||||
- name: checkout code
|
||||
uses: actions/checkout@v6
|
||||
- name: build binaries
|
||||
uses: wangyoucao577/go-release-action@v1.55
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
goversion: 1.23
|
||||
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 }}
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,11 +1,13 @@
|
||||
bin/
|
||||
|
||||
easyrsa
|
||||
easyrsa_master
|
||||
easyrsa_slave
|
||||
ccd
|
||||
ccd_master
|
||||
ccd_slave
|
||||
openvpn-web-ui
|
||||
openvpn-ui
|
||||
openvpn-admin
|
||||
ovpn-admin
|
||||
frontend/node_modules
|
||||
|
||||
main-packr.go
|
||||
|
||||
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@ -0,0 +1,17 @@
|
||||
FROM node:14.2-alpine3.11 AS frontend-builder
|
||||
COPY frontend/ /app
|
||||
RUN cd /app && npm install && npm run build
|
||||
|
||||
FROM golang:1.14.2-buster AS backend-builder
|
||||
RUN go get -u github.com/gobuffalo/packr/v2/packr2
|
||||
COPY --from=frontend-builder /app/static /app/frontend/static
|
||||
COPY . /app
|
||||
RUN cd /app && packr2 && env CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags='-linkmode external -extldflags "-static" -s -w' -o ovpn-admin && packr2 clean
|
||||
|
||||
FROM alpine:3.13
|
||||
WORKDIR /app
|
||||
COPY --from=backend-builder /app/ovpn-admin /app
|
||||
RUN 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.3-rc.1/openvpn-user-linux-amd64.tar.gz -O - | tar xz -C /usr/local/bin && \
|
||||
rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*
|
||||
@ -1,9 +1,7 @@
|
||||
FROM alpine:3.23
|
||||
ARG TARGETARCH
|
||||
RUN apk add --update bash openvpn easy-rsa iptables && \
|
||||
FROM alpine:3.13
|
||||
RUN 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 && \
|
||||
wget https://github.com/pashcovich/openvpn-user/releases/download/v1.0.3-rc.1/openvpn-user-linux-amd64.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
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
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.24.6-bullseye AS backend-builder
|
||||
RUN go install github.com/gobuffalo/packr/v2/packr2@latest
|
||||
COPY --from=frontend-builder /app/static /app/frontend/static
|
||||
COPY .. /app
|
||||
ARG TARGETARCH
|
||||
RUN cd /app && packr2 && env CGO_ENABLED=1 GOOS=linux GOARCH=${TARGETARCH} go build -a -tags netgo -ldflags '-linkmode external -extldflags -static -s -w' -o ovpn-admin && packr2 clean
|
||||
|
||||
FROM alpine:3.23
|
||||
WORKDIR /app
|
||||
COPY --from=backend-builder /app/ovpn-admin /app
|
||||
ARG TARGETARCH
|
||||
RUN apk add --update bash easy-rsa openssl openvpn coreutils && \
|
||||
ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \
|
||||
wget https://github.com/pashcovich/openvpn-user/releases/download/v1.0.4/openvpn-user-linux-${TARGETARCH}.tar.gz -O - | tar xz -C /usr/local/bin && \
|
||||
rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*
|
||||
RUN if [ -f "/usr/local/bin/openvpn-user-${TARGETARCH}" ]; then ln -s /usr/local/bin/openvpn-user-${TARGETARCH} /usr/local/bin/openvpn-user; fi
|
||||
54
Makefile
54
Makefile
@ -1,54 +0,0 @@
|
||||
export PATH := $(abspath bin/protoc/bin/):$(abspath bin/):${PATH}
|
||||
export SHELL := env PATH=$(PATH) /bin/sh
|
||||
|
||||
GOOS?=$(shell go env GOOS)
|
||||
GOARCH?=$(shell go env GOARCH)
|
||||
GOLANGCI_VERSION = 1.55.2
|
||||
HELM_DOCS_VERSION = 1.11.0
|
||||
|
||||
ifeq ($(GOARCH),arm)
|
||||
ARCH=armv7
|
||||
else
|
||||
ARCH=$(GOARCH)
|
||||
endif
|
||||
|
||||
COMMIT=$(shell git rev-parse --verify HEAD)
|
||||
|
||||
###########
|
||||
# BUILDING
|
||||
###########
|
||||
|
||||
###########
|
||||
# LINTING
|
||||
###########
|
||||
bin/golangci-lint: bin/golangci-lint-${GOLANGCI_VERSION}
|
||||
@ln -sf golangci-lint-${GOLANGCI_VERSION} bin/golangci-lint
|
||||
|
||||
bin/golangci-lint-${GOLANGCI_VERSION}:
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | BINARY=golangci-lint bash -s -- v${GOLANGCI_VERSION}
|
||||
@mv bin/golangci-lint $@
|
||||
|
||||
###########
|
||||
# HELM
|
||||
###########
|
||||
|
||||
bin/helm-docs: bin/helm-docs-${HELM_DOCS_VERSION}
|
||||
@ln -sf helm-docs-${HELM_DOCS_VERSION} bin/helm-docs
|
||||
bin/helm-docs-${HELM_DOCS_VERSION}:
|
||||
@mkdir -p bin
|
||||
curl -L https://github.com/norwoodj/helm-docs/releases/download/v${HELM_DOCS_VERSION}/helm-docs_${HELM_DOCS_VERSION}_$(shell uname)_x86_64.tar.gz | tar -zOxf - helm-docs > ./bin/helm-docs-${HELM_DOCS_VERSION} && chmod +x ./bin/helm-docs-${HELM_DOCS_VERSION}
|
||||
|
||||
.PHONY: lint fix
|
||||
lint: bin/golangci-lint
|
||||
bin/golangci-lint run
|
||||
|
||||
fix: bin/golangci-lint
|
||||
bin/golangci-lint run --fix
|
||||
|
||||
.PHONY: docs
|
||||
docs: bin/helm-docs
|
||||
bin/helm-docs -s file -c charts/ -t README.md.gotmpl
|
||||
|
||||
###########
|
||||
# TESTING
|
||||
###########
|
||||
148
README.md
148
README.md
@ -2,47 +2,45 @@
|
||||
|
||||
Simple web UI to manage OpenVPN users, their certificates & routes in Linux. While backend is written in Go, frontend is based on Vue.js.
|
||||
|
||||
***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.*
|
||||
Originally created in [Flant](https://flant.com/) for internal needs & used for years, then updated to be more modern and [publicly released](https://blog.flant.com/introducing-ovpn-admin-web-interface-for-openvpn/) in March'21. Your contributions are welcome!
|
||||
|
||||
## Features
|
||||
|
||||
* Adding, deleting OpenVPN users (generating certificates for them);
|
||||
* Revoking/restoring/rotating users certificates;
|
||||
* Adding OpenVPN users (generating certificates for them);
|
||||
* Revoking/restoring 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!**).
|
||||
* (optionally) Specifying/changing password for additional authorization in OpenVPN.
|
||||
|
||||
### Screenshots
|
||||
|
||||
Managing users in ovpn-admin:
|
||||

|
||||

|
||||
|
||||
An example of dashboard made using ovpn-admin metrics:
|
||||

|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
### Disclaimer
|
||||
|
||||
This tool uses external calls for `bash`, `coreutils` and `easy-rsa`, thus **Linux systems only are supported** at the moment.
|
||||
|
||||
### 1. Docker
|
||||
|
||||
There is a ready-to-use [docker-compose.yaml](https://github.com/palark/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/palark/ovpn-admin/blob/master/start.sh).
|
||||
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.
|
||||
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/palark/ovpn-admin.git
|
||||
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
|
||||
|
||||
@ -54,29 +52,18 @@ Requirements. You need Linux with the following components installed:
|
||||
Commands to execute:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/palark/ovpn-admin.git
|
||||
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.)
|
||||
(Please don't forgot to configure all needed params in advance.)
|
||||
|
||||
### 3. Prebuilt binary
|
||||
### 3. Prebuilt binary (WIP)
|
||||
|
||||
You can also download and use prebuilt binaries from the [releases](https://github.com/palark/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`).
|
||||
* If you use `--ccd` and `--ccd.path="/etc/openvpn/ccd"` and 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.
|
||||
* If you want to pass all the traffic generated by the user, you need to edit `ovpn-admin/templates/client.conf.tpl` and uncomment `redirect-gateway def1`.
|
||||
* Tested with openvpn-server versions 2.4 and 2.5 and with tls-auth mode only.
|
||||
* Not tested with Easy-RSA version > 3.0.8.
|
||||
* Status of user connections update every 28 seconds.
|
||||
* Master-replica synchronization and additional password authentication do not work with `--storage.backend=kubernetes.secrets` - **WIP**
|
||||
You can also download and use prebuilt binaries from the [releases](https://github.com/flant/ovpn-admin/releases) page — just choose a relevant tar.gz file.
|
||||
|
||||
## Usage
|
||||
|
||||
@ -84,100 +71,45 @@ You can also download and use prebuilt binaries from the [releases](https://gith
|
||||
usage: ovpn-admin [<flags>]
|
||||
|
||||
Flags:
|
||||
--help show context-sensitive help (try also --help-long and --help-man)
|
||||
|
||||
--help Show context-sensitive help (also try --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)
|
||||
|
||||
--role="master" server role master or slave
|
||||
--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)
|
||||
|
||||
url for master server
|
||||
--master.basic-auth.user="" user for basic auth on master server url
|
||||
--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)
|
||||
|
||||
password for basic auth on master server url
|
||||
--master.sync-frequency=600 master host data sync frequency in seconds.
|
||||
--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
|
||||
|
||||
network 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
|
||||
|
||||
comma separated addresses for openvpn servers
|
||||
--ovpn.server.behindLB ovpn behind cloud loadbalancer
|
||||
--ovpn.service="openvpn-external"
|
||||
(or OVPN_LB_SERVICE) the name of Kubernetes Service having the LoadBalancer
|
||||
type if your OpenVPN server is behind it
|
||||
ovpn behind cloud loadbalancer k8s service name
|
||||
|
||||
--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)
|
||||
|
||||
comma separated (alias=address) for openvpn servers mgmt interfaces
|
||||
--metrics.path="/metrics" URL path for surfacing collected metrics
|
||||
--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)
|
||||
|
||||
path to easyrsa index file.
|
||||
--ccd Enable client-config-dir.
|
||||
--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)
|
||||
|
||||
path to custom client.config.tpl file
|
||||
--templates.ccd-path="" path to custom ccd.tpl file
|
||||
--auth.password Enable additional password authorization.
|
||||
--auth.db="./easyrsa/pki/users.db"
|
||||
(or OVPN_AUTH_DB_PATH) database path for password authorization
|
||||
Database path fort password authorization.
|
||||
--debug Enable debug mode.
|
||||
--verbose Enable verbose mode.
|
||||
--version Show application version.
|
||||
|
||||
--auth.db-init
|
||||
(or OVPN_AUTH_DB_INIT) enable database init if user db not exists or size is 0
|
||||
|
||||
--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
|
||||
```
|
||||
|
||||
## Authors
|
||||
## Further information
|
||||
|
||||
ovpn-admin was originally created in [Flant](https://github.com/flant/) and used internally for years.
|
||||
|
||||
In March 2021, it [went public](https://medium.com/flant-com/introducing-ovpn-admin-a-web-interface-to-manage-openvpn-users-d81705ad8f23) and was still developed in Flant.
|
||||
Namely, [@vitaliy-sn](https://github.com/vitaliy-sn) created its first version in Python, and [@pashcovich](https://github.com/pashcovich) rewrote it in Go.
|
||||
|
||||
In November 2024, this project was moved to [Palark](https://github.com/palark/), which is currently responsible for its maintenance and development.
|
||||
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.
|
||||
|
||||
4
build.sh
4
build.sh
@ -2,10 +2,12 @@
|
||||
|
||||
PATH=$PATH:~/go/bin
|
||||
|
||||
#go get -u github.com/gobuffalo/packr/v2/packr2
|
||||
|
||||
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" $@
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags "-linkmode external -extldflags -static -s -w" -o ovpn-admin
|
||||
|
||||
packr2 clean
|
||||
|
||||
18
build_arm.sh
18
build_arm.sh
@ -1,18 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
PATH=$PATH:~/go/bin
|
||||
|
||||
cd frontend && npm install && npm run build && cd ..
|
||||
|
||||
packr2
|
||||
|
||||
if [[ "$GOOS" == "linux" ]]; then
|
||||
if [[ "$GOARCH" == "arm" ]]; then
|
||||
CC=arm-linux-gnueabi-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm go build -a -tags netgo -ldflags "-linkmode external -extldflags -static -s -w" $@
|
||||
fi
|
||||
if [[ "$GOARCH" == "arm64" ]]; then
|
||||
CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -a -tags netgo -ldflags "-linkmode external -extldflags -static -s -w" $@
|
||||
fi
|
||||
fi
|
||||
|
||||
packr2 clean
|
||||
207
certificates.go
207
certificates.go
@ -1,207 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"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)
|
||||
|
||||
certLifetimeDays, err := strconv.Atoi(*clientCertExpirationDays)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't get client certificate expiration value: %w", err)
|
||||
}
|
||||
|
||||
notBefore := time.Now()
|
||||
notAfter := notBefore.Add(time.Duration(certLifetimeDays) * 24 * time.Hour)
|
||||
if notAfter.After(ca.NotAfter) {
|
||||
notAfter = ca.NotAfter
|
||||
}
|
||||
|
||||
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: notBefore,
|
||||
NotAfter: notAfter,
|
||||
}
|
||||
|
||||
issuerBytes, err := x509.CreateCertificate(rand.Reader, &template, ca, &privKey.PublicKey, caPrivKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
issuerPEM = new(bytes.Buffer)
|
||||
_ = pem.Encode(issuerPEM, &pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: issuerBytes,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// return PEM encoded CRL
|
||||
func genCRL(certs []*RevokedCert, ca *x509.Certificate, caKey *rsa.PrivateKey) (crlPEM *bytes.Buffer, err error) {
|
||||
var revokedCertificates []pkix.RevokedCertificate
|
||||
|
||||
for _, cert := range certs {
|
||||
revokedCertificates = append(revokedCertificates, pkix.RevokedCertificate{SerialNumber: cert.Cert.SerialNumber, RevocationTime: cert.RevokedTime})
|
||||
}
|
||||
|
||||
revocationList := &x509.RevocationList{
|
||||
//SignatureAlgorithm: x509.SHA256WithRSA,
|
||||
RevokedCertificates: revokedCertificates,
|
||||
Number: big.NewInt(1),
|
||||
ThisUpdate: time.Now(),
|
||||
NextUpdate: time.Now().Add(180 * time.Hour * 24),
|
||||
//ExtraExtensions: []pkix.Extension{},
|
||||
}
|
||||
|
||||
crl, err := x509.CreateRevocationList(rand.Reader, revocationList, ca, caKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
crlPEM = new(bytes.Buffer)
|
||||
err = pem.Encode(crlPEM, &pem.Block{
|
||||
Type: "X509 CRL",
|
||||
Bytes: crl,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
owner: palark
|
||||
git-base-url: https://api.github.com/
|
||||
@ -1,15 +0,0 @@
|
||||
apiVersion: v1
|
||||
appVersion: "2.0.2"
|
||||
description: Simple web UI to manage OpenVPN users, their certificates & routes in Linux. While backend is written in Go, frontend is based on Vue.js.
|
||||
name: openvpn-admin
|
||||
version: "0.0.3"
|
||||
kubeVersion: ">=1.14.0-0"
|
||||
maintainers:
|
||||
- name: nabokihms
|
||||
email: max.nabokih@gmail.com
|
||||
url: github.com/nabokihms
|
||||
sources:
|
||||
- https://github.com/palark/openvpn-admin
|
||||
keywords:
|
||||
- kubernetes
|
||||
- openvpn
|
||||
@ -1,40 +0,0 @@
|
||||
# openvpn-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.
|
||||
|
||||
## Maintainers
|
||||
|
||||
| Name | Email | Url |
|
||||
| ---- | ------ | --- |
|
||||
| nabokihms | <max.nabokih@gmail.com> | <github.com/nabokihms> |
|
||||
|
||||
## Source Code
|
||||
|
||||
* <https://github.com/palark/openvpn-admin>
|
||||
|
||||
## Requirements
|
||||
|
||||
Kubernetes: `>=1.14.0-0`
|
||||
|
||||
## Values
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| ovpnAdmin.repo | string | `"ghcr.io/palark/ovpn-admin/ovpn-admin"` | |
|
||||
| openvpn.repo | string | `"ghcr.io/palark/ovpn-admin/openvpn"` | |
|
||||
| openvpn.subnet | string | `"172.16.200.0/255.255.255.0"` | |
|
||||
| openvpn.inlet | string | `"HostPort"` | |
|
||||
| openvpn.hostPort | int | `1194` | |
|
||||
| nodeSelector | object | `{}` | [Node selector](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector) configuration. |
|
||||
| tolerations | list | `[]` | [Tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) for node taints. See the [API reference](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling) for details. |
|
||||
| ingress.enabled | bool | `false` | Enable [ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/). |
|
||||
| ingress.className | string | `""` | Ingress [class name](https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-class). |
|
||||
| ingress.annotations | object | `{}` | Annotations to be added to the ingress. |
|
||||
| ingress.domain | string | `"changeme"` | |
|
||||
| ingress.basicAuth.user | string | `"admin"` | |
|
||||
| ingress.basicAuth.password | string | `"changeme"` | |
|
||||
|
||||
----------------------------------------------
|
||||
Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0)
|
||||
@ -1,88 +0,0 @@
|
||||
{{ $openvpnNetwork := required "A valid .Values.openvpn.subnet entry required!" .Values.openvpn.subnet }}
|
||||
{{ $openvpnNetworkAddress := index (splitList "/" $openvpnNetwork) 0 }}
|
||||
{{ $openvpnNetworkNetmask := index (splitList "/" $openvpnNetwork) 1 }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: openvpn
|
||||
data:
|
||||
openvpn.conf: |-
|
||||
user nobody
|
||||
group nogroup
|
||||
|
||||
mode server
|
||||
tls-server
|
||||
# dev-type tun
|
||||
dev tun
|
||||
proto tcp-server
|
||||
port 1194
|
||||
# local 127.0.0.1
|
||||
management 127.0.0.1 8989
|
||||
|
||||
tun-mtu 1500
|
||||
mssfix
|
||||
# only udp
|
||||
#fragment 1300
|
||||
|
||||
keepalive 10 60
|
||||
client-to-client
|
||||
persist-key
|
||||
persist-tun
|
||||
|
||||
cipher AES-128-CBC
|
||||
duplicate-cn
|
||||
|
||||
server {{ $openvpnNetworkAddress }} {{ $openvpnNetworkNetmask }}
|
||||
|
||||
topology subnet
|
||||
push "topology subnet"
|
||||
push "route-metric 9999"
|
||||
|
||||
verb 4
|
||||
|
||||
ifconfig-pool-persist /tmp/openvpn.ipp
|
||||
status /tmp/openvpn.status
|
||||
|
||||
key-direction 0
|
||||
|
||||
ca /etc/openvpn/certs/pki/ca.crt
|
||||
key /etc/openvpn/certs/pki/private/server.key
|
||||
cert /etc/openvpn/certs/pki/issued/server.crt
|
||||
dh /etc/openvpn/certs/pki/dh.pem
|
||||
crl-verify /etc/openvpn/certs/pki/crl.pem
|
||||
tls-auth /etc/openvpn/certs/pki/ta.key
|
||||
client-config-dir /etc/openvpn/ccd
|
||||
|
||||
entrypoint.sh: |-
|
||||
#!/bin/sh
|
||||
set -x
|
||||
|
||||
iptables -t nat -A POSTROUTING -s {{ $openvpnNetworkAddress }}/{{ $openvpnNetworkNetmask }} ! -d {{ $openvpnNetworkAddress }}/{{ $openvpnNetworkNetmask }} -j MASQUERADE
|
||||
|
||||
mkdir -p /dev/net
|
||||
if [ ! -c /dev/net/tun ]; then
|
||||
mknod /dev/net/tun c 10 200
|
||||
fi
|
||||
|
||||
wait_file() {
|
||||
file_path="$1"
|
||||
while true; do
|
||||
if [ -f $file_path ]; then
|
||||
break
|
||||
fi
|
||||
echo "wait $file_path"
|
||||
sleep 2
|
||||
done
|
||||
}
|
||||
|
||||
easyrsa_path="/etc/openvpn/certs"
|
||||
|
||||
wait_file "$easyrsa_path/pki/ca.crt"
|
||||
wait_file "$easyrsa_path/pki/private/server.key"
|
||||
wait_file "$easyrsa_path/pki/issued/server.crt"
|
||||
wait_file "$easyrsa_path/pki/ta.key"
|
||||
wait_file "$easyrsa_path/pki/dh.pem"
|
||||
wait_file "$easyrsa_path/pki/crl.pem"
|
||||
|
||||
openvpn --config /etc/openvpn/openvpn.conf
|
||||
@ -1,117 +0,0 @@
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: openvpn
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: openvpn
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: openvpn
|
||||
spec:
|
||||
{{- if .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- .Values.nodeSelector | toYaml | indent 8 | printf "\n%s" }}
|
||||
{{- end }}
|
||||
{{- if .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- .Values.tolerations | toYaml | indent 8 | printf "\n%s" }}
|
||||
{{- end }}
|
||||
terminationGracePeriodSeconds: 0
|
||||
serviceAccountName: openvpn
|
||||
containers:
|
||||
- name: ovpn-admin
|
||||
image: {{ .Values.ovpnAdmin.repo }}:master
|
||||
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.ovpnAdmin.repo }}:master
|
||||
command: [ '/entrypoint.sh' ]
|
||||
# imagePullPolicy: Always
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
add:
|
||||
- NET_ADMIN
|
||||
- NET_RAW
|
||||
- MKNOD
|
||||
- SETGID
|
||||
- SETUID
|
||||
drop:
|
||||
- ALL
|
||||
ports:
|
||||
- name: openvpn-tcp
|
||||
protocol: TCP
|
||||
containerPort: 1194
|
||||
{{- if eq .Values.openvpn.inlet "HostPort" }}
|
||||
hostPort: {{ .Values.openvpn.hostPort }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: dev-net
|
||||
mountPath: /dev/net
|
||||
- name: certs
|
||||
mountPath: /etc/openvpn/certs
|
||||
- name: ccd
|
||||
mountPath: /etc/openvpn/ccd
|
||||
- name: config
|
||||
mountPath: /etc/openvpn/openvpn.conf
|
||||
subPath: openvpn.conf
|
||||
readOnly: true
|
||||
- name: entrypoint
|
||||
mountPath: /entrypoint.sh
|
||||
subPath: entrypoint.sh
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
- name: dev-net
|
||||
emptyDir: {}
|
||||
- name: certs
|
||||
emptyDir: {}
|
||||
- name: ccd
|
||||
emptyDir: {}
|
||||
- name: config
|
||||
configMap:
|
||||
name: openvpn
|
||||
defaultMode: 0644
|
||||
- name: entrypoint
|
||||
configMap:
|
||||
name: openvpn
|
||||
defaultMode: 0755
|
||||
@ -1,32 +0,0 @@
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ovpn-admin
|
||||
annotations:
|
||||
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
|
||||
{{- with .Values.ingress.annotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.ingress.className }}
|
||||
ingressClassName: {{ . | quote }}
|
||||
{{- end }}
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ .Values.ingress.domain }}
|
||||
secretName: ingress-tls
|
||||
rules:
|
||||
- host: {{ .Values.ingress.domain }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ovpn-admin
|
||||
port:
|
||||
name: http
|
||||
@ -1,36 +0,0 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: openvpn
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: openvpn
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- services
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- secrets
|
||||
verbs:
|
||||
- "*"
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: openvpn
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: openvpn
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: openvpn
|
||||
@ -1,8 +0,0 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: basic-auth
|
||||
type: Opaque
|
||||
data:
|
||||
auth: {{ print .Values.ingress.basicAuth.user ":{PLAIN}" .Values.ingress.basicAuth.password | b64enc | quote }}
|
||||
@ -1,57 +0,0 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ovpn-admin
|
||||
spec:
|
||||
clusterIP: None
|
||||
ports:
|
||||
- name: http
|
||||
port: 8000
|
||||
protocol: TCP
|
||||
targetPort: 8000
|
||||
selector:
|
||||
app: openvpn
|
||||
---
|
||||
{{- if hasKey .Values.openvpn "inlet" }}
|
||||
|
||||
{{- if eq .Values.openvpn.inlet "LoadBalancer" }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: openvpn-external
|
||||
spec:
|
||||
externalTrafficPolicy: Local
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- name: openvpn-tcp
|
||||
protocol: TCP
|
||||
port: {{ .Values.openvpn.externalPort | default 1194 }}
|
||||
targetPort: openvpn-tcp
|
||||
selector:
|
||||
app: openvpn
|
||||
{{- else if eq .Values.openvpn.inlet "ExternalIP" }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: openvpn-external
|
||||
spec:
|
||||
type: ClusterIP
|
||||
externalIPs:
|
||||
- {{ .Values.openvpn.externalIP }}
|
||||
ports:
|
||||
- name: openvpn-tcp
|
||||
port: {{ .Values.openvpn.externalPort | default 1194 }}
|
||||
protocol: TCP
|
||||
targetPort: openvpn-tcp
|
||||
selector:
|
||||
app: openvpn
|
||||
{{- else if eq .Values.openvpn.inlet "HostPort" }}
|
||||
---
|
||||
{{- else }}
|
||||
{{- cat "Unsupported inlet type" .inlet | fail }}
|
||||
{{- end }}
|
||||
|
||||
{{- end }}
|
||||
@ -1,42 +0,0 @@
|
||||
ovpnAdmin:
|
||||
repo: ghcr.io/palark/ovpn-admin/ovpn-admin
|
||||
|
||||
openvpn:
|
||||
repo: ghcr.io/palark/ovpn-admin/openvpn
|
||||
subnet: 172.16.200.0/255.255.255.0
|
||||
# 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
|
||||
|
||||
# -- [Node selector](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector) configuration.
|
||||
nodeSelector: {}
|
||||
|
||||
# -- [Tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) for node taints.
|
||||
# See the [API reference](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling) for details.
|
||||
tolerations: []
|
||||
|
||||
ingress:
|
||||
# -- Enable [ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/).
|
||||
enabled: false
|
||||
|
||||
# -- Ingress [class name](https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-class).
|
||||
className: ""
|
||||
|
||||
# -- Annotations to be added to the ingress.
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
|
||||
domain: changeme
|
||||
|
||||
basicAuth:
|
||||
user: admin
|
||||
password: changeme
|
||||
@ -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": ""
|
||||
}
|
||||
@ -7,10 +7,6 @@ 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:
|
||||
@ -22,21 +18,8 @@ services:
|
||||
ovpn-admin:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.ovpn-admin
|
||||
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"
|
||||
command: /app/ovpn-admin --debug --ovpn.network="172.16.100.0/22" --master.sync-token="TOKEN" --easyrsa.path="/mnt/easyrsa" --easyrsa.index-path="/mnt/easyrsa/pki/index.txt" --ovpn.server="127.0.0.1:7777:tcp"
|
||||
network_mode: service:openvpn
|
||||
volumes:
|
||||
- ./easyrsa_master:/mnt/easyrsa
|
||||
|
||||
@ -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 && \
|
||||
|
||||
14929
frontend/package-lock.json
generated
14929
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -7,18 +7,17 @@
|
||||
"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": "^1.12.0",
|
||||
"bootstrap-vue": "^2.22.0",
|
||||
"axios": "^0.19.2",
|
||||
"bootstrap-vue": "^2.21.2",
|
||||
"normalize.css": "^8.0.1",
|
||||
"vue": "^2.6.14",
|
||||
"vue-clipboard2": "^0.3.3",
|
||||
"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",
|
||||
"vue-notification": "^1.3.20"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
@ -26,23 +25,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": "^9.0.0",
|
||||
"sass-loader": "^16.0.5",
|
||||
"terser-webpack-plugin": "^5.3.0",
|
||||
"vue-loader": "^17.0.0",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"webpack": "^5.98.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"webpack-dev-server": "^5.2.1"
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,8 +57,8 @@ new Vue({
|
||||
filterable: true,
|
||||
},
|
||||
{
|
||||
label: 'Active Connections',
|
||||
field: 'Connections',
|
||||
label: 'Connection Server',
|
||||
field: 'ConnectionServer',
|
||||
filterable: true,
|
||||
},
|
||||
{
|
||||
@ -107,38 +107,6 @@ new Vue({
|
||||
showForServerRole: ['master'],
|
||||
showForModule: ["core"],
|
||||
},
|
||||
{
|
||||
name: 'u-delete',
|
||||
label: 'Delete',
|
||||
class: 'btn-danger',
|
||||
showWhenStatus: 'Revoked',
|
||||
showForServerRole: ['master'],
|
||||
showForModule: ["core"],
|
||||
},
|
||||
{
|
||||
name: 'u-delete',
|
||||
label: 'Delete',
|
||||
class: 'btn-danger',
|
||||
showWhenStatus: 'Expired',
|
||||
showForServerRole: ['master'],
|
||||
showForModule: ["core"],
|
||||
},
|
||||
{
|
||||
name: 'u-rotate',
|
||||
label: 'Rotate',
|
||||
class: 'btn-warning',
|
||||
showWhenStatus: 'Revoked',
|
||||
showForServerRole: ['master'],
|
||||
showForModule: ["core"],
|
||||
},
|
||||
{
|
||||
name: 'u-rotate',
|
||||
label: 'Rotate',
|
||||
class: 'btn-warning',
|
||||
showWhenStatus: 'Expired',
|
||||
showForServerRole: ['master'],
|
||||
showForModule: ["core"],
|
||||
},
|
||||
{
|
||||
name: 'u-unrevoke',
|
||||
label: 'Unrevoke',
|
||||
@ -193,14 +161,10 @@ new Vue({
|
||||
newPassword: '',
|
||||
passwordChangeStatus: '',
|
||||
passwordChangeMessage: '',
|
||||
rotateUserMessage: '',
|
||||
deleteUserMessage: '',
|
||||
modalNewUserVisible: false,
|
||||
modalShowConfigVisible: false,
|
||||
modalShowCcdVisible: false,
|
||||
modalChangePasswordVisible: false,
|
||||
modalRotateUserVisible: false,
|
||||
modalDeleteUserVisible: false,
|
||||
openvpnConfig: '',
|
||||
ccd: {
|
||||
Name: '',
|
||||
@ -240,16 +204,6 @@ new Vue({
|
||||
_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();
|
||||
@ -297,8 +251,8 @@ new Vue({
|
||||
})
|
||||
},
|
||||
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"
|
||||
@ -306,12 +260,6 @@ new Vue({
|
||||
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'} : {}
|
||||
},
|
||||
@ -324,12 +272,6 @@ new Vue({
|
||||
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 +288,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;
|
||||
@ -369,6 +302,14 @@ new Vue({
|
||||
});
|
||||
},
|
||||
|
||||
staticAddrCheckboxOnChange: function() {
|
||||
var staticAddrInput = document.getElementById('static-address');
|
||||
var staticAddrEnable = document.getElementById('enable-static');
|
||||
|
||||
staticAddrInput.disabled = !staticAddrEnable.checked;
|
||||
staticAddrInput.value == "dynamic" ? staticAddrInput.value = "" : staticAddrInput.value = "dynamic";
|
||||
},
|
||||
|
||||
getServerSetting: function() {
|
||||
var _this = this;
|
||||
axios.request(axios_cfg('api/server/settings'))
|
||||
@ -453,52 +394,6 @@ new Vue({
|
||||
_this.$notify({title: 'Changing password for user ' + _this.username + ' failed!', type: 'error'})
|
||||
});
|
||||
},
|
||||
|
||||
rotateUser: function(user) {
|
||||
var _this = this;
|
||||
|
||||
_this.u.rotateUserMessage = "";
|
||||
|
||||
var data = new URLSearchParams();
|
||||
data.append('username', user);
|
||||
data.append('password', _this.u.newPassword);
|
||||
|
||||
axios.request(axios_cfg('api/user/rotate', data, 'form'))
|
||||
.then(function(response) {
|
||||
_this.u.roatateUserStatus = 200;
|
||||
_this.u.newPassword = '';
|
||||
_this.getUserData();
|
||||
_this.u.modalRotateUserVisible = false;
|
||||
_this.$notify({title: 'Certificates for user ' + _this.username + ' rotated!', type: 'success'})
|
||||
})
|
||||
.catch(function(error) {
|
||||
_this.u.roatateUserStatus = error.response.status;
|
||||
_this.u.rotateUserMessage = error.response.data.message;
|
||||
_this.$notify({title: 'Rotate certificates for user ' + _this.username + ' failed!', type: 'error'})
|
||||
})
|
||||
},
|
||||
deleteUser: function(user) {
|
||||
var _this = this;
|
||||
|
||||
_this.u.deleteUserMessage = "";
|
||||
|
||||
var data = new URLSearchParams();
|
||||
data.append('username', user);
|
||||
|
||||
axios.request(axios_cfg('api/user/delete', data, 'form'))
|
||||
.then(function(response) {
|
||||
_this.u.deleteUserStatus = 200;
|
||||
_this.u.newPassword = '';
|
||||
_this.getUserData();
|
||||
_this.u.modalDeleteUserVisible = false;
|
||||
_this.$notify({title: 'User ' + _this.username + ' deleted!', type: 'success'})
|
||||
})
|
||||
.catch(function(error) {
|
||||
_this.u.deleteUserStatus = error.response.status;
|
||||
_this.u.deleteUserMessage = error.response.data.message;
|
||||
_this.$notify({title: 'Deleting user ' + _this.username + ' failed!', type: 'error'})
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
@ -48,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;
|
||||
}
|
||||
|
||||
@ -42,6 +42,10 @@
|
||||
</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">
|
||||
@ -119,10 +123,12 @@
|
||||
<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>
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<input id="enable-static" type="checkbox" @change="staticAddrCheckboxOnChange()" v-if="serverRole == 'master'" v-bind:checked="!customAddressDisabled">
|
||||
</div>
|
||||
</div>
|
||||
<input id="static-address" 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">
|
||||
@ -185,50 +191,6 @@
|
||||
</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>
|
||||
|
||||
@ -1,50 +1,70 @@
|
||||
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',
|
||||
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'
|
||||
style: [
|
||||
'./src/style.js',
|
||||
]
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, './static/dist'),
|
||||
publicPath: '/dist/',
|
||||
filename: '[name].min.js'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
'vue-style-loader',
|
||||
'css-loader'
|
||||
],
|
||||
},
|
||||
extensions: ['*', '.js', '.vue', '.json']
|
||||
},
|
||||
}
|
||||
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'vue$': 'vue/dist/vue.esm.js'
|
||||
},
|
||||
extensions: ['*', '.js', '.vue', '.json']
|
||||
},
|
||||
devServer: {
|
||||
historyApiFallback: true,
|
||||
noInfo: true,
|
||||
overlay: true
|
||||
},
|
||||
performance: {
|
||||
hints: false
|
||||
},
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports.devtool = 'false'
|
||||
// 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: false
|
||||
}),
|
||||
new webpack.LoaderOptionsPlugin({
|
||||
minimize: true
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
75
go.mod
75
go.mod
@ -1,66 +1,21 @@
|
||||
module ovpn-admin
|
||||
|
||||
go 1.24.0
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/gobuffalo/packr/v2 v2.8.3
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
k8s.io/api v0.34.3
|
||||
k8s.io/apimachinery v0.34.3
|
||||
k8s.io/client-go v0.34.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/gobuffalo/logger v1.0.6 // indirect
|
||||
github.com/gobuffalo/packd v1.0.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/gobuffalo/packr/v2 v2.8.1
|
||||
github.com/karrick/godirwalk v1.16.1 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // 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/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/term v0.34.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
github.com/magefile/mage v1.11.0 // indirect
|
||||
github.com/prometheus/client_golang v1.8.0
|
||||
github.com/prometheus/common v0.15.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.7.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/sys v0.0.0-20210225080010-8e9945a5478f // indirect
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
k8s.io/apimachinery v0.20.4
|
||||
k8s.io/client-go v0.20.4
|
||||
)
|
||||
|
||||
226
helpers.go
226
helpers.go
@ -1,43 +1,39 @@
|
||||
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 parseDate(layout,datetime string) time.Time {
|
||||
t, err := time.Parse(layout, datetime)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
log.Println(err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func parseDateToString(layout, datetime, format string) string {
|
||||
func parseDateToString(layout,datetime,format string) string {
|
||||
return parseDate(layout, datetime).Format(format)
|
||||
}
|
||||
|
||||
func parseDateToUnix(layout, datetime string) int64 {
|
||||
func parseDateToUnix(layout,datetime string) int64 {
|
||||
return parseDate(layout, datetime).Unix()
|
||||
}
|
||||
|
||||
func runBash(script string) string {
|
||||
log.Debugln(script)
|
||||
if *debug {
|
||||
log.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 +44,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 +54,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 +100,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\n", url, resp.StatusCode )
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
@ -183,124 +114,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
|
||||
}
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
apt-get update
|
||||
apt-get install -y curl
|
||||
apt-get install -y libc6 libc6-dev gcc-arm-linux-gnueabi gcc-aarch64-linux-gnu
|
||||
|
||||
curl -sL https://deb.nodesource.com/setup_16.x | bash -
|
||||
apt-get install -y nodejs
|
||||
|
||||
PATH=$PATH:~/go/bin
|
||||
|
||||
go install github.com/gobuffalo/packr/v2/packr2@latest
|
||||
@ -1,12 +1,13 @@
|
||||
#!/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
|
||||
apt-get install -y npm nodejs
|
||||
|
||||
PATH=$PATH:~/go/bin
|
||||
|
||||
go install github.com/gobuffalo/packr/v2/packr2@latest
|
||||
go get -u github.com/gobuffalo/packr/v2/packr2
|
||||
|
||||
cd frontend && npm install && npm run build && cd ..
|
||||
|
||||
packr2
|
||||
|
||||
|
||||
794
kubernetes.go
794
kubernetes.go
@ -1,794 +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{
|
||||
labelKeyIndexTxt: "",
|
||||
labelKeyType: labelValueClientAuth,
|
||||
labelKeyName: commonName,
|
||||
labelKeyManagedBy: labelValueManagedByApp,
|
||||
},
|
||||
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.transferRoutes(secret, 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
|
||||
}
|
||||
|
||||
// transferRoutes transfers configured routes from revoked certs to a new one
|
||||
func (openVPNPKI *OpenVPNPKI) transferRoutes(revokedSecret *v1.Secret, newNameCert string) error {
|
||||
ccd, ok := revokedSecret.Data["ccd"]
|
||||
if !ok || len(ccd) == 0 {
|
||||
log.Infof("No CCD data found in secret %s", revokedSecret.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
openVPNPKI.secretUpdateCcd(newNameCert, ccd)
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -8,7 +8,7 @@ env
|
||||
auth_usr=$(head -1 $1)
|
||||
auth_passwd=$(tail -1 $1)
|
||||
|
||||
if [ $common_name = $auth_usr ]; then
|
||||
if [ $common_name = $username ]; then
|
||||
openvpn-user auth --db.path /etc/openvpn/easyrsa/pki/users.db --user ${auth_usr} --password ${auth_passwd}
|
||||
else
|
||||
echo "Authorization failed"
|
||||
|
||||
@ -21,10 +21,10 @@ else
|
||||
done
|
||||
else
|
||||
echo "Generating new certs"
|
||||
easyrsa --batch init-pki
|
||||
easyrsa init-pki
|
||||
cp -R /usr/share/easy-rsa/* $EASY_RSA_LOC/pki
|
||||
echo "ca" | easyrsa build-ca nopass
|
||||
easyrsa --batch build-server-full server nopass
|
||||
easyrsa build-server-full server nopass
|
||||
easyrsa gen-dh
|
||||
openvpn --genkey --secret ./pki/ta.key
|
||||
fi
|
||||
@ -48,7 +48,7 @@ if [ ${OVPN_PASSWD_AUTH} = "true" ]; then
|
||||
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 && openvpn-user db-migrate --db.path=$EASY_RSA_LOC/pki/users.db
|
||||
openvpn-user db-init --db.path=$EASY_RSA_LOC/pki/users.db
|
||||
fi
|
||||
|
||||
[ -d $EASY_RSA_LOC/pki ] && chmod 755 $EASY_RSA_LOC/pki
|
||||
|
||||
@ -14,7 +14,6 @@ keepalive 10 60
|
||||
persist-key
|
||||
persist-tun
|
||||
topology subnet
|
||||
#duplicate-cn
|
||||
#proto tcp
|
||||
#port 1194
|
||||
#dev tun0
|
||||
|
||||
4
start.sh
4
start.sh
@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# About 'docker compose' and 'docker-compose'
|
||||
# We are using Docker Compose in plugin mode with Docker. For more details, see: https://docs.docker.com/compose/install/linux/. If you need to use the standalone Docker Compose, you can modify the command `docker compose` to `docker-compose` accordingly.
|
||||
docker compose -p openvpn-master up -d --build
|
||||
docker-compose -p openvpn-master up -d --build
|
||||
|
||||
@ -13,7 +13,7 @@ tls-client
|
||||
remote-cert-tls server
|
||||
# uncomment below lines for use with linux
|
||||
#script-security 2
|
||||
# if you use resolved
|
||||
# if use 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
|
||||
|
||||
15
werf.yaml
15
werf.yaml
@ -1,19 +1,10 @@
|
||||
project: ovpn-admin
|
||||
configVersion: 1
|
||||
build:
|
||||
platform:
|
||||
- linux/amd64
|
||||
{{- if eq .Env "release" }}
|
||||
- linux/arm64
|
||||
- linux/arm/v7
|
||||
- linux/arm/v8
|
||||
{{- end }}
|
||||
staged: true
|
||||
|
||||
---
|
||||
image: ovpn-admin
|
||||
dockerfile: Dockerfile.ovpn-admin
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
|
||||
---
|
||||
image: openvpn
|
||||
dockerfile: Dockerfile.openvpn
|
||||
context: .
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user