Skip to content

Commit

Permalink
feat: openvpn server with ssh access
Browse files Browse the repository at this point in the history
  • Loading branch information
Morriz committed Mar 13, 2024
1 parent 52cb470 commit 4ac0845
Show file tree
Hide file tree
Showing 18 changed files with 388 additions and 103 deletions.
56 changes: 56 additions & 0 deletions .github/workflows/client.ovpn
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@

client
nobind
dev tun
remote-cert-tls server

cipher AES-256-GCM

<cert>
-----BEGIN CERTIFICATE-----
MIIDVDCCAjygAwIBAgIQdKSjq6vD90W/8PD0dEYiTzANBgkqhkiG9w0BAQsFADAW
MRQwEgYDVQQDDAtFYXN5LVJTQSBDQTAeFw0yNDAzMDgxMTQzMzBaFw0yNjA2MTEx
MTQzMzBaMBExDzANBgNVBAMMBmdpdGh1YjCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBAMR/8kbZDHboTI7gDYI5tdkYVGuXwr0w5lC1udo5nlGHd85QYen3
l92sZNV/cI6bB15Q5k1HBFxPfekR0cwM0axtbKyIFh6GoTDduVx8hTgAxup9hfiv
BP5utgmPE9oIKtrNytuIjRY15oQqsf/UKEhCe4W00MttqScUW2w1OOstF27cZaGL
OnnZu688wnpD+eh9usnjJ+fKdWUOQG2NFcw/hVDuLbJxLpeu2jlf7QuAhGz+4snA
02Z1V/uA9Q5u83pl1s/Z5Po5CcJLWB+/8vUSSp9WbclV+WDqCiCLo0eY+rmxnjq2
ammDZdnSLdFVZ2YpLZhTgH0orYAcII3+kC8CAwEAAaOBojCBnzAJBgNVHRMEAjAA
MB0GA1UdDgQWBBQAf0QwnaQcxyWtMFnktgxEMTH11zBRBgNVHSMESjBIgBT6VKhJ
N6t9nMYc/5GCeoDsptOAlqEapBgwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0GCFGvs
75ksUEGWlHHBlyCLV/mlZQ9SMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAsGA1UdDwQE
AwIHgDANBgkqhkiG9w0BAQsFAAOCAQEAMLLiA+J63lwAScQmXMsQUpBWd8NwFgJe
QFL/vjL56ReGQUipIZiboB+p4pBA9hg9GvTdAE2vVtrLeHY2qRQG6MGlKLM9mCb+
SXSx0bjs7A3pVqBvybPnPXLwDjRSdoK6qBmoH9+wR9uyDGl3Ntit5JOfB4X70+h8
9PUXImnY8IK11JgxXMA9tu2ntPPMdtnbdrfdbUA7tOlTL9Tm5JNcFpblXo9HMuan
kin1jh03NijIT8KJapfCGzO+qIcnNLRlAktSbLBj9qwXr9jcd4hJlPKel+Osw75Q
+qKhFttyZrIuhLpcp69ztFw6s8D0uYBIpN1ohjo5iB1VY3bYgmXKhw==
-----END CERTIFICATE-----
</cert>
<ca>
-----BEGIN CERTIFICATE-----
MIIDSzCCAjOgAwIBAgIUa+zvmSxQQZaUccGXIItX+aVlD1IwDQYJKoZIhvcNAQEL
BQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMjQwMzA4MTExNjM0WhcNMzQw
MzA2MTExNjM0WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBANjr60eUEbZ2i9wsVKGbpsyOUliNSVlJmBYLg8N7
HdPWPfR6+u/s+Gw4Nld2a4RPJpXZwcU0wHHFeEc9c+Wf/LxOcdMvuS9DrsvcCcAB
M8M+3vNX/Oz1zSZK5koLSi0r14nmvnXg/A1w8r3GTSsKauu80/+xbToiX0k+wfdy
wYT9Lfec6/XY3GOb0dm1x88huhvtwvj709ZvnU8CQSXgbfYap6N7EtBWBG15XouS
WZKAilWMWjh1u4jIxtFnz9p6k9TBAjj6BypjCfU7Xwoy8Xp8i5VhR7jrekYD04HE
VYT3t+IXEvNah9POfA63onWjPFuIl6AQ24MXUBKfrzrGXbkCAwEAAaOBkDCBjTAd
BgNVHQ4EFgQU+lSoSTerfZzGHP+RgnqA7KbTgJYwUQYDVR0jBEowSIAU+lSoSTer
fZzGHP+RgnqA7KbTgJahGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghRr7O+Z
LFBBlpRxwZcgi1f5pWUPUjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkq
hkiG9w0BAQsFAAOCAQEAB/OCyK9K6pQZAm+1lrv7ik60vZ/PGb0t0BTOnjrPJIEA
Yv5apo67XIsyOaePkGQvVVaMeyM+sPIY5Db2BCSQxpkFtHqipodzM0hUouYD7042
ESmskdBcUkm5j4B92aIPYT7r33XvDEPZ+2zKzWBfZH7RLM7gmAsreqNBA3mbGZrg
xJwPLfgc3xybp9UAqAAJXGV0vSoyBlfSqFFYdDqI/1+J8hiaZqINqX4SAUT/VnFO
3sIOyB8dPLNuq2mr1DKn2NVTzm1DJgJxdSJlwuNQhj0khsUoMzLcCIzseyQzfzcx
qDRrxCR/f70nwmcdMkJ81niu60T6BykxHe+CPKWLSg==
-----END CERTIFICATE-----
</ca>

