mirror of
https://github.com/angristan/openvpn-install.git
synced 2025-12-17 17:27:03 +01:00
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:
@@ -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
3
.github/FUNDING.yml
vendored
@@ -1,5 +1,2 @@
|
||||
patreon: stanislas
|
||||
liberapay: stanislas
|
||||
ko_fi: stanislas
|
||||
github: angristan
|
||||
custom: https://coindrop.to/stanislas
|
||||
|
||||
@@ -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 -->
|
||||
31
.github/ISSUE_TEMPLATE/feature-request.md
vendored
31
.github/ISSUE_TEMPLATE/feature-request.md
vendored
@@ -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.
|
||||
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -1,6 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
10
.github/issue_template.md
vendored
Normal file
10
.github/issue_template.md
vendored
Normal 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.
|
||||
--->
|
||||
9
.github/linters/.markdown-lint.yml
vendored
9
.github/linters/.markdown-lint.yml
vendored
@@ -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
7
.github/pull_request_template.md
vendored
Normal 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
104
.github/workflows/do-test.yml
vendored
Normal 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
275
.github/workflows/docker-test.yml
vendored
Normal 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
|
||||
20
.github/workflows/lint.yml
vendored
20
.github/workflows/lint.yml
vendored
@@ -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 }}
|
||||
|
||||
100
.github/workflows/test.yml
vendored
100
.github/workflows/test.yml
vendored
@@ -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()
|
||||
73
.github/workflows/update-easyrsa-hash.yml
vendored
Normal file
73
.github/workflows/update-easyrsa-hash.yml
vendored
Normal 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
8
.trivyignore
Normal 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
7
AGENTS.md
Normal 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
48
FAQ.md
@@ -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
140
Makefile
Normal 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
177
README.md
@@ -2,17 +2,44 @@
|
||||
|
||||

|
||||

|
||||

|
||||
[](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
|
||||
|
||||
[](https://star-history.com/#angristan/openvpn-install&Date)
|
||||
|
||||
7
biome.json
Normal file
7
biome.json
Normal 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
60
docker-compose.yml
Normal 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
|
||||
2010
openvpn-install.sh
2010
openvpn-install.sh
File diff suppressed because it is too large
Load Diff
17
renovate.json
Normal file
17
renovate.json
Normal 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
26
test/Dockerfile.client
Normal 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
69
test/Dockerfile.server
Normal 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
368
test/client-entrypoint.sh
Executable 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
627
test/server-entrypoint.sh
Executable 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
87
test/validate-output.sh
Executable 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
|
||||
Reference in New Issue
Block a user