Merge master into PR #962

Resolve conflicts by integrating CLIENT_FILEPATH and client file
ownership features into the refactored helper function structure.

- Add getClientOwner() helper to determine file owner
- Add setClientConfigPermissions() helper to apply chmod/chown
- Update generateClientConfig() to accept filepath parameter
- Update newClient() and renewClient() to support CLIENT_FILEPATH
  env var and set proper file permissions
This commit is contained in:
Stanislas Lange
2025-12-13 20:30:49 +01:00
27 changed files with 3477 additions and 854 deletions

View File

@@ -1,3 +1,10 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
[*.sh]
indent_style = tab
indent_size = 4

3
.github/FUNDING.yml vendored
View File

@@ -1,5 +1,2 @@
patreon: stanislas
liberapay: stanislas
ko_fi: stanislas
github: angristan
custom: https://coindrop.to/stanislas

View File

@@ -1,35 +0,0 @@
---
name: Bug report / Support request
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**⚠️ Unless you are sure you find a bug with the script, please open a [discussion](https://github.com/angristan/openvpn-install/discussions) instead of an issue!**
**Checklist**
- [ ] I read the [README](https://github.com/angristan/openvpn-install/blob/master/README.md)
- [ ] I read the [FAQ](https://github.com/angristan/openvpn-install/blob/master/FAQ.md)
- [ ] I searched the [issues](https://github.com/angristan/openvpn-install/issues?q=is%3Aissue+)
- [ ] I searched the [discussion](https://github.com/angristan/openvpn-install/discussions)
- [ ] My issue is about the script, and not OpenVPN itself
<!---
If you need help with OpenVPN itself, please us the [community forums](https://forums.openvpn.net/) or [Stack Overflow](https://stackoverflow.com/questions/tagged/openvpn)
--->
Pease include as much details as possible in your issue:
- Description of the issue
- How to reproduce the issue
- What did you expected should happen
- Logs
- Server/Client versions (OS, OpenVPN, etc)
- Any context or information that could help
---
<!-- Write your report below this line -->

View File

@@ -1,31 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Checklist**
- [ ] I read the [README](https://github.com/angristan/openvpn-install/blob/master/README.md)
- [ ] I read the [FAQ](https://github.com/angristan/openvpn-install/blob/master/FAQ.md)
- [ ] I searched the [issues](https://github.com/angristan/openvpn-install/issues?q=is%3Aissue+)
- [ ] My issue is about the script, and not OpenVPN itself
<!---
If you need help with OpenVPN itself, please us the [community forums](https://forums.openvpn.net/) or [Stack Overflow](https://stackoverflow.com/questions/tagged/openvpn)
--->
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,6 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"

10
.github/issue_template.md vendored Normal file
View File

@@ -0,0 +1,10 @@
<!---
❗️ Please read ❗️
➡️ If you need help with OpenVPN itself, please use the community forums (https://forums.openvpn.net/) or Stack Overflow (https://stackoverflow.com/questions/tagged/openvpn)
➡️ For the script, prefer opening a discussion thread for help: https://github.com/angristan/openvpn-install/discussions
💡 It helps keep the issue tracker clean and focused on bugs and feature requests.
🙏 Please include as much information as possible, and make sure you're running the latest version of the script.
✍️ Please state the Linux distribution you're using and its version, as well as the OpenVPN version.
✋ For feature requests, remember that this script is meant to be simple and easy to use. If you want to add a lot of options, it's better to fork the project.
--->

View File

@@ -1 +1,8 @@
{ 'MD013': null, 'MD045': null, 'MD040': null, 'MD036': null }
{
"MD013": null,
"MD045": null,
"MD040": null,
"MD036": null,
"MD041": null,
"MD060": null,
}

7
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,7 @@
<!---
❗️ Please read ❗️
➡️ Please make sure you've followed the guidelines: https://github.com/angristan/openvpn-install#contributing
✅ Please make sure your changes are tested and working
🗣️ Please avoid large PRs, and discuss changes in a GitHub issue first
✋ If the changes are too big and not in line with the project, they will probably be rejected. Remember that this script is meant to be simple and easy to use! And that added features increase maintenance burden.
--->

104
.github/workflows/do-test.yml vendored Normal file
View File

@@ -0,0 +1,104 @@
# DigitalOcean E2E tests (manual trigger only)
# Primary CI testing is now done via Docker in docker-test.yml
# This workflow is kept for real-world VM testing when needed
on:
workflow_dispatch:
name: Test
permissions:
contents: read
jobs:
install:
runs-on: ubuntu-latest
if: github.repository == 'angristan/openvpn-install' && github.actor == 'angristan'
strategy:
matrix:
os-image:
- debian-12-x64
- debian-13-x64
- ubuntu-22-04-x64
- ubuntu-24-04-x64
- fedora-42-x64
# - centos-stream-9-x64 # yum oomkill
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup doctl
uses: digitalocean/action-doctl@135ac0aa0eed4437d547c6f12c364d3006b42824 # v2.5.1
with:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
- name: Create server
run: doctl compute droplet create "openvpn-action-${GITHUB_RUN_ID}-${GITHUB_RUN_NUMBER}-${{ matrix.os-image }}" --size s-1vcpu-1gb --image "${{ matrix.os-image }}" --region lon1 --enable-ipv6 --ssh-keys be:66:76:61:a8:71:93:aa:e3:19:ba:d8:0d:d2:2d:d4 --wait
- name: Get server ID
run: echo "value=$(doctl compute droplet list -o json | jq -r '.[] | select(.name == "'"openvpn-action-${GITHUB_RUN_ID}-${GITHUB_RUN_NUMBER}-${{ matrix.os-image }}"'").id')" >> "$GITHUB_OUTPUT"
id: server_id
- name: Move server to dedicated project
run: doctl projects resources assign "$DIGITALOCEAN_PROJECT_ID" --resource=do:droplet:"$SERVER_ID"
env:
DIGITALOCEAN_PROJECT_ID: ${{ secrets.DIGITALOCEAN_PROJECT_ID }}
SERVER_ID: ${{ steps.server_id.outputs.value }}
- name: Wait for server to boot
run: sleep 90
- name: Get server IP
run: echo "value=$(doctl compute droplet list -o json | jq -r '.[] | select(.name == "'"openvpn-action-${GITHUB_RUN_ID}-${GITHUB_RUN_NUMBER}-${{ matrix.os-image }}"'").networks.v4 | .[] | select(.type == "'"public"'").ip_address')" >> "$GITHUB_OUTPUT"
id: server_ip
- name: Get server OS
run: echo "value=$(echo "${{ matrix.os-image }}" | cut -d '-' -f1)" >> "$GITHUB_OUTPUT"
id: server_os
- name: Setup remote server (Debian/Ubuntu)
if: steps.server_os.outputs.value == 'debian' || steps.server_os.outputs.value == 'ubuntu'
uses: appleboy/ssh-action@7eaf76671a0d7eec5d98ee897acda4f968735a17 # v1.2.0
with:
host: ${{ steps.server_ip.outputs.value }}
username: root
key: ${{ secrets.SSH_KEY }}
script: set -x && apt-get update && apt-get -o DPkg::Lock::Timeout=120 install -y git
- name: Setup remote server (Fedora)
if: steps.server_os.outputs.value == 'fedora'
uses: appleboy/ssh-action@7eaf76671a0d7eec5d98ee897acda4f968735a17 # v1.2.0
with:
host: ${{ steps.server_ip.outputs.value }}
username: root
key: ${{ secrets.SSH_KEY }}
script: set -x && dnf install -y git
- name: Setup remote server (CentOS)
if: steps.server_os.outputs.value == 'centos'
uses: appleboy/ssh-action@7eaf76671a0d7eec5d98ee897acda4f968735a17 # v1.2.0
with:
host: ${{ steps.server_ip.outputs.value }}
username: root
key: ${{ secrets.SSH_KEY }}
script: set -x && yum install -y git
- name: Download repo and checkout current commit
uses: appleboy/ssh-action@7eaf76671a0d7eec5d98ee897acda4f968735a17 # v1.2.0
with:
host: ${{ steps.server_ip.outputs.value }}
username: root
key: ${{ secrets.SSH_KEY }}
script: set -x && git clone https://github.com/angristan/openvpn-install.git && cd openvpn-install && git checkout ${{ github.sha }}
- name: Run openvpn-install.sh in headless mode
uses: appleboy/ssh-action@7eaf76671a0d7eec5d98ee897acda4f968735a17 # v1.2.0
with:
host: ${{ steps.server_ip.outputs.value }}
username: root
key: ${{ secrets.SSH_KEY }}
script: 'set -x && AUTO_INSTALL=y bash -x ~/openvpn-install/openvpn-install.sh && ps aux | grep openvpn | grep -v grep > /dev/null 2>&1 && echo "Success: OpenVPN is running" && exit 0 || echo "Failure: OpenVPN is not running" && exit 1'
- name: Delete server
run: doctl compute droplet delete -f "openvpn-action-${GITHUB_RUN_ID}-${GITHUB_RUN_NUMBER}-${{ matrix.os-image }}"
if: always()

275
.github/workflows/docker-test.yml vendored Normal file
View File

@@ -0,0 +1,275 @@
---
on:
push:
branches: [master]
pull_request:
workflow_dispatch:
name: Docker Test
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
docker-test:
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
os:
- name: ubuntu-18.04
image: ubuntu:18.04
- name: ubuntu-20.04
image: ubuntu:20.04
- name: ubuntu-22.04
image: ubuntu:22.04
- name: ubuntu-24.04
image: ubuntu:24.04
- name: debian-11
image: debian:11
- name: debian-12
image: debian:12
- name: centos-stream-9
image: quay.io/centos/centos:stream9
- name: centos-stream-10
image: quay.io/centos/centos:stream10
- name: fedora-42
image: fedora:42
- name: fedora-43
image: fedora:43
- name: rocky-8
image: rockylinux/rockylinux:8
- name: rocky-9
image: rockylinux/rockylinux:9
- name: rocky-10
image: rockylinux/rockylinux:10
- name: almalinux-8
image: almalinux:8
- name: almalinux-9
image: almalinux:9
- name: almalinux-10
image: almalinux:10
- name: archlinux
image: archlinux:latest
- name: opensuse-leap-16.0
image: opensuse/leap:16.0
- name: opensuse-tumbleweed
image: opensuse/tumbleweed
- name: oraclelinux-8
image: oraclelinux:8
- name: oraclelinux-9
image: oraclelinux:9
- name: oraclelinux-10
image: oraclelinux:10
- name: amazonlinux-2023
image: amazonlinux:2023
# Default TLS settings (tls-crypt-v2)
tls:
- name: tls-crypt-v2
sig: "1"
key_file: tls-crypt-v2.key
# Additional TLS types tested on Ubuntu 24.04 only
include:
- os:
name: ubuntu-24.04-tls-crypt
image: ubuntu:24.04
tls:
name: tls-crypt
sig: "2"
key_file: tls-crypt.key
- os:
name: ubuntu-24.04-tls-auth
image: ubuntu:24.04
tls:
name: tls-auth
sig: "3"
key_file: tls-auth.key
name: ${{ matrix.os.name }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Build server image
run: |
docker build \
--build-arg BASE_IMAGE=${{ matrix.os.image }} \
-t openvpn-server \
-f test/Dockerfile.server .
- name: Build client image
run: docker build -t openvpn-client -f test/Dockerfile.client .
- name: Create Docker network
run: docker network create --subnet=172.28.0.0/24 vpn-test
- name: Create shared volume
run: docker volume create shared-config
- name: Start OpenVPN server
run: |
docker run -d \
--name openvpn-server \
--hostname openvpn-server \
--privileged \
--cgroupns=host \
--device=/dev/net/tun:/dev/net/tun \
--sysctl net.ipv4.ip_forward=1 \
--network vpn-test \
--ip 172.28.0.10 \
-v shared-config:/shared \
-v /sys/fs/cgroup:/sys/fs/cgroup:rw \
--tmpfs /run \
--tmpfs /run/lock \
--stop-signal SIGRTMIN+3 \
-e TLS_SIG=${{ matrix.tls.sig }} \
-e TLS_KEY_FILE=${{ matrix.tls.key_file }} \
openvpn-server
- name: Wait for server installation and startup
run: |
echo "Waiting for OpenVPN server to install and client config to be ready..."
for i in {1..90}; do
# Get service status (properly handle non-zero exit codes)
# systemctl is-active returns exit code 3 for "inactive"/"failed", so capture output without checking exit code
SERVICE_STATUS="$(docker exec openvpn-server systemctl is-active openvpn-test.service 2>/dev/null)" || true
[ -z "$SERVICE_STATUS" ] && SERVICE_STATUS="unknown"
# Fail fast if service failed
if [ "$SERVICE_STATUS" = "failed" ]; then
echo "ERROR: openvpn-test.service failed during installation"
docker exec openvpn-server systemctl status openvpn-test.service 2>&1 || true
docker exec openvpn-server journalctl -u openvpn-test.service --no-pager -n 100 2>&1 || true
exit 1
fi
# Check if OpenVPN server is running and client config exists
# The service will be "activating" while waiting for client tests - that's expected
OPENVPN_RUNNING=false
CONFIG_EXISTS=false
if docker exec openvpn-server pgrep -f "openvpn.*server.conf" > /dev/null 2>&1; then
OPENVPN_RUNNING=true
fi
if docker exec openvpn-server test -f /shared/client.ovpn 2>/dev/null; then
CONFIG_EXISTS=true
fi
if [ "$OPENVPN_RUNNING" = true ] && [ "$CONFIG_EXISTS" = true ]; then
echo "OpenVPN server is running and client config is ready!"
break
fi
echo "Waiting... ($i/90) - Service: $SERVICE_STATUS, OpenVPN running: $OPENVPN_RUNNING, Config exists: $CONFIG_EXISTS"
sleep 5
done
# Final verification with retry (handles race condition during cert renewal restart)
OPENVPN_STARTED=false
for retry in {1..5}; do
if docker exec openvpn-server pgrep -f "openvpn.*server.conf" > /dev/null 2>&1; then
OPENVPN_STARTED=true
break
fi
echo "Waiting for OpenVPN process... (retry $retry/5)"
sleep 2
done
if [ "$OPENVPN_STARTED" = false ]; then
echo "ERROR: OpenVPN server failed to start"
docker exec openvpn-server systemctl status openvpn-server@server 2>&1 || true
docker exec openvpn-server journalctl -u openvpn-test.service --no-pager -n 100 2>&1 || true
exit 1
fi
if ! docker exec openvpn-server test -f /shared/client.ovpn 2>/dev/null; then
echo "ERROR: Client config not generated"
docker exec openvpn-server journalctl -u openvpn-test.service --no-pager -n 100 2>&1 || true
exit 1
fi
echo "Server ready for client connection!"
- name: Verify client config was generated
run: |
docker run --rm -v shared-config:/shared alpine \
ls -la /shared/
docker run --rm -v shared-config:/shared alpine \
cat /shared/client.ovpn
- name: Start OpenVPN client and run tests
run: |
docker run \
--name openvpn-client \
--hostname openvpn-client \
--cap-add=NET_ADMIN \
--device=/dev/net/tun:/dev/net/tun \
--network vpn-test \
--ip 172.28.0.20 \
-v shared-config:/shared \
openvpn-client &
# Wait for tests to complete (look for success message)
# Extended timeout for revocation e2e tests
for i in {1..180}; do
if docker logs openvpn-client 2>&1 | grep -q "ALL TESTS PASSED"
then
echo "Tests passed!"
exit 0
fi
if docker logs openvpn-client 2>&1 | grep -q "FAIL:"; then
echo "Tests failed!"
docker logs openvpn-client
exit 1
fi
echo "Waiting for tests... ($i/180)"
sleep 2
done
echo "Timeout waiting for tests"
docker logs openvpn-client
exit 1
- name: Show server logs
if: always()
run: docker logs openvpn-server 2>&1 || true
- name: Show systemd journal logs
if: always()
run: |
echo "=== openvpn-test.service status ==="
docker exec openvpn-server systemctl status openvpn-test.service 2>&1 || true
echo ""
echo "=== openvpn-test.service journal ==="
docker exec openvpn-server journalctl -u openvpn-test.service --no-pager -n 100 2>&1 || true
echo ""
echo "=== openvpn-server@server.service journal ==="
docker exec openvpn-server journalctl -u openvpn-server@server.service --no-pager -n 50 2>&1 || true
- name: Show install script log
if: always()
run: |
docker cp openvpn-server:/opt/openvpn-install.log /tmp/openvpn-install.log 2>/dev/null && \
cat /tmp/openvpn-install.log || echo "No install log found"
- name: Show client logs
if: always()
run: docker logs openvpn-client 2>&1 || true
- name: Cleanup
if: always()
run: |
docker stop openvpn-server openvpn-client 2>/dev/null || true
docker rm openvpn-server openvpn-client 2>/dev/null || true
docker network rm vpn-test 2>/dev/null || true
docker volume rm shared-config 2>/dev/null || true

View File

@@ -1,14 +1,28 @@
on: [push, pull_request]
on:
push:
branches: [master]
pull_request:
workflow_dispatch:
name: Lint
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
super-linter:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
- name: Lint Code Base
uses: github/super-linter@v4.1.0
uses: super-linter/super-linter@502f4fe48a81a392756e173e39a861f8c8efe056 # v8
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,100 +0,0 @@
on:
push:
branches:
- master
name: Test
jobs:
install:
runs-on: ubuntu-latest
if: github.repository == 'angristan/openvpn-install' && github.actor == 'angristan'
strategy:
matrix:
os-image:
- debian-9-x64
- debian-10-x64
- debian-11-x64
- ubuntu-18-04-x64
- ubuntu-20-04-x64
- ubuntu-21-04-x64
- ubuntu-21-10-x64
- fedora-33-x64
- fedora-34-x64
- fedora-35-x64
- centos-7-x64
- centos-8-x64
steps:
- uses: actions/checkout@v2.4.0
- name: Setup doctl
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
- name: Create server
run: doctl compute droplet create openvpn-action-$GITHUB_RUN_ID-$GITHUB_RUN_NUMBER-${{ matrix.os-image }} --size s-1vcpu-1gb --image ${{ matrix.os-image }} --region lon1 --enable-ipv6 --ssh-keys be:66:76:61:a8:71:93:aa:e3:19:ba:d8:0d:d2:2d:d4 --wait
- name: Get server ID
run: echo ::set-output name=value::$(doctl compute droplet list -o json | jq -r '.[] | select(.name == "'openvpn-action-$GITHUB_RUN_ID-$GITHUB_RUN_NUMBER-${{ matrix.os-image }}'").id')
id: server_id
- name: Move server to dedicated project
run: doctl projects resources assign ${{ secrets.DIGITALOCEAN_PROJECT_ID }} --resource=do:droplet:${{ steps.server_id.outputs.value }}
- name: Wait for server to boot
run: sleep 90
- name: Get server IP
run: echo ::set-output name=value::$(doctl compute droplet list -o json | jq -r '.[] | select(.name == "'openvpn-action-$GITHUB_RUN_ID-$GITHUB_RUN_NUMBER-${{ matrix.os-image }}'").networks.v4 | .[] | select(.type == "'public'").ip_address')
id: server_ip
- name: Get server OS
run: echo ::set-output name=value::$(echo ${{ matrix.os-image }} | cut -d '-' -f1)
id: server_os
- name: Setup remote server (Debian/Ubuntu)
if: steps.server_os.outputs.value == 'debian' || steps.server_os.outputs.value == 'ubuntu'
uses: appleboy/ssh-action@v0.1.4
with:
host: ${{ steps.server_ip.outputs.value }}
username: root
key: ${{ secrets.SSH_KEY }}
script: set -x && apt-get update && apt-get install -y git
- name: Setup remote server (Fedora)
if: steps.server_os.outputs.value == 'fedora'
uses: appleboy/ssh-action@v0.1.4
with:
host: ${{ steps.server_ip.outputs.value }}
username: root
key: ${{ secrets.SSH_KEY }}
script: set -x && dnf install -y git
- name: Setup remote server (CentOS)
if: steps.server_os.outputs.value == 'centos'
uses: appleboy/ssh-action@v0.1.4
with:
host: ${{ steps.server_ip.outputs.value }}
username: root
key: ${{ secrets.SSH_KEY }}
script: set -x && yum install -y git
- name: Download repo and checkout current commit
uses: appleboy/ssh-action@v0.1.4
with:
host: ${{ steps.server_ip.outputs.value }}
username: root
key: ${{ secrets.SSH_KEY }}
script: set -x && git clone https://github.com/angristan/openvpn-install.git && cd openvpn-install && git checkout ${{ github.event.pull_request.head.sha }}
- name: Run openvpn-install.sh in headless mode
uses: appleboy/ssh-action@v0.1.4
with:
host: ${{ steps.server_ip.outputs.value }}
username: root
key: ${{ secrets.SSH_KEY }}
script: 'set -x && AUTO_INSTALL=y bash -x ~/openvpn-install/openvpn-install.sh && ps aux | grep openvpn | grep -v grep > /dev/null 2>&1 && echo "Success: OpenVPN is running" && exit 0 || echo "Failure: OpenVPN is not running" && exit 1'
- name: Delete server
run: doctl compute droplet delete -f openvpn-action-$GITHUB_RUN_ID-$GITHUB_RUN_NUMBER-${{ matrix.os-image }}
if: always()

View File

@@ -0,0 +1,73 @@
name: Update Easy-RSA SHA256
# Note: This workflow commits and pushes changes to openvpn-install.sh.
# Uses PAT to trigger CI on the resulting commit. Infinite recursion is prevented
# by the 'renovate/' branch prefix check - CI commits don't re-trigger this workflow.
# Requires: Create a PAT with 'contents: write' scope and add as repository secret 'PAT'
on:
pull_request:
types: [opened, synchronize]
paths:
- "openvpn-install.sh"
permissions:
contents: read
jobs:
update-hash:
if: startsWith(github.head_ref, 'renovate/')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.head_ref }}
token: ${{ secrets.PAT }}
persist-credentials: false
- name: Extract version and update SHA256
run: |
VERSION=$(grep -oP 'EASYRSA_VERSION="\K[^"]+' openvpn-install.sh)
if [ -z "$VERSION" ]; then
echo "Error: Failed to extract EASYRSA_VERSION"
exit 1
fi
echo "Easy-RSA version: $VERSION"
CURRENT_SHA=$(grep -oP 'EASYRSA_SHA256="\K[^"]+' openvpn-install.sh)
if [ -z "$CURRENT_SHA" ]; then
echo "Error: Failed to extract EASYRSA_SHA256"
exit 1
fi
echo "Current SHA256: $CURRENT_SHA"
TARBALL_URL="https://github.com/OpenVPN/easy-rsa/releases/download/v${VERSION}/EasyRSA-${VERSION}.tgz"
if ! curl -fsSL "$TARBALL_URL" -o /tmp/easyrsa.tgz; then
echo "Error: Failed to download Easy-RSA tarball from $TARBALL_URL"
exit 1
fi
NEW_SHA=$(sha256sum /tmp/easyrsa.tgz | cut -d' ' -f1)
echo "New SHA256: $NEW_SHA"
if [ "$CURRENT_SHA" != "$NEW_SHA" ]; then
sed -i "s|EASYRSA_SHA256=\"$CURRENT_SHA\"|EASYRSA_SHA256=\"$NEW_SHA\"|" openvpn-install.sh
echo "SHA256 updated"
echo "HASH_CHANGED=true" >> "$GITHUB_ENV"
else
echo "SHA256 already correct"
fi
- name: Commit changes
if: env.HASH_CHANGED == 'true'
run: |
if ! git diff --quiet openvpn-install.sh; then
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add openvpn-install.sh
git commit -m "chore: update Easy-RSA SHA256 hash"
git push
else
echo "No changes to commit"
fi

8
.trivyignore Normal file
View File

@@ -0,0 +1,8 @@
# Test containers require root for OpenVPN NET_ADMIN capability
AVD-DS-0002
# Test containers don't need healthcheck
AVD-DS-0026
# False positive: yum clean all is present in the conditional but Trivy doesn't detect it
AVD-DS-0015

7
AGENTS.md Normal file
View File

@@ -0,0 +1,7 @@
- Use gh CLI to interact with GitHub
- Test locally using the Docker setup when needed
- When doing changes, check if README/FAQ and tests needs to be updated
- Remember the script and documentation needs to be accessible to a moderately technical audience
- Keep PR description concise (no test plan)
- Don't use gh cli to post comments on the developer's behalf
- Don't amend commits and force push unless told otherwise

48
FAQ.md
View File

@@ -8,6 +8,14 @@ You can, of course, it's even recommended, update the `openvpn` package with you
---
**Q:** How do I renew certificates before they expire?
**A:** Run the script again and select "Renew certificates" from the menu. You can renew either client certificates or the server certificate. The script will show you the current expiration date for each certificate and let you choose a new validity period (default: 3650 days / 10 years).
For client renewals, a new `.ovpn` file will be generated that you need to distribute to the client. For server renewals, the OpenVPN service will need to be restarted (the script will prompt you).
---
**Q:** How do I check for DNS leaks?
**A:** Go to [browserleaks.com](https://browserleaks.com/dns) or [ipleak.net](https://ipleak.net/) (both perform IPv4 and IPv6 check) with your browser. Your IP should not show up (test without and without the VPN). The DNS servers should be the ones you selected during the setup, not your IP address nor your ISP's DNS servers' addresses.
@@ -27,7 +35,7 @@ up /etc/openvpn/update-resolv-conf
down /etc/openvpn/update-resolv-conf
```
Centos 6, 7
CentOS 6, 7
```
script-security 2
@@ -35,7 +43,7 @@ up /usr/share/doc/openvpn-2.4.8/contrib/pull-resolv-conf/client.up
down /usr/share/doc/openvpn-2.4.8/contrib/pull-resolv-conf/client.down
```
Centos 8, Fedora 30, 31
CentOS 8, Fedora 30, 31
```
script-security 2
@@ -63,7 +71,7 @@ down /usr/share/openvpn/contrib/pull-resolv-conf/client.down
- AES CBC
- tls-auth
If your client is <2.3.3, remove `tls-version-min 1.2` from your `/etc/openvpn/server.conf` and `.ovpn` files.
If your client is <2.3.3, remove `tls-version-min 1.2` from your `/etc/openvpn/server/server.conf` and `.ovpn` files.
---
@@ -83,7 +91,7 @@ If your client is <2.3.3, remove `tls-version-min 1.2` from your `/etc/openvpn/s
**A:** Iptables rules are saved at `/etc/iptables/add-openvpn-rules.sh` and `/etc/iptables/rm-openvpn-rules.sh`. They are managed by the service `/etc/systemd/system/iptables-openvpn.service`
Sysctl options are at `/etc/sysctl.d/20-openvpn.conf`
Sysctl options are at `/etc/sysctl.d/99-openvpn.conf`
---
@@ -109,13 +117,13 @@ Sysctl options are at `/etc/sysctl.d/20-openvpn.conf`
**Q:** How can I access computers the OpenVPN server's remote LAN?
**A:** Add a route with the subnet of the remote network to `/etc/openvpn/server.conf` and restart openvpn. Example: `push "route 192.168.1.0 255.255.255.0"` if the server's LAN is `192.168.1.0/24`
**A:** Add a route with the subnet of the remote network to `/etc/openvpn/server/server.conf` and restart OpenVPN. Example: `push "route 192.168.1.0 255.255.255.0"` if the server's LAN is `192.168.1.0/24`
---
**Q:** How can I add multiple users in one go?
**A:** Here is a sample bash script to achieve this:
**A:** Here is a sample Bash script to achieve this:
```sh
userlist=(user1 user2 user3)
@@ -137,17 +145,39 @@ done < users.txt
**Q:** How do I change the default `.ovpn` file created for future clients?
**A:** You can edit the template out of which `.ovpn` files are created by editing `/etc/openvpn/client-template.txt`
**A:** You can edit the template out of which `.ovpn` files are created by editing `/etc/openvpn/server/client-template.txt`
---
**Q:** For my clients - I want to set my internal network to pass through the VPN and the rest to go through my internet?
**A:** You would need to edit the `.ovpn` file. You can edit the template out of which those files are created by editing `/etc/openvpn/client-template.txt` file and adding
**A:** You would need to edit the `.ovpn` file. You can edit the template out of which those files are created by editing `/etc/openvpn/server/client-template.txt` file and adding
```sh
route-nopull
route 10.0.0.0 255.0.0.0
```
So for example - here it would route all traffic of `10.0.0.0/8` to the vpn. And the rest through the internet.
So for example - here it would route all traffic of `10.0.0.0/8` to the VPN. And the rest through the internet.
---
**Q:** I have enabled IPv6 and my VPN client gets an IPv6 address. Why do I reach the sites or other dual-stacked destinations via IPv4 only?
**A:** This is because inside the tunnel you don't get a publicly routable IPv6 address, instead you get an ULA (Unlique Local Lan) address. Operating systems don't prefer this all the time. You can fix this in your operating system policies as it's unrelated to the VPN itself:
Windows (commands needs to run cmd.exe as Administrator):
```
netsh interface ipv6 add prefixpolicy fd00::/8 3 1
```
Linux:
edit `/etc/gai.conf` and uncomment the following line and also change its value to `1`:
```
label fc00::/7 1
```
This will not work properly unless you add you your VPN server `server.conf` one or two lines to push at least 1 (one) IPv6 DNS server. Most providers have IPv6 servers as well, add two more lines of `push "dhcp-option DNS <IPv6>"`

140
Makefile Normal file
View File

@@ -0,0 +1,140 @@
.PHONY: test test-build test-up test-down test-logs test-clean
# Run the full test suite
test: test-build test-up
@echo "Waiting for tests to complete..."
@for i in $$(seq 1 180); do \
if docker logs openvpn-client 2>&1 | grep -q "ALL TESTS PASSED"; then \
echo "✓ Tests passed!"; \
$(MAKE) test-down; \
exit 0; \
fi; \
if docker logs openvpn-client 2>&1 | grep -q "FAIL:"; then \
echo "✗ Tests failed!"; \
docker logs openvpn-client; \
$(MAKE) test-down; \
exit 1; \
fi; \
echo "Waiting... ($$i/180)"; \
sleep 2; \
done; \
echo "Timeout waiting for tests"; \
$(MAKE) test-down; \
exit 1
# Build test containers
test-build:
BASE_IMAGE=$(BASE_IMAGE) docker compose build
# Start test containers
test-up:
docker compose up -d
# Stop and remove test containers
test-down:
docker compose down -v --remove-orphans
# View logs
test-logs:
docker compose logs -f
# View server logs only
test-logs-server:
docker logs -f openvpn-server
# View client logs only
test-logs-client:
docker logs -f openvpn-client
# Full cleanup
test-clean: test-down
docker rmi openvpn-install-openvpn-server openvpn-install-openvpn-client 2>/dev/null || true
docker volume prune -f
# Interactive shell into server container
test-shell-server:
docker exec -it openvpn-server /bin/bash
# Interactive shell into client container
test-shell-client:
docker exec -it openvpn-client /bin/bash
# Test specific distributions
test-ubuntu-18.04:
$(MAKE) test BASE_IMAGE=ubuntu:18.04
test-ubuntu-20.04:
$(MAKE) test BASE_IMAGE=ubuntu:20.04
test-ubuntu-22.04:
$(MAKE) test BASE_IMAGE=ubuntu:22.04
test-ubuntu-24.04:
$(MAKE) test BASE_IMAGE=ubuntu:24.04
test-debian-11:
$(MAKE) test BASE_IMAGE=debian:11
test-debian-12:
$(MAKE) test BASE_IMAGE=debian:12
test-fedora-40:
$(MAKE) test BASE_IMAGE=fedora:40
test-fedora-41:
$(MAKE) test BASE_IMAGE=fedora:41
test-rocky-8:
$(MAKE) test BASE_IMAGE=rockylinux:8
test-rocky-9:
$(MAKE) test BASE_IMAGE=rockylinux:9
test-almalinux-8:
$(MAKE) test BASE_IMAGE=almalinux:8
test-almalinux-9:
$(MAKE) test BASE_IMAGE=almalinux:9
test-oracle-8:
$(MAKE) test BASE_IMAGE=oraclelinux:8
test-oracle-9:
$(MAKE) test BASE_IMAGE=oraclelinux:9
test-amazon-2023:
$(MAKE) test BASE_IMAGE=amazonlinux:2023
test-arch:
$(MAKE) test BASE_IMAGE=archlinux:latest
test-centos-stream-9:
$(MAKE) test BASE_IMAGE=quay.io/centos/centos:stream9
test-opensuse-leap:
$(MAKE) test BASE_IMAGE=opensuse/leap:16.0
test-opensuse-tumbleweed:
$(MAKE) test BASE_IMAGE=opensuse/tumbleweed
# Test all distributions (runs sequentially)
test-all:
$(MAKE) test-ubuntu-18.04
$(MAKE) test-ubuntu-20.04
$(MAKE) test-ubuntu-22.04
$(MAKE) test-ubuntu-24.04
$(MAKE) test-debian-11
$(MAKE) test-debian-12
$(MAKE) test-fedora-40
$(MAKE) test-fedora-41
$(MAKE) test-rocky-8
$(MAKE) test-rocky-9
$(MAKE) test-almalinux-8
$(MAKE) test-almalinux-9
$(MAKE) test-oracle-8
$(MAKE) test-oracle-9
$(MAKE) test-amazon-2023
$(MAKE) test-arch
$(MAKE) test-centos-stream-9
$(MAKE) test-opensuse-leap
$(MAKE) test-opensuse-tumbleweed

177
README.md
View File

@@ -2,17 +2,44 @@
![Test](https://github.com/angristan/openvpn-install/workflows/Test/badge.svg)
![Lint](https://github.com/angristan/openvpn-install/workflows/Lint/badge.svg)
![visitors](https://visitor-badge.glitch.me/badge?page_id=angristan.openvpn-install)
[![Say Thanks!](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/angristan)
OpenVPN installer for Debian, Ubuntu, Fedora, CentOS, Arch Linux, Oracle Linux, Rocky Linux and AlmaLinux.
OpenVPN installer for Debian, Ubuntu, Fedora, openSUSE, CentOS, Amazon Linux, Arch Linux, Oracle Linux, Rocky Linux and AlmaLinux.
This script will let you setup your own secure VPN server in just a few seconds.
You can also check out [wireguard-install](https://github.com/angristan/wireguard-install), a simple installer for a simpler, safer, faster and more modern VPN protocol.
## What is this?
This script is meant to be run on your own server, whether it's a VPS or a dedicated server, or even a computer at home.
Once set up, you will be able to generate client configuration files for every device you want to connect.
Each client will be able to route its internet traffic through the server, fully encrypted.
```mermaid
graph LR
A[Phone] -->|Encrypted| VPN
B[Laptop] -->|Encrypted| VPN
C[Computer] -->|Encrypted| VPN
VPN[OpenVPN Server]
VPN --> I[Internet]
```
## Why OpenVPN?
OpenVPN was the de facto standard for open-source VPNs when this script was created. WireGuard came later and is simpler and faster for most use cases. Check out [wireguard-install](https://github.com/angristan/wireguard-install).
That said, OpenVPN still makes sense when you need:
- **TCP support**: works in restrictive environments where UDP is blocked (corporate networks, airports, hotels, etc.)
- **Password-protected private keys**: WireGuard configs store the private key in plain text
- **Legacy compatibility**: clients exist for pretty much every platform, including older systems
## Usage
First, get the script and make it executable:
First, on your server, get the script and make it executable:
```bash
curl -O https://raw.githubusercontent.com/angristan/openvpn-install/master/openvpn-install.sh
@@ -33,13 +60,12 @@ When OpenVPN is installed, you can run the script again, and you will get the ch
- Add a client
- Remove a client
- Renew certificates (client or server)
- Uninstall OpenVPN
In your home directory, you will have `.ovpn` files. These are the client configuration files. Download them from your server and connect using your favorite OpenVPN client.
If you have any question, head to the [FAQ](#faq) first. Please read everything before opening an issue.
**PLEASE do not send me emails or private messages asking for help.** The only place to get help is the issues. Other people may be able to help and in the future, other users may also run into the same issue as you. My time is not available for free just for you, you're not special.
If you have any question, head to the [FAQ](#faq) first. And if you need help, you can open a [discussion](https://github.com/angristan/openvpn-install/discussions). Please search existing issues and dicussions first.
### Headless install
@@ -70,6 +96,9 @@ If you want to customise your installation, you can export them or specify them
- `CUSTOMIZE_ENC=n`
- `CLIENT=clientname`
- `PASS=1`
- `MULTI_CLIENT=n`
- `CLIENT_CERT_DURATION_DAYS=3650`
- `SERVER_CERT_DURATION_DAYS=3650`
If the server is behind NAT, you can specify its endpoint with the `ENDPOINT` variable. If the endpoint is the public IP address which it is behind, you can use `ENDPOINT=$(curl -4 ifconfig.co)` (the script will default to this). The endpoint can be an IPv4 or a domain.
@@ -96,6 +125,8 @@ export PASS="1"
## Features
- Installs and configures a ready-to-use OpenVPN server
- Certificate renewal for both client and server certificates
- Uses [official OpenVPN repositories](https://community.openvpn.net/openvpn/wiki/OpenvpnSoftwareRepos) when possible for the latest stable releases
- Iptables rules and forwarding managed in a seamless way
- If needed, the script can cleanly remove OpenVPN, including configuration and iptables rules
- Customisable encryption settings, enhanced default settings (see [Security and Encryption](#security-and-encryption) below)
@@ -109,31 +140,32 @@ export PASS="1"
- Block DNS leaks on Windows 10
- Randomised server certificate name
- Choice to protect clients with a password (private key encryption)
- Option to allow multiple devices to use the same client profile simultaneously (disables persistent IP addresses)
- Many other little things!
## Compatibility
The script supports these OS and architectures:
The script supports these Linux distributions:
| | i386 | amd64 | armhf | arm64 |
| --------------- | ---- | ----- | ----- | ----- |
| Amazon Linux 2 | ❔ | ✅ | ❔ | ❔ |
| Arch Linux | ❔ | ✅ | ❔ | ✅ |
| CentOS 7 | ✅ | ✅ | ✅ | ✅ |
| CentOS 8 | ❌ | ✅ | ❌ | ✅ |
| Debian >= 9 | ✅ | ✅ | ✅ | ✅ |
| Fedora >= 27 | ❔ | ✅ | ❔ | ❔ |
| Ubuntu 16.04 | ✅ | ✅ | ❌ | ❌ |
| Ubuntu >= 18.04 | ✅ | ✅ | ✅ | ✅ |
| Oracle Linux 8 | ❌ | ✅ | ❌ | ❔ |
| Rocky Linux 8 | | ✅ | ❔ | ❔ |
| AlmaLinux 8 | ❌ | ✅ | ❌ | ❔ |
| | Support |
| ------------------- | ------- |
| AlmaLinux >= 8 | ✅ 🤖 |
| Amazon Linux 2023 | ✅ 🤖 |
| Arch Linux | ✅ 🤖 |
| CentOS Stream >= 8 | ✅ 🤖 |
| Debian >= 11 | ✅ 🤖 |
| Fedora >= 40 | ✅ 🤖 |
| openSUSE Leap >= 16 | ✅ 🤖 |
| openSUSE Tumbleweed | ✅ 🤖 |
| Oracle Linux >= 8 | ✅ 🤖 |
| Rocky Linux >= 8 | ✅ 🤖 |
| Ubuntu >= 18.04 | ✅ 🤖 |
To be noted:
- It should work on Debian 8+ and Ubuntu 16.04+. But versions not in the table above are not officially supported.
- The script is regularly tested against the distributions marked with a 🤖 only.
- It's only tested on `amd64` architecture.
- The script requires `systemd`.
- The script is regularly tested against `amd64` only.
## Fork
@@ -149,10 +181,9 @@ More Q&A in [FAQ.md](FAQ.md).
**A:** I recommend these:
- [Vultr](https://www.vultr.com/?ref=8537055-6G): Worldwide locations, IPv6 support, starting at \$3.50/month
- [Hetzner](https://hetzner.cloud/?ref=ywtlvZsjgeDq): Germany, IPv6, 20 TB of traffic, starting at €3/month
- [Digital Ocean](https://goo.gl/qXrNLK): Worldwide locations, IPv6 support, starting at \$5/month
- [PulseHeberg](https://goo.gl/76yqW5): France, unlimited bandwidth, starting at €3/month
- [Vultr](https://www.vultr.com/?ref=8948982-8H): Worldwide locations, IPv6 support, starting at \$5/month
- [Hetzner](https://hetzner.cloud/?ref=ywtlvZsjgeDq): Germany, Finland and USA. IPv6, 20 TB of traffic, starting at 4.5€/month
- [Digital Ocean](https://m.do.co/c/ed0ba143fe53): Worldwide locations, IPv6 support, starting at \$4/month
---
@@ -162,7 +193,7 @@ More Q&A in [FAQ.md](FAQ.md).
- Windows: [The official OpenVPN community client](https://openvpn.net/index.php/download/community-downloads.html).
- Linux: The `openvpn` package from your distribution. There is an [official APT repository](https://community.openvpn.net/openvpn/wiki/OpenvpnSoftwareRepos) for Debian/Ubuntu based distributions.
- macOS: [Tunnelblick](https://tunnelblick.net/), [Viscosity](https://www.sparklabs.com/viscosity/).
- macOS: [Tunnelblick](https://tunnelblick.net/), [Viscosity](https://www.sparklabs.com/viscosity/), [OpenVPN for Mac](https://openvpn.net/client-connect-vpn-for-mac-os/).
- Android: [OpenVPN for Android](https://play.google.com/store/apps/details?id=de.blinkt.openvpn).
- iOS: [The official OpenVPN Connect client](https://itunes.apple.com/us/app/openvpn-connect/id590379981).
@@ -182,38 +213,46 @@ More Q&A in [FAQ.md](FAQ.md).
More Q&A in [FAQ.md](FAQ.md).
## One-stop solutions for public cloud
Solutions that provision a ready to use OpenVPN server based on this script in one go are available for:
- AWS using Terraform at [`openvpn-terraform-install`](https://github.com/dumrauf/openvpn-terraform-install)
- Terraform AWS module [`openvpn-ephemeral`](https://registry.terraform.io/modules/paulmarsicloud/openvpn-ephemeral/aws/latest)
## Contributing
## Discuss changes
Please open an issue before submitting a PR if you want to discuss a change, especially if it's a big one.
### Code formatting
We use [shellcheck](https://github.com/koalaman/shellcheck) and [shfmt](https://github.com/mvdan/sh) to enforce bash styling guidelines and good practices. They are executed for each commit / PR with GitHub Actions, so you can check the configuration [here](https://github.com/angristan/openvpn-install/blob/master/.github/workflows/push.yml).
We use [shellcheck](https://github.com/koalaman/shellcheck) and [shfmt](https://github.com/mvdan/sh) to enforce Bash styling guidelines and good practices. They are executed for each commit / PR with GitHub Actions, so you can check the [lint workflow configuration](https://github.com/angristan/openvpn-install/blob/master/.github/workflows/lint.yml).
## Security and Encryption
OpenVPN's default settings are pretty weak regarding encryption. This script aims to improve that.
> [!NOTE]
> This script was created in 2016 when OpenVPN's defaults were quite weak. Back then, customising encryption settings was essential for a secure setup. Since then, OpenVPN has significantly improved its defaults, but the script still offers customisation options.
OpenVPN 2.4 was a great update regarding encryption. It added support for ECDSA, ECDH, AES GCM, NCP and tls-crypt.
OpenVPN 2.3 and earlier shipped with outdated defaults like Blowfish (BF-CBC), TLS 1.0, and SHA1. Each major release since has brought significant improvements:
- **OpenVPN 2.4** (2016): Added ECDSA, ECDH, AES-GCM, NCP (cipher negotiation), and tls-crypt
- **OpenVPN 2.5** (2020): Default cipher changed from BF-CBC to AES-256-GCM:AES-128-GCM, added ChaCha20-Poly1305, tls-crypt-v2, and TLS 1.3 support
- **OpenVPN 2.6** (2023): TLS 1.2 minimum by default, compression blocked by default, `--peer-fingerprint` for PKI-less setups, and DCO kernel acceleration
If you want more information about an option mentioned below, head to the [OpenVPN manual](https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage). It is very complete.
Most of OpenVPN's encryption-related stuff is managed by [Easy-RSA](https://github.com/OpenVPN/easy-rsa). Defaults parameters are in the [vars.example](https://github.com/OpenVPN/easy-rsa/blob/v3.0.7/easyrsa3/vars.example) file.
Certificate and PKI management is handled by [Easy-RSA](https://github.com/OpenVPN/easy-rsa). Default parameters are in the [vars.example](https://github.com/OpenVPN/easy-rsa/blob/v3.2.2/easyrsa3/vars.example) file.
### Compression
> [!NOTE]
> OpenVPN 2.6+ defaults `--allow-compression` to `no`, which blocks even server-pushed compression. Prior versions allowed servers to push compression settings to clients.
By default, OpenVPN doesn't enable compression. This script provides support for LZ0 and LZ4 (v1/v2) algorithms, the latter being more efficient.
However, it is discouraged to use compression since the [VORACLE attack](https://protonvpn.com/blog/voracle-attack/) makes use of it.
### TLS version
OpenVPN accepts TLS 1.0 by default, which is nearly [20 years old](https://en.wikipedia.org/wiki/Transport_Layer_Security#TLS_1.0).
> [!NOTE]
> OpenVPN 2.6+ defaults to TLS 1.2 minimum. Prior versions accepted TLS 1.0 by default.
OpenVPN 2.5 and earlier accepted TLS 1.0 by default, which is nearly [20 years old](https://en.wikipedia.org/wiki/Transport_Layer_Security#TLS_1.0).
With `tls-version-min 1.2` we enforce TLS 1.2, which the best protocol available currently for OpenVPN.
@@ -236,7 +275,10 @@ OpenVPN uses `SHA-256` as the signature hash by default, and so does the script.
### Data channel
By default, OpenVPN uses `BF-CBC` as the data channel cipher. Blowfish is an old (1993) and weak algorithm. Even the official OpenVPN documentation admits it.
> [!NOTE]
> The default data channel cipher changed in OpenVPN 2.5. Prior versions defaulted to `BF-CBC`, while OpenVPN 2.5+ defaults to `AES-256-GCM:AES-128-GCM`. OpenVPN 2.6+ also includes `CHACHA20-POLY1305` in the default cipher list when available.
By default, OpenVPN 2.4 and earlier used `BF-CBC` as the data channel cipher. Blowfish is an old (1993) and weak algorithm. Even the official OpenVPN documentation admits it.
> The default is BF-CBC, an abbreviation for Blowfish in Cipher Block Chaining mode.
>
@@ -253,6 +295,8 @@ AES-256 is 40% slower than AES-128, and there isn't any real reason to use a 256
AES-GCM is an [AEAD cipher](https://en.wikipedia.org/wiki/Authenticated_encryption) which means it simultaneously provides confidentiality, integrity, and authenticity assurances on the data.
ChaCha20-Poly1305 is another AEAD cipher that provides similar security to AES-GCM. It is particularly useful on devices without hardware AES acceleration (AES-NI), such as older CPUs and many ARM-based devices, where it can be significantly faster than AES.
The script supports the following ciphers:
- `AES-128-GCM`
@@ -261,10 +305,11 @@ The script supports the following ciphers:
- `AES-128-CBC`
- `AES-192-CBC`
- `AES-256-CBC`
- `CHACHA20-POLY1305` (requires OpenVPN 2.5+)
And defaults to `AES-128-GCM`.
OpenVPN 2.4 added a feature called "NCP": _Negotiable Crypto Parameters_. It means you can provide a cipher suite like with HTTPS. It is set to `AES-256-GCM:AES-128-GCM` by default and overrides the `--cipher` parameter when used with an OpenVPN 2.4 client. For the sake of simplicity, the script set both the `--cipher` and `--ncp-cipher` to the cipher chosen above.
OpenVPN 2.4 added a feature called "NCP": _Negotiable Crypto Parameters_. It means you can provide a cipher suite like with HTTPS. It is set to `AES-256-GCM:AES-128-GCM` by default and overrides the `--cipher` parameter when used with an OpenVPN 2.4 client. For the sake of simplicity, the script sets `--cipher` (fallback for non-NCP clients), `--data-ciphers` (modern OpenVPN 2.5+ naming), and `--ncp-ciphers` (legacy alias for OpenVPN 2.4 compatibility) to the cipher chosen above.
### Control channel
@@ -275,9 +320,11 @@ The script proposes the following options, depending on the certificate:
- ECDSA:
- `TLS-ECDHE-ECDSA-WITH-AES-128-GCM-SHA256`
- `TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA384`
- `TLS-ECDHE-ECDSA-WITH-CHACHA20-POLY1305-SHA256` (requires OpenVPN 2.5+)
- RSA:
- `TLS-ECDHE-RSA-WITH-AES-128-GCM-SHA256`
- `TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384`
- `TLS-ECDHE-RSA-WITH-CHACHA20-POLY1305-SHA256` (requires OpenVPN 2.5+)
It defaults to `TLS-ECDHE-*-WITH-AES-128-GCM-SHA256`.
@@ -312,7 +359,7 @@ The script provides the following choices:
It defaults to `SHA256`.
### `tls-auth` and `tls-crypt`
### `tls-auth`, `tls-crypt`, and `tls-crypt-v2`
From the OpenVPN wiki, about `tls-auth`:
@@ -334,14 +381,54 @@ So both provide an additional layer of security and mitigate DoS attacks. They a
`tls-crypt` is an OpenVPN 2.4 feature that provides encryption in addition to authentication (unlike `tls-auth`). It is more privacy-friendly.
The script supports both and uses `tls-crypt` by default.
`tls-crypt-v2` is an OpenVPN 2.5 feature that builds on `tls-crypt` by using **per-client keys** instead of a shared key. Each client receives a unique key derived from a server key. This provides:
- **Better security**: If a client key is compromised, other clients are not affected
- **Easier key management**: Client keys can be revoked individually without regenerating the server key
- **Scalability**: Better suited for large deployments with many clients
The script supports all three options:
- `tls-crypt-v2` (default): Per-client keys for better security
- `tls-crypt`: Shared key for all clients, compatible with OpenVPN 2.4+
- `tls-auth`: HMAC authentication only (no encryption), compatible with older clients
### Certificate type verification (`remote-cert-tls`)
The server is configured with `remote-cert-tls client`, which requires connecting peers to have a certificate with the "TLS Web Client Authentication" extended key usage. This prevents a server certificate from being used to impersonate a client.
Similarly, clients are configured with `remote-cert-tls server` to ensure they only connect to servers presenting valid server certificates. This protects against an attacker with a valid client certificate setting up a rogue server.
### Data Channel Offload (DCO)
[Data Channel Offload](https://openvpn.net/as-docs/openvpn-data-channel-offload.html) (DCO) is a kernel acceleration feature that significantly improves OpenVPN performance by keeping data channel encryption/decryption in kernel space, eliminating costly context switches between user and kernel space for each packet.
DCO was merged into the Linux kernel 6.16 (April 2025).
**Requirements:**
- OpenVPN 2.6.0 or later
- Linux kernel 6.16+ (built-in) or `ovpn-dco` kernel module
- UDP protocol (TCP is not supported)
- AEAD cipher (`AES-128-GCM`, `AES-256-GCM`, or `CHACHA20-POLY1305`)
- Compression disabled
The script's default settings (AES-128-GCM, UDP, no compression) are DCO-compatible. When DCO is available and the configuration is compatible, OpenVPN will automatically use it for improved performance.
**Note:** DCO must be supported on both the server and the client for full acceleration. Client support is available in OpenVPN 2.6+ (Linux, Windows, FreeBSD) and OpenVPN Connect 3.4+ (Windows). macOS does not currently support DCO, but clients can still connect to DCO-enabled servers with partial performance benefits on the server-side.
The script will display the DCO availability status during installation.
## Say thanks
You can [say thanks](https://saythanks.io/to/angristan%40pm.me) if you want!
You can [say thanks](https://saythanks.io/to/angristan) if you want!
## Credits & Licence
Many thanks to the [contributors](https://github.com/Angristan/OpenVPN-install/graphs/contributors) and Nyr's original work.
This project is under the [MIT Licence](https://raw.githubusercontent.com/Angristan/openvpn-install/master/LICENSE)
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=angristan/openvpn-install&type=Date)](https://star-history.com/#angristan/openvpn-install&Date)

7
biome.json Normal file
View File

@@ -0,0 +1,7 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"formatter": {
"indentStyle": "space",
"indentWidth": 2
}
}

60
docker-compose.yml Normal file
View File

@@ -0,0 +1,60 @@
---
services:
openvpn-server:
build:
context: .
dockerfile: test/Dockerfile.server
args:
BASE_IMAGE: ${BASE_IMAGE:-ubuntu:24.04}
container_name: openvpn-server
hostname: openvpn-server
privileged: true
cgroupns: host
devices:
- /dev/net/tun:/dev/net/tun
sysctls:
- net.ipv4.ip_forward=1
volumes:
- shared-config:/shared
- /sys/fs/cgroup:/sys/fs/cgroup:rw
tmpfs:
- /run
- /run/lock
networks:
vpn-test:
ipv4_address: 172.28.0.10
stop_signal: SIGRTMIN+3
healthcheck:
test: ["CMD", "pgrep", "openvpn"]
interval: 5s
timeout: 3s
retries: 30
openvpn-client:
build:
context: .
dockerfile: test/Dockerfile.client
container_name: openvpn-client
hostname: openvpn-client
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
volumes:
- shared-config:/shared
networks:
vpn-test:
ipv4_address: 172.28.0.20
depends_on:
openvpn-server:
condition: service_healthy
volumes:
shared-config:
networks:
vpn-test:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/24

File diff suppressed because it is too large Load Diff

17
renovate.json Normal file
View File

@@ -0,0 +1,17 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"ignorePaths": [".github/workflows/do-test.yml"],
"customManagers": [
{
"customType": "regex",
"managerFilePatterns": ["/^openvpn-install\\.sh$/"],
"matchStrings": [
"readonly\\s+EASYRSA_VERSION=\"(?<currentValue>\\d+\\.\\d+\\.\\d+)\""
],
"depNameTemplate": "OpenVPN/easy-rsa",
"datasourceTemplate": "github-releases",
"extractVersionTemplate": "^v(?<version>.*)$"
}
]
}

26
test/Dockerfile.client Normal file
View File

@@ -0,0 +1,26 @@
# checkov:skip=CKV_DOCKER_2:Test container doesn't need healthcheck
# checkov:skip=CKV_DOCKER_3:OpenVPN client requires root for NET_ADMIN
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
# Install OpenVPN client and testing tools
# dnsutils provides dig for DNS testing with Unbound
RUN apt-get update && apt-get install -y --no-install-recommends \
openvpn \
iproute2 \
iputils-ping \
procps \
dnsutils \
&& rm -rf /var/lib/apt/lists/*
# Create TUN device directory (device will be mounted at runtime)
RUN mkdir -p /dev/net
# Copy test scripts
COPY test/client-entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
WORKDIR /etc/openvpn
ENTRYPOINT ["/entrypoint.sh"]

69
test/Dockerfile.server Normal file
View File

@@ -0,0 +1,69 @@
# checkov:skip=CKV_DOCKER_2:Test container doesn't need healthcheck
# checkov:skip=CKV_DOCKER_3:OpenVPN server requires root for NET_ADMIN
# checkov:skip=CKV_DOCKER_7:Base image is parameterized, some use latest tag
ARG BASE_IMAGE=ubuntu:24.04
FROM ${BASE_IMAGE}
ARG BASE_IMAGE
ENV DEBIAN_FRONTEND=noninteractive
# Install basic dependencies based on the OS
# dnsutils/bind-utils provides dig for DNS testing with Unbound
RUN if command -v apt-get >/dev/null; then \
apt-get update && apt-get install -y --no-install-recommends \
iproute2 iptables curl procps systemd systemd-sysv dnsutils \
&& rm -rf /var/lib/apt/lists/*; \
elif command -v dnf >/dev/null; then \
dnf install -y --allowerasing \
iproute iptables curl procps-ng systemd tar gzip bind-utils \
&& dnf clean all; \
elif command -v yum >/dev/null; then \
yum install -y \
iproute iptables curl procps-ng systemd tar gzip bind-utils \
&& yum clean all; \
elif command -v pacman >/dev/null; then \
pacman -Syu --noconfirm \
iproute2 iptables curl procps-ng bind \
&& pacman -Scc --noconfirm; \
elif command -v zypper >/dev/null; then \
zypper install -y \
iproute2 iptables curl procps systemd tar gzip bind-utils gawk \
&& zypper clean -a; \
fi
# Create TUN device (will be mounted at runtime)
RUN mkdir -p /dev/net
# Copy the install script
COPY openvpn-install.sh /opt/openvpn-install.sh
RUN chmod +x /opt/openvpn-install.sh
# Copy test scripts
COPY test/server-entrypoint.sh /entrypoint.sh
COPY test/validate-output.sh /opt/test/validate-output.sh
RUN chmod +x /entrypoint.sh /opt/test/validate-output.sh
# Create systemd service for the test script
RUN printf '%s\n' \
'[Unit]' \
'Description=OpenVPN Installation Test' \
'After=network.target' \
'' \
'[Service]' \
'Type=oneshot' \
'Environment=HOME=/root' \
'WorkingDirectory=/root' \
'ExecStart=/entrypoint.sh' \
'RemainAfterExit=yes' \
'StandardOutput=journal+console' \
'StandardError=journal+console' \
'' \
'[Install]' \
'WantedBy=multi-user.target' \
> /etc/systemd/system/openvpn-test.service \
&& systemctl enable openvpn-test.service
WORKDIR /opt
STOPSIGNAL SIGRTMIN+3
CMD ["/sbin/init"]

368
test/client-entrypoint.sh Executable file
View File

@@ -0,0 +1,368 @@
#!/bin/bash
set -e
echo "=== OpenVPN Client Container ==="
# Create TUN device if it doesn't exist
if [ ! -c /dev/net/tun ]; then
mkdir -p /dev/net
mknod /dev/net/tun c 10 200
chmod 600 /dev/net/tun
fi
echo "TUN device ready"
# Wait for client config to be available
echo "Waiting for client config..."
MAX_WAIT=120
WAITED=0
while [ ! -f /shared/client.ovpn ] && [ $WAITED -lt $MAX_WAIT ]; do
sleep 2
WAITED=$((WAITED + 2))
echo "Waiting... ($WAITED/$MAX_WAIT seconds)"
done
if [ ! -f /shared/client.ovpn ]; then
echo "ERROR: Client config not found after ${MAX_WAIT}s"
exit 1
fi
echo "Client config found!"
cat /shared/client.ovpn
# Connect to VPN
echo "Connecting to OpenVPN server..."
openvpn --config /shared/client.ovpn --daemon --log /var/log/openvpn.log
# Wait for connection
echo "Waiting for VPN connection..."
MAX_WAIT=60
WAITED=0
while ! ip addr show tun0 2>/dev/null | grep -q "inet " && [ $WAITED -lt $MAX_WAIT ]; do
sleep 2
WAITED=$((WAITED + 2))
echo "Waiting for tun0... ($WAITED/$MAX_WAIT seconds)"
# Check for errors
if [ -f /var/log/openvpn.log ]; then
tail -5 /var/log/openvpn.log
fi
done
if ! ip addr show tun0 2>/dev/null | grep -q "inet "; then
echo "ERROR: VPN connection failed"
echo "=== OpenVPN log ==="
cat /var/log/openvpn.log || true
exit 1
fi
echo "=== VPN Connected! ==="
ip addr show tun0
# Run connectivity tests
echo ""
echo "=== Running connectivity tests ==="
# Test 1: Check tun0 interface
echo "Test 1: Checking tun0 interface..."
if ip addr show tun0 | grep -q "10.8.0"; then
echo "PASS: tun0 interface has correct IP range (10.8.0.x)"
else
echo "FAIL: tun0 interface doesn't have expected IP"
exit 1
fi
# Test 2: Ping VPN gateway
echo "Test 2: Pinging VPN gateway (10.8.0.1)..."
if ping -c 10 10.8.0.1; then
echo "PASS: Can ping VPN gateway"
else
echo "FAIL: Cannot ping VPN gateway"
exit 1
fi
# Test 3: DNS resolution through Unbound
echo "Test 3: Testing DNS resolution via Unbound (10.8.0.1)..."
DNS_SUCCESS=false
DNS_MAX_RETRIES=10
for i in $(seq 1 $DNS_MAX_RETRIES); do
DIG_OUTPUT=$(dig @10.8.0.1 example.com +short +time=5 2>&1)
if [ -n "$DIG_OUTPUT" ] && ! echo "$DIG_OUTPUT" | grep -qi "timed out\|SERVFAIL\|connection refused"; then
DNS_SUCCESS=true
break
fi
echo "DNS attempt $i failed:"
echo "$DIG_OUTPUT"
sleep 2
done
if [ "$DNS_SUCCESS" = true ]; then
echo "PASS: DNS resolution through Unbound works"
echo "Resolved example.com to: $(dig @10.8.0.1 example.com +short +time=5)"
else
echo "FAIL: DNS resolution through Unbound failed after $DNS_MAX_RETRIES attempts"
dig @10.8.0.1 example.com +time=5 || true
exit 1
fi
echo ""
echo "=== Initial connectivity tests PASSED ==="
# Signal server that initial tests passed
touch /shared/initial-tests-passed
# =====================================================
# Certificate Revocation E2E Tests
# =====================================================
echo ""
echo "=== Starting Certificate Revocation E2E Tests ==="
REVOKE_CLIENT="revoketest"
# Wait for revoke test client config
echo "Waiting for revoke test client config..."
MAX_WAIT=120
WAITED=0
while [ ! -f /shared/revoke-client-config-ready ] && [ $WAITED -lt $MAX_WAIT ]; do
sleep 2
WAITED=$((WAITED + 2))
echo "Waiting for revoke test config... ($WAITED/$MAX_WAIT seconds)"
done
if [ ! -f /shared/revoke-client-config-ready ]; then
echo "FAIL: Revoke test client config not ready in time"
exit 1
fi
if [ ! -f "/shared/$REVOKE_CLIENT.ovpn" ]; then
echo "FAIL: Revoke test client config file not found"
exit 1
fi
echo "Revoke test client config found!"
# Disconnect current VPN (testclient) before connecting with revoke test client
echo "Disconnecting current VPN connection..."
pkill openvpn || true
sleep 2
# Connect with revoke test client
echo "Connecting with '$REVOKE_CLIENT' certificate..."
openvpn --config "/shared/$REVOKE_CLIENT.ovpn" --daemon --log /var/log/openvpn-revoke.log
# Wait for connection
echo "Waiting for VPN connection with revoke test client..."
MAX_WAIT=60
WAITED=0
while ! ip addr show tun0 2>/dev/null | grep -q "inet " && [ $WAITED -lt $MAX_WAIT ]; do
sleep 2
WAITED=$((WAITED + 2))
echo "Waiting for tun0... ($WAITED/$MAX_WAIT seconds)"
if [ -f /var/log/openvpn-revoke.log ]; then
tail -3 /var/log/openvpn-revoke.log
fi
done
if ! ip addr show tun0 2>/dev/null | grep -q "inet "; then
echo "FAIL: VPN connection with revoke test client failed"
cat /var/log/openvpn-revoke.log || true
exit 1
fi
echo "PASS: Connected with '$REVOKE_CLIENT' certificate"
ip addr show tun0
# Verify connectivity
if ping -c 2 10.8.0.1 >/dev/null 2>&1; then
echo "PASS: Can ping VPN gateway with revoke test client"
else
echo "FAIL: Cannot ping VPN gateway with revoke test client"
exit 1
fi
# Signal server that we're connected with revoke test client
touch /shared/revoke-client-connected
# Wait for server to signal us to disconnect
echo "Waiting for server to signal disconnect..."
MAX_WAIT=60
WAITED=0
while [ ! -f /shared/revoke-client-disconnect ] && [ $WAITED -lt $MAX_WAIT ]; do
sleep 2
WAITED=$((WAITED + 2))
done
if [ ! -f /shared/revoke-client-disconnect ]; then
echo "FAIL: Server did not signal disconnect"
exit 1
fi
# Disconnect
echo "Disconnecting revoke test client..."
pkill openvpn || true
# Wait for openvpn to fully exit and tun0 to be released
WAITED=0
MAX_WAIT_DISCONNECT=10
while (pgrep openvpn >/dev/null || ip addr show tun0 2>/dev/null | grep -q "inet ") && [ $WAITED -lt $MAX_WAIT_DISCONNECT ]; do
sleep 1
WAITED=$((WAITED + 1))
done
# Verify disconnected
if ip addr show tun0 2>/dev/null | grep -q "inet "; then
echo "FAIL: tun0 still has IP after disconnect"
exit 1
fi
echo "PASS: Disconnected successfully"
# Signal server that we're disconnected
touch /shared/revoke-client-disconnected
# Wait for server to revoke the certificate and signal us to reconnect
echo "Waiting for server to revoke certificate and signal reconnect..."
MAX_WAIT=60
WAITED=0
while [ ! -f /shared/revoke-try-reconnect ] && [ $WAITED -lt $MAX_WAIT ]; do
sleep 2
WAITED=$((WAITED + 2))
done
if [ ! -f /shared/revoke-try-reconnect ]; then
echo "FAIL: Server did not signal to try reconnect"
exit 1
fi
# Try to reconnect with the now-revoked certificate (should fail)
echo "Attempting to reconnect with revoked certificate (should fail)..."
rm -f /var/log/openvpn-revoke-fail.log
openvpn --config "/shared/$REVOKE_CLIENT.ovpn" --daemon --log /var/log/openvpn-revoke-fail.log
# Wait and check if connection fails
# The connection should fail due to certificate being revoked
echo "Waiting to verify connection is rejected..."
CONNECT_FAILED=false
MAX_WAIT=30
WAITED=0
while [ $WAITED -lt $MAX_WAIT ]; do
sleep 2
WAITED=$((WAITED + 2))
# Check if tun0 came up (would mean revocation didn't work)
if ip addr show tun0 2>/dev/null | grep -q "inet "; then
echo "FAIL: Connection succeeded with revoked certificate!"
cat /var/log/openvpn-revoke-fail.log || true
exit 1
fi
# Check for certificate verification failure in log
if [ -f /var/log/openvpn-revoke-fail.log ]; then
if grep -qi "certificate verify failed\|TLS Error\|AUTH_FAILED\|certificate revoked" /var/log/openvpn-revoke-fail.log; then
echo "Connection correctly rejected (certificate revoked)"
CONNECT_FAILED=true
break
fi
fi
echo "Checking connection status... ($WAITED/$MAX_WAIT seconds)"
if [ -f /var/log/openvpn-revoke-fail.log ]; then
tail -3 /var/log/openvpn-revoke-fail.log
fi
done
# Kill any remaining openvpn process
pkill openvpn 2>/dev/null || true
sleep 1
# Even if we didn't see explicit error, verify tun0 is not up
if ip addr show tun0 2>/dev/null | grep -q "inet "; then
echo "FAIL: tun0 interface exists - revoked cert may have connected"
exit 1
fi
if [ "$CONNECT_FAILED" = true ]; then
echo "PASS: Connection with revoked certificate was correctly rejected"
else
echo "PASS: Connection with revoked certificate did not succeed (no tun0)"
echo "OpenVPN log:"
cat /var/log/openvpn-revoke-fail.log || true
fi
# Signal server that reconnect with revoked cert failed
touch /shared/revoke-reconnect-failed
# =====================================================
# Test connecting with new certificate (same name)
# =====================================================
echo ""
echo "=== Testing connection with recreated certificate ==="
# Wait for server to create new cert and signal us
echo "Waiting for new client config with same name..."
MAX_WAIT=120
WAITED=0
while [ ! -f /shared/new-client-config-ready ] && [ $WAITED -lt $MAX_WAIT ]; do
sleep 2
WAITED=$((WAITED + 2))
echo "Waiting for new config... ($WAITED/$MAX_WAIT seconds)"
done
if [ ! -f /shared/new-client-config-ready ]; then
echo "FAIL: New client config not ready in time"
exit 1
fi
if [ ! -f "/shared/$REVOKE_CLIENT-new.ovpn" ]; then
echo "FAIL: New client config file not found"
exit 1
fi
echo "New client config found!"
# Connect with the new certificate
echo "Connecting with new '$REVOKE_CLIENT' certificate..."
rm -f /var/log/openvpn-new.log
openvpn --config "/shared/$REVOKE_CLIENT-new.ovpn" --daemon --log /var/log/openvpn-new.log
# Wait for connection
echo "Waiting for VPN connection with new certificate..."
MAX_WAIT=60
WAITED=0
while ! ip addr show tun0 2>/dev/null | grep -q "inet " && [ $WAITED -lt $MAX_WAIT ]; do
sleep 2
WAITED=$((WAITED + 2))
echo "Waiting for tun0... ($WAITED/$MAX_WAIT seconds)"
if [ -f /var/log/openvpn-new.log ]; then
tail -3 /var/log/openvpn-new.log
fi
done
if ! ip addr show tun0 2>/dev/null | grep -q "inet "; then
echo "FAIL: VPN connection with new certificate failed"
cat /var/log/openvpn-new.log || true
exit 1
fi
echo "PASS: Connected with new '$REVOKE_CLIENT' certificate"
ip addr show tun0
# Verify connectivity
if ping -c 2 10.8.0.1 >/dev/null 2>&1; then
echo "PASS: Can ping VPN gateway with new certificate"
else
echo "FAIL: Cannot ping VPN gateway with new certificate"
exit 1
fi
# Signal server that we connected with new cert
touch /shared/new-client-connected
echo ""
echo "=== Certificate Revocation E2E Tests PASSED ==="
echo ""
echo "=========================================="
echo " ALL TESTS PASSED!"
echo "=========================================="
# Keep container running for debugging if needed
exec tail -f /var/log/openvpn-new.log 2>/dev/null || tail -f /var/log/openvpn.log 2>/dev/null || sleep infinity

627
test/server-entrypoint.sh Executable file
View File

@@ -0,0 +1,627 @@
#!/bin/bash
set -e
echo "=== OpenVPN Server Container ==="
# Create TUN device if it doesn't exist
if [ ! -c /dev/net/tun ]; then
mkdir -p /dev/net
mknod /dev/net/tun c 10 200
chmod 600 /dev/net/tun
fi
echo "TUN device ready"
# Set up environment for auto-install
export AUTO_INSTALL=y
export FORCE_COLOR=1
export APPROVE_INSTALL=y
export APPROVE_IP=y
export IPV6_SUPPORT=n
export PORT_CHOICE=1
export PROTOCOL_CHOICE=1
export DNS=2 # Self-hosted Unbound DNS resolver
export COMPRESSION_ENABLED=n
export CLIENT=testclient
export PASS=1
export ENDPOINT=openvpn-server
# TLS key type configuration (default: tls-crypt-v2)
# TLS_SIG: 1=tls-crypt-v2, 2=tls-crypt, 3=tls-auth
# TLS_KEY_FILE: the expected key file name for verification
TLS_SIG="${TLS_SIG:-1}"
TLS_KEY_FILE="${TLS_KEY_FILE:-tls-crypt-v2.key}"
export TLS_SIG
# If using non-default TLS settings, enable encryption customization
if [ "$TLS_SIG" != "1" ]; then
export CUSTOMIZE_ENC=y
# Set other encryption defaults when customizing
export CIPHER_CHOICE=1 # AES-128-GCM
export CERT_TYPE=1 # ECDSA
export CERT_CURVE_CHOICE=1 # prime256v1
export CC_CIPHER_CHOICE=1 # ECDHE-ECDSA-AES-128-GCM-SHA256
export DH_TYPE=1 # ECDH
export DH_CURVE_CHOICE=1 # prime256v1
export HMAC_ALG_CHOICE=1 # SHA-256
echo "Testing TLS key type: $TLS_SIG (key file: $TLS_KEY_FILE)"
else
export CUSTOMIZE_ENC=n
fi
echo "Running OpenVPN install script..."
# Run in subshell because the script calls 'exit 0' after generating client config
# Capture output to validate logging format, while still displaying it
# Use || true to prevent set -e from exiting on failure, then check exit code
INSTALL_OUTPUT="/tmp/install-output.log"
(bash /opt/openvpn-install.sh) 2>&1 | tee "$INSTALL_OUTPUT"
INSTALL_EXIT_CODE=${PIPESTATUS[0]}
echo "=== Installation complete (exit code: $INSTALL_EXIT_CODE) ==="
# Validate that all output uses proper logging format (ANSI color codes)
echo "Validating output format..."
if /opt/test/validate-output.sh "$INSTALL_OUTPUT"; then
echo "PASS: All script output uses proper log formatting"
else
echo "FAIL: Script output contains unformatted lines"
echo "This indicates echo statements that should use log_* functions"
exit 1
fi
if [ "$INSTALL_EXIT_CODE" -ne 0 ]; then
echo "ERROR: Install script failed with exit code $INSTALL_EXIT_CODE"
exit 1
fi
# Verify all expected files were created
echo "Verifying installation..."
MISSING_FILES=0
for f in \
/etc/openvpn/server/server.conf \
/etc/openvpn/server/ca.crt \
/etc/openvpn/server/ca.key \
"/etc/openvpn/server/$TLS_KEY_FILE" \
/etc/openvpn/server/crl.pem \
/etc/openvpn/server/easy-rsa/pki/ca.crt \
/etc/iptables/add-openvpn-rules.sh \
/root/testclient.ovpn; do
if [ ! -f "$f" ]; then
echo "ERROR: Missing file: $f"
MISSING_FILES=$((MISSING_FILES + 1))
fi
done
if [ $MISSING_FILES -gt 0 ]; then
echo "ERROR: $MISSING_FILES required files are missing"
exit 1
fi
echo "All required files present"
# Copy client config to shared volume for the client container
cp /root/testclient.ovpn /shared/client.ovpn
sed -i 's/^remote .*/remote openvpn-server 1194/' /shared/client.ovpn
echo "Client config copied to /shared/client.ovpn"
# =====================================================
# Verify systemd service file configuration
# =====================================================
echo ""
echo "=== Verifying systemd service configuration ==="
# Check that the correct service file was created
SERVICE_FILE="/etc/systemd/system/openvpn-server@.service"
if [ -f "$SERVICE_FILE" ]; then
echo "PASS: openvpn-server@.service exists at $SERVICE_FILE"
else
echo "FAIL: openvpn-server@.service not found at $SERVICE_FILE"
echo "Contents of /etc/systemd/system/:"
find /etc/systemd/system/ -maxdepth 1 -name '*openvpn*' -ls 2>/dev/null || echo "No openvpn service files found"
exit 1
fi
# Verify the service file points to /etc/openvpn/server/ (not patched back to /etc/openvpn/)
if grep -q "/etc/openvpn/server" "$SERVICE_FILE"; then
echo "PASS: Service file uses correct path /etc/openvpn/server/"
else
echo "FAIL: Service file does not reference /etc/openvpn/server/"
echo "Service file contents:"
cat "$SERVICE_FILE"
exit 1
fi
# Verify the service file syntax is valid (if systemd-analyze is available)
if command -v systemd-analyze >/dev/null 2>&1; then
echo "Validating service file syntax..."
if systemd-analyze verify "$SERVICE_FILE" 2>&1 | tee /tmp/service-verify.log; then
echo "PASS: Service file syntax is valid"
else
# systemd-analyze verify may return non-zero for warnings, check for actual errors
if grep -qi "error" /tmp/service-verify.log; then
echo "FAIL: Service file has syntax errors"
cat /tmp/service-verify.log
exit 1
else
echo "PASS: Service file syntax is valid (warnings only)"
fi
fi
else
echo "SKIP: systemd-analyze not available, skipping syntax validation"
fi
# Verify the old service file pattern (openvpn@.service) was NOT created
OLD_SERVICE_FILE="/etc/systemd/system/openvpn@.service"
if [ -f "$OLD_SERVICE_FILE" ]; then
echo "FAIL: Legacy openvpn@.service was created (should use openvpn-server@.service)"
exit 1
else
echo "PASS: Legacy openvpn@.service not present (correct)"
fi
echo "=== systemd service configuration verified ==="
echo ""
echo "Server config:"
cat /etc/openvpn/server/server.conf
# =====================================================
# Test certificate renewal functionality
# =====================================================
echo ""
echo "=== Testing Certificate Renewal ==="
# Get the original certificate serial number for comparison
ORIG_CERT_SERIAL=$(openssl x509 -in /etc/openvpn/server/easy-rsa/pki/issued/testclient.crt -noout -serial | cut -d= -f2)
echo "Original client certificate serial: $ORIG_CERT_SERIAL"
# Test client certificate renewal using the script
echo "Testing client certificate renewal..."
RENEW_OUTPUT="/tmp/renew-client-output.log"
(MENU_OPTION=3 RENEW_OPTION=1 CLIENTNUMBER=1 CLIENT_CERT_DURATION_DAYS=3650 bash /opt/openvpn-install.sh) 2>&1 | tee "$RENEW_OUTPUT" || true
# Verify renewal succeeded
if grep -q "Certificate for client testclient renewed" "$RENEW_OUTPUT"; then
echo "PASS: Client renewal completed successfully"
else
echo "FAIL: Client renewal did not complete"
cat "$RENEW_OUTPUT"
exit 1
fi
# Verify new certificate has different serial
NEW_CERT_SERIAL=$(openssl x509 -in /etc/openvpn/server/easy-rsa/pki/issued/testclient.crt -noout -serial | cut -d= -f2)
echo "New client certificate serial: $NEW_CERT_SERIAL"
if [ "$ORIG_CERT_SERIAL" != "$NEW_CERT_SERIAL" ]; then
echo "PASS: Certificate serial changed (renewal created new cert)"
else
echo "FAIL: Certificate serial unchanged"
exit 1
fi
# Verify renewed certificate has correct validity period
# The default is 3650 days, so the cert should be valid for ~10 years from now
CLIENT_CERT_NOT_AFTER=$(openssl x509 -in /etc/openvpn/server/easy-rsa/pki/issued/testclient.crt -noout -enddate | cut -d= -f2)
CLIENT_CERT_NOT_BEFORE=$(openssl x509 -in /etc/openvpn/server/easy-rsa/pki/issued/testclient.crt -noout -startdate | cut -d= -f2)
echo "Client certificate valid from: $CLIENT_CERT_NOT_BEFORE"
echo "Client certificate valid until: $CLIENT_CERT_NOT_AFTER"
# Calculate days until expiry (should be close to 3650)
CERT_END_EPOCH=$(date -d "$CLIENT_CERT_NOT_AFTER" +%s 2>/dev/null || date -j -f "%b %d %T %Y %Z" "$CLIENT_CERT_NOT_AFTER" +%s 2>/dev/null)
NOW_EPOCH=$(date +%s)
DAYS_VALID_ACTUAL=$(((CERT_END_EPOCH - NOW_EPOCH) / 86400))
echo "Client certificate validity: $DAYS_VALID_ACTUAL days"
# Should be between 3640 and 3650 days (allowing some tolerance for timing)
if [ "$DAYS_VALID_ACTUAL" -ge 3640 ] && [ "$DAYS_VALID_ACTUAL" -le 3650 ]; then
echo "PASS: Client certificate validity is correct (~3650 days)"
else
echo "FAIL: Client certificate validity is unexpected: $DAYS_VALID_ACTUAL days (expected ~3650)"
exit 1
fi
# Verify new .ovpn file was generated
if [ -f /root/testclient.ovpn ]; then
echo "PASS: New .ovpn file generated"
else
echo "FAIL: .ovpn file not found after renewal"
exit 1
fi
# Verify CRL was updated
if [ -f /etc/openvpn/server/crl.pem ]; then
echo "PASS: CRL file exists"
else
echo "FAIL: CRL file missing after renewal"
exit 1
fi
# Update shared client config with renewed certificate
cp /root/testclient.ovpn /shared/client.ovpn
sed -i 's/^remote .*/remote openvpn-server 1194/' /shared/client.ovpn
echo "Updated client config with renewed certificate"
echo "=== Client Certificate Renewal Tests PASSED ==="
# =====================================================
# Test server certificate renewal
# =====================================================
echo ""
echo "=== Testing Server Certificate Renewal ==="
# Get server certificate name and original serial (extract basename since path may be relative)
SERVER_NAME=$(basename "$(grep '^cert ' /etc/openvpn/server/server.conf | cut -d ' ' -f 2)" .crt)
ORIG_SERVER_SERIAL=$(openssl x509 -in "/etc/openvpn/server/easy-rsa/pki/issued/$SERVER_NAME.crt" -noout -serial | cut -d= -f2)
echo "Server certificate: $SERVER_NAME"
echo "Original server certificate serial: $ORIG_SERVER_SERIAL"
# Test server certificate renewal
echo "Testing server certificate renewal..."
RENEW_SERVER_OUTPUT="/tmp/renew-server-output.log"
(MENU_OPTION=3 RENEW_OPTION=2 CONTINUE=y SERVER_CERT_DURATION_DAYS=3650 bash /opt/openvpn-install.sh) 2>&1 | tee "$RENEW_SERVER_OUTPUT" || true
# Verify renewal succeeded
if grep -q "Server certificate renewed successfully" "$RENEW_SERVER_OUTPUT"; then
echo "PASS: Server renewal completed successfully"
else
echo "FAIL: Server renewal did not complete"
cat "$RENEW_SERVER_OUTPUT"
exit 1
fi
# Verify new certificate has different serial
NEW_SERVER_SERIAL=$(openssl x509 -in "/etc/openvpn/server/easy-rsa/pki/issued/$SERVER_NAME.crt" -noout -serial | cut -d= -f2)
echo "New server certificate serial: $NEW_SERVER_SERIAL"
if [ "$ORIG_SERVER_SERIAL" != "$NEW_SERVER_SERIAL" ]; then
echo "PASS: Server certificate serial changed (renewal created new cert)"
else
echo "FAIL: Server certificate serial unchanged"
exit 1
fi
# Verify renewed server certificate has correct validity period
SERVER_CERT_NOT_AFTER=$(openssl x509 -in "/etc/openvpn/server/easy-rsa/pki/issued/$SERVER_NAME.crt" -noout -enddate | cut -d= -f2)
SERVER_CERT_NOT_BEFORE=$(openssl x509 -in "/etc/openvpn/server/easy-rsa/pki/issued/$SERVER_NAME.crt" -noout -startdate | cut -d= -f2)
echo "Server certificate valid from: $SERVER_CERT_NOT_BEFORE"
echo "Server certificate valid until: $SERVER_CERT_NOT_AFTER"
# Calculate days until expiry (should be close to 3650)
SERVER_END_EPOCH=$(date -d "$SERVER_CERT_NOT_AFTER" +%s 2>/dev/null || date -j -f "%b %d %T %Y %Z" "$SERVER_CERT_NOT_AFTER" +%s 2>/dev/null)
SERVER_DAYS_VALID=$(((SERVER_END_EPOCH - NOW_EPOCH) / 86400))
echo "Server certificate validity: $SERVER_DAYS_VALID days"
if [ "$SERVER_DAYS_VALID" -ge 3640 ] && [ "$SERVER_DAYS_VALID" -le 3650 ]; then
echo "PASS: Server certificate validity is correct (~3650 days)"
else
echo "FAIL: Server certificate validity is unexpected: $SERVER_DAYS_VALID days (expected ~3650)"
exit 1
fi
# Verify the new certificate was copied to /etc/openvpn/server/
if [ -f "/etc/openvpn/server/$SERVER_NAME.crt" ]; then
DEPLOYED_SERIAL=$(openssl x509 -in "/etc/openvpn/server/$SERVER_NAME.crt" -noout -serial | cut -d= -f2)
if [ "$NEW_SERVER_SERIAL" = "$DEPLOYED_SERIAL" ]; then
echo "PASS: New server certificate deployed to /etc/openvpn/server/"
else
echo "FAIL: Deployed certificate doesn't match renewed certificate"
exit 1
fi
else
echo "FAIL: Server certificate not found in /etc/openvpn/server/"
exit 1
fi
echo "=== Server Certificate Renewal Tests PASSED ==="
echo ""
echo "=== All Certificate Renewal Tests PASSED ==="
echo ""
# =====================================================
# Verify Unbound DNS resolver (started by systemd via install script)
# =====================================================
echo "=== Verifying Unbound DNS Resolver ==="
if [ -f /etc/unbound/unbound.conf ]; then
# Verify Unbound is running (started by systemctl in install script)
echo "Checking Unbound service status..."
for _ in $(seq 1 30); do
if pgrep -x unbound >/dev/null; then
echo "PASS: Unbound is running"
break
fi
sleep 1
done
if ! pgrep -x unbound >/dev/null; then
echo "FAIL: Unbound is not running"
systemctl status unbound 2>&1 || true
journalctl -u unbound --no-pager -n 50 2>&1 || true
exit 1
fi
else
echo "FAIL: /etc/unbound/unbound.conf not found"
exit 1
fi
echo ""
echo "=== Verifying Unbound Installation ==="
# Verify Unbound config exists in conf.d directory
UNBOUND_OPENVPN_CONF="/etc/unbound/unbound.conf.d/openvpn.conf"
if [ -f "$UNBOUND_OPENVPN_CONF" ]; then
echo "PASS: Found Unbound config at $UNBOUND_OPENVPN_CONF"
else
echo "FAIL: OpenVPN Unbound config not found at $UNBOUND_OPENVPN_CONF"
echo "Contents of /etc/unbound/:"
ls -la /etc/unbound/
ls -la /etc/unbound/unbound.conf.d/ 2>/dev/null || true
exit 1
fi
# Verify Unbound listens on VPN gateway
if grep -q "interface: 10.8.0.1" "$UNBOUND_OPENVPN_CONF"; then
echo "PASS: Unbound configured to listen on 10.8.0.1"
else
echo "FAIL: Unbound not configured for 10.8.0.1"
cat "$UNBOUND_OPENVPN_CONF"
exit 1
fi
# Verify OpenVPN pushes correct DNS
if grep -q 'push "dhcp-option DNS 10.8.0.1"' /etc/openvpn/server/server.conf; then
echo "PASS: OpenVPN configured to push Unbound DNS"
else
echo "FAIL: OpenVPN not configured to push Unbound DNS"
grep "dhcp-option DNS" /etc/openvpn/server/server.conf || echo "No DNS push found"
exit 1
fi
echo "=== Unbound Installation Verified ==="
echo ""
# Verify OpenVPN server (started by systemd via install script)
echo "Verifying OpenVPN server..."
# Verify iptables NAT rules exist (applied by iptables-openvpn service)
echo "Verifying iptables NAT rules..."
for _ in $(seq 1 10); do
if iptables -t nat -L POSTROUTING -n | grep -q "10.8.0.0"; then
echo "PASS: NAT POSTROUTING rule for 10.8.0.0/24 exists"
break
fi
sleep 1
done
if ! iptables -t nat -L POSTROUTING -n | grep -q "10.8.0.0"; then
echo "FAIL: NAT POSTROUTING rule for 10.8.0.0/24 not found"
echo "Current NAT rules:"
iptables -t nat -L POSTROUTING -n -v
systemctl status iptables-openvpn 2>&1 || true
exit 1
fi
# Verify IP forwarding is enabled
if [ "$(cat /proc/sys/net/ipv4/ip_forward)" != "1" ]; then
echo "ERROR: IP forwarding is not enabled"
exit 1
fi
# Wait for OpenVPN to start (started by systemctl in install script)
echo "Waiting for OpenVPN server to start..."
for _ in $(seq 1 30); do
if pgrep -f "openvpn.*server.conf" >/dev/null; then
echo "PASS: OpenVPN server is running"
break
fi
sleep 1
done
if ! pgrep -f "openvpn.*server.conf" >/dev/null; then
echo "FAIL: OpenVPN server is not running"
systemctl status openvpn-server@server 2>&1 || true
journalctl -u openvpn-server@server --no-pager -n 50 2>&1 || true
exit 1
fi
# =====================================================
# Wait for initial client tests to complete
# =====================================================
echo ""
echo "=== Waiting for initial client connectivity tests ==="
MAX_WAIT=120
WAITED=0
while [ ! -f /shared/initial-tests-passed ] && [ $WAITED -lt $MAX_WAIT ]; do
sleep 2
WAITED=$((WAITED + 2))
echo "Waiting for initial tests... ($WAITED/$MAX_WAIT seconds)"
done
if [ ! -f /shared/initial-tests-passed ]; then
echo "ERROR: Initial client tests did not complete in time"
exit 1
fi
echo "Initial client tests passed, proceeding with revocation tests"
# =====================================================
# Test certificate revocation functionality
# =====================================================
echo ""
echo "=== Testing Certificate Revocation ==="
# Create a new client for revocation testing
REVOKE_CLIENT="revoketest"
echo "Creating client '$REVOKE_CLIENT' for revocation testing..."
REVOKE_CREATE_OUTPUT="/tmp/revoke-create-output.log"
(MENU_OPTION=1 CLIENT=$REVOKE_CLIENT PASS=1 CLIENT_CERT_DURATION_DAYS=3650 bash /opt/openvpn-install.sh) 2>&1 | tee "$REVOKE_CREATE_OUTPUT" || true
if [ -f "/root/$REVOKE_CLIENT.ovpn" ]; then
echo "PASS: Client '$REVOKE_CLIENT' created successfully"
else
echo "FAIL: Failed to create client '$REVOKE_CLIENT'"
cat "$REVOKE_CREATE_OUTPUT"
exit 1
fi
# Copy config for revocation test client
cp "/root/$REVOKE_CLIENT.ovpn" "/shared/$REVOKE_CLIENT.ovpn"
sed -i 's/^remote .*/remote openvpn-server 1194/' "/shared/$REVOKE_CLIENT.ovpn"
echo "Copied $REVOKE_CLIENT config to /shared/"
# Signal client that revoke test config is ready
touch /shared/revoke-client-config-ready
# Wait for client to confirm connection with revoke test client
echo "Waiting for client to connect with '$REVOKE_CLIENT' certificate..."
MAX_WAIT=60
WAITED=0
while [ ! -f /shared/revoke-client-connected ] && [ $WAITED -lt $MAX_WAIT ]; do
sleep 2
WAITED=$((WAITED + 2))
echo "Waiting for revoke test connection... ($WAITED/$MAX_WAIT seconds)"
done
if [ ! -f /shared/revoke-client-connected ]; then
echo "ERROR: Client did not connect with revoke test certificate"
exit 1
fi
echo "PASS: Client connected with '$REVOKE_CLIENT' certificate"
# Signal client to disconnect before revocation
touch /shared/revoke-client-disconnect
# Wait for client to disconnect
echo "Waiting for client to disconnect..."
MAX_WAIT=30
WAITED=0
while [ ! -f /shared/revoke-client-disconnected ] && [ $WAITED -lt $MAX_WAIT ]; do
sleep 2
WAITED=$((WAITED + 2))
done
if [ ! -f /shared/revoke-client-disconnected ]; then
echo "ERROR: Client did not signal disconnect"
exit 1
fi
echo "Client disconnected"
# Now revoke the certificate
echo "Revoking certificate for '$REVOKE_CLIENT'..."
REVOKE_OUTPUT="/tmp/revoke-output.log"
# MENU_OPTION=2 is revoke, CLIENTNUMBER is dynamically determined from index.txt
# We need to find the client number for revoketest
REVOKE_CLIENT_NUM=$(tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep "^V" | grep -n "CN=$REVOKE_CLIENT\$" | cut -d: -f1)
if [ -z "$REVOKE_CLIENT_NUM" ]; then
echo "ERROR: Could not find client number for '$REVOKE_CLIENT'"
cat /etc/openvpn/server/easy-rsa/pki/index.txt
exit 1
fi
echo "Revoke client number: $REVOKE_CLIENT_NUM"
(MENU_OPTION=2 CLIENTNUMBER=$REVOKE_CLIENT_NUM bash /opt/openvpn-install.sh) 2>&1 | tee "$REVOKE_OUTPUT" || true
if grep -q "Certificate for client $REVOKE_CLIENT revoked" "$REVOKE_OUTPUT"; then
echo "PASS: Certificate for '$REVOKE_CLIENT' revoked successfully"
else
echo "FAIL: Failed to revoke certificate"
cat "$REVOKE_OUTPUT"
exit 1
fi
# Verify certificate is marked as revoked in index.txt
if tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep -q "^R.*CN=$REVOKE_CLIENT\$"; then
echo "PASS: Certificate marked as revoked in index.txt"
else
echo "FAIL: Certificate not marked as revoked"
cat /etc/openvpn/server/easy-rsa/pki/index.txt
exit 1
fi
# Signal client to try reconnecting (should fail)
touch /shared/revoke-try-reconnect
# Wait for client to confirm that connection with revoked cert failed
echo "Waiting for client to confirm revoked cert connection failure..."
MAX_WAIT=60
WAITED=0
while [ ! -f /shared/revoke-reconnect-failed ] && [ $WAITED -lt $MAX_WAIT ]; do
sleep 2
WAITED=$((WAITED + 2))
echo "Waiting for reconnect failure confirmation... ($WAITED/$MAX_WAIT seconds)"
done
if [ ! -f /shared/revoke-reconnect-failed ]; then
echo "ERROR: Client did not confirm that revoked cert connection failed"
exit 1
fi
echo "PASS: Connection with revoked certificate correctly rejected"
echo "=== Certificate Revocation Tests PASSED ==="
# =====================================================
# Test reusing revoked client name
# =====================================================
echo ""
echo "=== Testing Reuse of Revoked Client Name ==="
# Create a new certificate with the same name as the revoked one
echo "Creating new client with same name '$REVOKE_CLIENT'..."
RECREATE_OUTPUT="/tmp/recreate-output.log"
(MENU_OPTION=1 CLIENT=$REVOKE_CLIENT PASS=1 CLIENT_CERT_DURATION_DAYS=3650 bash /opt/openvpn-install.sh) 2>&1 | tee "$RECREATE_OUTPUT" || true
if [ -f "/root/$REVOKE_CLIENT.ovpn" ]; then
echo "PASS: New client '$REVOKE_CLIENT' created successfully (reusing revoked name)"
else
echo "FAIL: Failed to create client with revoked name"
cat "$RECREATE_OUTPUT"
exit 1
fi
# Verify the new certificate is valid (V) in index.txt
if tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep -q "^V.*CN=$REVOKE_CLIENT\$"; then
echo "PASS: New certificate is valid in index.txt"
else
echo "FAIL: New certificate not marked as valid"
cat /etc/openvpn/server/easy-rsa/pki/index.txt
exit 1
fi
# Verify there's also a revoked entry (both should exist)
REVOKED_COUNT=$(tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep -c "^R.*CN=$REVOKE_CLIENT\$")
VALID_COUNT=$(tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep -c "^V.*CN=$REVOKE_CLIENT\$")
echo "Certificates for '$REVOKE_CLIENT': $REVOKED_COUNT revoked, $VALID_COUNT valid"
if [ "$REVOKED_COUNT" -ge 1 ] && [ "$VALID_COUNT" -eq 1 ]; then
echo "PASS: Both revoked and new valid certificate entries exist"
else
echo "FAIL: Unexpected certificate state"
cat /etc/openvpn/server/easy-rsa/pki/index.txt
exit 1
fi
# Copy the new config
cp "/root/$REVOKE_CLIENT.ovpn" "/shared/$REVOKE_CLIENT-new.ovpn"
sed -i 's/^remote .*/remote openvpn-server 1194/' "/shared/$REVOKE_CLIENT-new.ovpn"
echo "Copied new $REVOKE_CLIENT config to /shared/"
# Signal client that new config is ready
touch /shared/new-client-config-ready
# Wait for client to confirm successful connection with new cert
echo "Waiting for client to connect with new '$REVOKE_CLIENT' certificate..."
MAX_WAIT=60
WAITED=0
while [ ! -f /shared/new-client-connected ] && [ $WAITED -lt $MAX_WAIT ]; do
sleep 2
WAITED=$((WAITED + 2))
echo "Waiting for new cert connection... ($WAITED/$MAX_WAIT seconds)"
done
if [ ! -f /shared/new-client-connected ]; then
echo "ERROR: Client did not connect with new certificate"
exit 1
fi
echo "PASS: Client connected with new '$REVOKE_CLIENT' certificate"
echo "=== Reuse of Revoked Client Name Tests PASSED ==="
echo ""
echo "=== All Revocation Tests PASSED ==="
# Server tests complete - systemd keeps the container running via /sbin/init
# OpenVPN service (openvpn-server@server) continues independently
echo "Server tests complete. Container will remain running via systemd."
echo "OpenVPN is managed by: systemctl status openvpn-server@server"

87
test/validate-output.sh Executable file
View File

@@ -0,0 +1,87 @@
#!/bin/bash
# Validates that script output only contains properly formatted log messages
# All output from openvpn-install.sh should use logging functions
#
# Usage: ./validate-output.sh <output_file>
# Or pipe: some_command | ./validate-output.sh
set -euo pipefail
INPUT_FILE="${1:-/dev/stdin}"
# Valid output patterns:
# - Lines starting with ANSI escape codes (colored output)
# - Lines starting with our log prefixes (non-TTY mode)
# - Lines starting with > (command echo from run_cmd)
# - Empty lines
# ANSI escape code pattern
ANSI_PATTERN=$'^\033\\['
# Log prefix patterns (for non-TTY mode where colors are disabled)
# These match: [INFO], [WARN], [ERROR], [OK], [DEBUG], or > (command line)
LOG_PREFIXES='^(\[INFO\]|\[WARN\]|\[ERROR\]|\[OK\]|\[DEBUG\]|> )'
# Count issues
INVALID_LINES=0
TOTAL_LINES=0
LINE_NUM=0
echo "Validating script output for unformatted lines..."
echo ""
while IFS= read -r line || [[ -n "$line" ]]; do
LINE_NUM=$((LINE_NUM + 1))
# Skip empty lines
if [[ -z "$line" ]]; then
continue
fi
TOTAL_LINES=$((TOTAL_LINES + 1))
# Check if line starts with ANSI escape code (colored output from log functions)
if [[ "$line" =~ $ANSI_PATTERN ]]; then
continue
fi
# Check if line starts with our log prefixes (non-TTY mode)
if [[ "$line" =~ $LOG_PREFIXES ]]; then
continue
fi
# If we get here, the line doesn't match expected patterns - it's raw output
INVALID_LINES=$((INVALID_LINES + 1))
# Truncate long lines for display
if [[ ${#line} -gt 100 ]]; then
DISPLAY_LINE="${line:0:100}..."
else
DISPLAY_LINE="$line"
fi
echo " [LEAK] Line $LINE_NUM: $DISPLAY_LINE"
done <"$INPUT_FILE"
echo ""
echo "----------------------------------------"
echo "Total lines checked: $TOTAL_LINES"
echo "Invalid lines found: $INVALID_LINES"
if [[ $INVALID_LINES -gt 0 ]]; then
echo ""
echo "ERROR: Found $INVALID_LINES line(s) without proper log formatting."
echo ""
echo "All user-visible output should use log_* functions:"
echo " - log_info 'message' -> [INFO] message"
echo " - log_warn 'message' -> [WARN] message"
echo " - log_error 'message' -> [ERROR] message"
echo " - log_success 'message' -> [OK] message"
echo " - run_cmd 'desc' cmd -> > cmd"
echo ""
echo "Raw echo statements or command output should not leak to stdout."
exit 1
fi
echo ""
echo "All output is properly formatted!"
exit 0