key-direction 1

redirect-gateway def1
26 changes: 26 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,29 @@ jobs:
bin/format.sh
bin/lint.sh
bin/test.sh
- name: Install OpenVPN
run: |
sudo apt update
sudo apt install -y openvpn openvpn-systemd-resolved
- name: Connect to VPN
uses: 'Morriz/github-openvpn-connect-action@v3'
with:
config_file: .github/workflows/client.ovpn
host: ${{ secrets.API_HOST }}
ca: '${{ secrets.OVPN_CA }}'
cert: '${{ secrets.OVPN_CERT }}'
client_key: '${{ secrets.OVPN_USER_KEY }}'
client_pass: '${{ secrets.OVPN_USER_PASS }}'
tls_auth_key: '${{ secrets.OVPN_TLS_AUTH_KEY }}'

- name: Use SSH
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: '${{ secrets.SSH_PRIVATE_KEY }}'
passphrase: '${{ secrets.SSH_PASSPHRASE }}'
script: |
curl -v 'https://${{ secrets.API_HOST }}/update-upstream/itsUP?apikey=${{ secrets.API_KEY }}'
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
__pycache__
.aider*
*.bak*
.env
.history
.mypy_cache
Expand Down
125 changes: 105 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,22 @@ Still interested? Then read on...
- [Managed service deployments \& updates](#managed-service-deployments--updates)
- [\*Zero downtime?](#zero-downtime)
- [Prerequisites](#prerequisites)
- [Dev/ops tools](#devops-tools)
- [utility functions](#utility-functions)
- [Utility scripts](#utility-scripts)
- [Howto](#howto)
- [Install \& run](#install--run)
- [Configure services](#configure-services)
- [Configure plugins](#configure-plugins)
- [CrowdSec](#crowdsec)
- [Using the Api \& OpenApi spec](#using-the-api--openapi-spec)
- [Webhooks](#webhooks)
- [Dev/ops tools](#devops-tools)
- [utility functions for dev workflow](#utility-functions-for-dev-workflow)
- [Utility scripts](#utility-scripts)
- [Webhooks](#webhooks)
- [OpenVPN server with SSH access](#openvpn-server-with-ssh-access)
- [1. Initialize the configuration files and certificates](#1-initialize-the-configuration-files-and-certificates)
- [2. Create a client file](#2-create-a-client-file)
- [3. Retrieve the client configuration with embedded certificates and place in github workflow folder](#3-retrieve-the-client-configuration-with-embedded-certificates-and-place-in-github-workflow-folder)
- [4. SSH access](#4-ssh-access)
- [5. Make sure port 1194 is portforwarding the UDP protocol.](#5-make-sure-port-1194-is-portforwarding-the-udp-protocol)
- [Questions one might have](#questions-one-might-have)
- [What about Nginx?](#what-about-nginx)
- [Does this scale to more machines?](#does-this-scale-to-more-machines)
Expand Down Expand Up @@ -74,12 +80,35 @@ It is surely possible to deploy stateful services but beware that those might no

- [docker](https://www.docker.com) daemon and client
- docker [rollout](https://github.com/Wowu/docker-rollout) plugin
- [openvpn](https://openvpn.net): for testing vpn access (optional)

**Infra:**

- Portforwarding of port `80` and `443` to the machine running this stack. This stack MUST overtake whatever routing you now have, but don't worry, as it supports your home assistant setup and forwards any traffic it expects to it (if you finish the pre-configured `home-assistant` project in `db.yml`)
- A wildcard dns domain like `*.itsup.example.com` that points to your home ip. This allows to choose whatever subdomain for your services. You may of course choose and manage any domain in a similar fashion for a public service, but I suggest not going through such trouble for anything private.

## Dev/ops tools

### utility functions

Source `lib/functions.sh` to get:

- `dcp`: run a `docker compose` command targeting the proxy stack (`proxy` + `terminate` services): `dcp logs -f`
- `dcu`: run a `docker compose` command targeting a specific upstream: `dcu test up`
- `dca`: run a `docker compose` command targeting all upstreams: `dca ps`
- `dcpx`: execute a command in one of the proxy containers: `dcpx traefik-web 'rm -rf /etc/acme/acme.json && shutdown' && dcp up`
- `dcux`: execute a command in one of the upstream containers: `dcux test test-informant env`

In effect these wrapper commands achieve the same as when going into an `upstream/\*`folder and running`docker compose` there.
I don't want to switch folders/terminals all the time and want to keep a "project root" history of my commands so I choose this approach.

### Utility scripts

- `bin/update-certs.py`: pull certs and reload the proxy if any certs were created or updated. You could run this in a crontab every week if you want to stay up to date.
- `bin/write-artifacts.py`: after updating `db.yml` you can run this script to generate new artifacts.
- `bin/validate-db.py`: also ran from `bin/write-artifacts.py`
- `bin/requirements-update.sh`: You may want to update requirements once in a while ;)

## Howto

### Install & run
Expand Down Expand Up @@ -131,6 +160,7 @@ The following docker service properties exist at the service root level and MUST
- image
- port
- name
- restart
- volumes

(Also see `lib/models.py`)
Expand Down Expand Up @@ -187,7 +217,7 @@ All endpoints do auth and expect an incoming Bearer token to be set to `.env/API

Exception: Only github webhook endpoints (check for annotation `@app.hooks.register(...`) get it from the `github_secret` header.

#### Webhooks
### Webhooks

Webhooks are used for the following:

Expand All @@ -200,27 +230,78 @@ One GitHub webhook listening to `workflow_job`s is provided, which needs:

I mainly use GitHub workflows and created webhooks for my individual projects, so I can just manage all webhooks in one place.

## Dev/ops tools
**NOTE:**

### utility functions for dev workflow
When using crowdsec this webhook is probably not coming in as it exits the Azure cloud (public IP range), which is also host to many malicious actors that spin up ephemeral intrusion tools. To still receive signals from github you can use a vpn setup as the one used in this repo (check `.github/workflows/test.yml`).

Source `lib/functions.sh` to get:
### OpenVPN server with SSH access

- `dcp`: run a `docker compose` command targeting the proxy stack (`proxy` + `terminate` services): `dcp logs -f`
- `dcu`: run a `docker compose` command targeting a specific upstream: `dcu test up`
- `dca`: run a `docker compose` command targeting all upstreams: `dca ps`
- `dcpx`: execute a command in one of the proxy containers: `dcpx traefik-web 'rm -rf /etc/acme/acme.json && shutdown' && dcp up`
- `dcux`: execute a command in one of the upstream containers: `dcux test test-informant env`
This setup contains a project called "vpn" which runs an openvpn service that gives ssh access. To bootstrap it:

In effect these wrapper commands achieve the same as when going into an `upstream/\*`folder and running`docker compose` there.
I don't want to switch folders/terminals all the time and want to keep a "project root" history of my commands so I choose this approach.
#### 1. Initialize the configuration files and certificates

### Utility scripts
```
dcu vpn run vpn-openvpn ovpn_genconfig -u udp4://vpn.itsup.example.com
dcu vpn run vpn-openvpn ovpn_initpki
```
- `bin/update-certs.py`: pull certs and reload the proxy if any certs were created or updated. You could run this in a crontab every week if you want to stay up to date.
- `bin/write-artifacts.py`: after updating `db.yml` you can run this script to generate new artifacts.
- `bin/validate-db.py`: also ran from `bin/write-artifacts.py`
- `bin/requirements-update.sh`: You may want to update requirements once in a while ;)
Save the signing passphrase you created.
#### 2. Create a client file
```
export CLIENTNAME='github'
dcu vpn run vpn-openvpn easyrsa build-client-full $CLIENTNAME
```
Save the client passphrase you created as it will be used for `OVPN_PASSWORD` below.
#### 3. Retrieve the client configuration with embedded certificates and place in github workflow folder
```
dcu vpn run vpn-openvpn ovpn_getclient $CLIENTNAME combined > .github/workflows/client.ovpn
```
**IMPORTANT:** Now change `udp` to `udp4` in the `remote: ...` line to target UDP with IPv4 as docker is still not there.
Test access (expects local `openvpn` installed):
```
sudo openvpn .github/workflows/client.ovpn
```
Now save the `$OVPN_USER_KEY` from `client.ovpn`'s `<key>$OVPN_USER_KEY</key>` and remove the `<key>...</key>`.
Also save the `$OVPN_TLS_AUTH_KEY` from `<tls-auth...` section and remove it.
Add the secrets to your github repo
- `OVPN_USERNAME`: `github`
- `OVPN_PASSWORD`: the client passphrase
- `OVPN_USER_KEY`
- `OVPN_TLS_AUTH_KEY`
#### 4. SSH access
In order for ssh access by github, create a private key and add the pub part to the `authorized_keys` on the host:
```

ssh-keygen -t ed25519 -C "your_email@example.com"
cat ~/.ssh/id_ed25519.pub >> ~/.ssh/authorized_keys

```
Add the secrets to GitHub:
- `SERVER_HOST`: the hostname of this repo's api server
- `SERVER_USERNAME`: the username that has access to your host's ssh server
- `SSH_PRIVATE_KEY`: the private key of the user
#### 5. Make sure port 1194 is portforwarding the UDP protocol.
Now we can start the server and expect all to work ok.
If you wish to revoke a cert or do something else, please visit this page: [kylemanna/docker-openvpn/blob/master/docs/docker-compose.md](https://github.com/kylemanna/docker-openvpn/blob/master/docs/docker-compose.md)
## Questions one might have
Expand All @@ -237,3 +318,7 @@ In the future we might consider expanding this setup to use docker swarm, as it
**Don't blame this infra automation tooling for anything going wrong inside your containers!**
I suggest you repeat that mantra now and then and question yourself when things go wrong: where lies the problem?
```

```
32 changes: 25 additions & 7 deletions db.yml.sample
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,43 @@ projects:
services:
- name: host.docker.internal
port: 8888
- description: VPN server
domain: vpn.example.com
entrypoint: openvpn
name: vpn
services:
- additional_properties:
cap_add:
- NET_ADMIN
# change tag to x86_64 if not on ARM:
hostport: 1194
hostport: 1194
image: nubacuk/docker-openvpn:aarch64
name: openvpn
port: 1194
protocol: udp
restart: always
volumes:
- /etc/openvpn
- description: test project to demonstrate inter service connectivity
domain: hello.example.com
name: test
entrypoint: master
services:
- image: otomi/nodejs-helloworld:v1.2.13
name: master
env:
- env:
TARGET: cost concerned people
INFORMANT: http://test-informant:8080
image: otomi/nodejs-helloworld:v1.2.13
name: master
volumes:
- /data/bla
- /etc/dida
- image: otomi/nodejs-helloworld:v1.2.13
name: informant
- additional_properties:
cpus: 0.1
env:
TARGET: boss
additional_properties:
cpus: 0.1
image: otomi/nodejs-helloworld:v1.2.13
name: informant
- description: whoami service
domain: whoami.example.com
entrypoint: web
Expand Down
6 changes: 3 additions & 3 deletions lib/data_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def test_get_projects_with_filter(self, _: Mock) -> None:
domain="hello.example.com",
entrypoint="master",
services=[
test_projects[2].services[0],
test_projects[3].services[0],
],
),
]
Expand Down Expand Up @@ -125,8 +125,8 @@ def test_upsert_nonexistent_project_fixed(self, mock_write_projects: Mock, mock_
mock_write_projects.assert_called_once_with(test_projects + [new_project])

# Upsert a project's service' env
@mock.patch("lib.data.get_project", return_value=test_projects[2].copy())
@mock.patch("lib.data.get_service", return_value=test_projects[2].services[1].copy())
@mock.patch("lib.data.get_project", return_value=test_projects[3].model_copy())
@mock.patch("lib.data.get_service", return_value=test_projects[3].services[1].model_copy())
@mock.patch("lib.data.upsert_service")
def test_upsert_env(self, mock_upsert_service: Mock, mock_get_service: Mock, mock_get_project: Mock) -> None:

Expand Down
18 changes: 16 additions & 2 deletions lib/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from enum import Enum
from typing import Any, Dict, List

from github_webhooks.schemas import WebhookCommonPayload
Expand Down Expand Up @@ -30,13 +31,24 @@ class PluginRegistry(BaseModel):
crowdsec: Plugin


class Protocol(str, Enum):
"""Protocol enum"""

tcp = "tcp"
udp = "udp"


class Service(BaseModel):
"""Service model"""

additional_properties: Dict[str, Any] = {}
"""Additional docker compose properties to pass to the service"""
command: str = None
"""The command to run in the service"""
env: Env = None
"""A dictionary of environment variables to pass to the service"""
hostport: int = None
"""The port to expose on the host"""
image: str = None
"""The image name plus tag to use for the service"""
labels: List[str] = []
Expand All @@ -52,10 +64,12 @@ class Service(BaseModel):
"""When set, the service will be exposed on this domain."""
proxyprotocol: bool = True
"""When set, the service will be exposed using the PROXY protocol version 2"""
protocol: Protocol = Protocol.tcp
"""The protocol to use for the service"""
restart: str = "unless-stopped"
"""The restart policy to use for the service"""
volumes: List[str] = []
"""A list of volumes to mount in the service"""
additional_properties: Dict[str, Any] = {}
"""Additional docker compose properties to pass to the service"""


class Project(BaseModel):
Expand Down
Loading

0 comments on commit 4ac0845

Please sign in to comment.