mirror of
https://github.com/h44z/wg-portal.git
synced 2025-10-05 16:06:17 +00:00
Compare commits
20 Commits
v2.0.0-rc.
...
v2.0.0
Author | SHA1 | Date | |
---|---|---|---|
|
4ccc59c109 | ||
|
8271dd7c1f | ||
|
4ca37089bc | ||
|
8e5d5138c0 | ||
|
c73286e11a | ||
|
b4aa6f8ef3 | ||
|
432c627f9b | ||
|
cd60761ea7 | ||
|
2c8304417b | ||
|
020ebb64e7 | ||
|
923d4a6188 | ||
|
2b46dca770 | ||
|
b9c4ca04f5 | ||
|
e6b01a9903 | ||
|
2f79dd04c0 | ||
|
e5ed9736b3 | ||
|
c8353b85ae | ||
|
6142031387 | ||
|
dd86d0ff49 | ||
|
bdd426a679 |
2
.github/workflows/docker-publish.yml
vendored
2
.github/workflows/docker-publish.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
push:
|
||||
branches: [master, stable]
|
||||
branches: [master, stable, legacy]
|
||||
# Publish vX.X.X tags as releases.
|
||||
tags: ["v*.*.*"]
|
||||
|
||||
|
6
.github/workflows/pages.yml
vendored
6
.github/workflows/pages.yml
vendored
@@ -2,7 +2,11 @@ name: github-pages
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
tags: ["v*"]
|
||||
tags:
|
||||
- 'v*'
|
||||
- '!v*-alpha*'
|
||||
- '!v*-beta*'
|
||||
- '!v*-rc*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -33,6 +33,7 @@ ssh.key
|
||||
wg_portal.db
|
||||
sqlite.db
|
||||
/config.yml
|
||||
/config.yaml
|
||||
/config/
|
||||
venv/
|
||||
.cache/
|
||||
|
19
README.md
19
README.md
@@ -1,4 +1,4 @@
|
||||
# WireGuard Portal (v2 - testing)
|
||||
# WireGuard Portal v2
|
||||
|
||||
[](https://github.com/h44z/wg-portal/actions/workflows/docker-publish.yml)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
@@ -8,14 +8,6 @@
|
||||

|
||||
[](https://hub.docker.com/r/wgportal/wg-portal/)
|
||||
|
||||
> [!CAUTION]
|
||||
> Version 2 is currently under development and may contain bugs and breaking changes.
|
||||
> It is not advised to use this version in production. Use version [v1](https://github.com/h44z/wg-portal/tree/stable) instead.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Since the project was accepted by the Docker-Sponsored Open Source Program, the Docker image location has moved to [wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal).
|
||||
> Please update the Docker image from **h44z/wg-portal** to **wgportal/wg-portal**.
|
||||
|
||||
## Introduction
|
||||
<!-- Text from this line # is included in docs/documentation/overview.md -->
|
||||
**WireGuard Portal** is a simple, web-based configuration portal for [WireGuard](https://wireguard.com) server management.
|
||||
@@ -23,7 +15,7 @@ The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) l
|
||||
interfaces. This allows for the seamless activation or deactivation of new users without disturbing existing VPN
|
||||
connections.
|
||||
|
||||
The configuration portal supports using a database (SQLite, MySQL, MsSQL or Postgres), OAuth or LDAP
|
||||
The configuration portal supports using a database (SQLite, MySQL, MsSQL, or Postgres), OAuth or LDAP
|
||||
(Active Directory or OpenLDAP) as a user source for authentication and profile data.
|
||||
|
||||
## Features
|
||||
@@ -44,7 +36,7 @@ The configuration portal supports using a database (SQLite, MySQL, MsSQL or Post
|
||||
* Handles route and DNS settings like wg-quick does
|
||||
* Exposes Prometheus metrics for monitoring and alerting
|
||||
* REST API for management and client deployment
|
||||
* Webhook for custom actions on peer, interface or user updates
|
||||
* Webhook for custom actions on peer, interface, or user updates
|
||||
|
||||
<!-- Text to this line # is included in docs/documentation/overview.md -->
|
||||

|
||||
@@ -68,3 +60,8 @@ For the complete documentation visit [wgportal.org](https://wgportal.org).
|
||||
## License
|
||||
|
||||
* MIT License. [MIT](LICENSE.txt) or <https://opensource.org/licenses/MIT>
|
||||
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Since the project was accepted by the Docker-Sponsored Open Source Program, the Docker image location has moved to [wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal).
|
||||
> Please update the Docker image from **h44z/wg-portal** to **wgportal/wg-portal**.
|
||||
|
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
evbus "github.com/vardius/message-bus"
|
||||
"gorm.io/gorm/schema"
|
||||
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/adapters"
|
||||
@@ -41,6 +42,8 @@ func main() {
|
||||
|
||||
cfg.LogStartupValues()
|
||||
|
||||
dbEncryptedSerializer := app.NewGormEncryptedStringSerializer(cfg.Database.EncryptionPassphrase)
|
||||
schema.RegisterSerializer("encstr", dbEncryptedSerializer)
|
||||
rawDb, err := adapters.NewDatabase(cfg.Database)
|
||||
internal.AssertNoError(err)
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
---
|
||||
services:
|
||||
wg-portal:
|
||||
image: wgportal/wg-portal:latest
|
||||
image: wgportal/wg-portal:v2
|
||||
container_name: wg-portal
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
@@ -10,8 +10,10 @@ services:
|
||||
max-file: "3"
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
# Use host network mode for WireGuard and the UI. Ensure that access to the UI is properly secured.
|
||||
network_mode: "host"
|
||||
volumes:
|
||||
# left side is the host path, right side is the container path
|
||||
- /etc/wireguard:/etc/wireguard
|
||||
- ./data:/app/data
|
||||
- ./config:/app/config
|
||||
|
@@ -15,7 +15,7 @@ web:
|
||||
site_title: My WireGuard Server
|
||||
site_company_name: My Company
|
||||
listening_address: :8080
|
||||
external_url: https://my.externa-domain.com
|
||||
external_url: https://my.external-domain.com
|
||||
csrf_secret: super-s3cr3t-csrf
|
||||
session_secret: super-s3cr3t-session
|
||||
request_logging: true
|
||||
@@ -31,6 +31,7 @@ database:
|
||||
debug: true
|
||||
type: sqlite
|
||||
dsn: data/sqlite.db
|
||||
encryption_passphrase: change-this-s3cr3t-encryption-passphrase
|
||||
```
|
||||
|
||||
## LDAP Authentication and Synchronization
|
||||
|
@@ -1,10 +1,10 @@
|
||||
This page provides an overview of **all available configuration options** for WireGuard Portal.
|
||||
|
||||
You can supply these configurations in a **YAML** file (e.g. `config.yaml`) when starting the Portal.
|
||||
The path of the configuration file defaults to **config/config.yml** in the working directory of the executable.
|
||||
It is possible to override configuration filepath using the environment variable `WG_PORTAL_CONFIG`.
|
||||
You can supply these configurations in a **YAML** file when starting the Portal.
|
||||
The path of the configuration file defaults to `config/config.yaml` (or `config/config.yml`) in the working directory of the executable.
|
||||
It is possible to override the configuration filepath using the environment variable `WG_PORTAL_CONFIG`.
|
||||
For example: `WG_PORTAL_CONFIG=/etc/wg-portal/config.yaml ./wg-portal`.
|
||||
Also, environment variable substitution in config file is supported. Refer to [syntax](https://github.com/a8m/envsubst?tab=readme-ov-file#docs).
|
||||
Also, environment variable substitution in the config file is supported. Refer to the [syntax](https://github.com/a8m/envsubst?tab=readme-ov-file#docs).
|
||||
|
||||
Configuration examples are available on the [Examples](./examples.md) page.
|
||||
|
||||
@@ -15,6 +15,7 @@ Configuration examples are available on the [Examples](./examples.md) page.
|
||||
core:
|
||||
admin_user: admin@wgportal.local
|
||||
admin_password: wgportal
|
||||
admin_api_token: ""
|
||||
editable_keys: true
|
||||
create_default_peer: false
|
||||
create_default_peer_on_creation: false
|
||||
@@ -35,13 +36,15 @@ advanced:
|
||||
config_storage_path: ""
|
||||
expiry_check_interval: 15m
|
||||
rule_prio_offset: 20000
|
||||
route_table_offset: 20000
|
||||
api_admin_only: true
|
||||
|
||||
database:
|
||||
debug: false
|
||||
slow_query_threshold: 0
|
||||
slow_query_threshold: "0"
|
||||
type: sqlite
|
||||
dsn: data/sqlite.db
|
||||
encryption_passphrase: ""
|
||||
|
||||
statistics:
|
||||
use_ping_checks: true
|
||||
@@ -79,6 +82,7 @@ web:
|
||||
session_secret: very_secret
|
||||
csrf_secret: extremely_secret
|
||||
request_logging: false
|
||||
expose_host_info: false
|
||||
cert_file: ""
|
||||
key_File: ""
|
||||
|
||||
@@ -214,13 +218,15 @@ Additional or more specialized configuration options for logging and interface c
|
||||
Configuration for the underlying database used by WireGuard Portal.
|
||||
Supported databases include SQLite, MySQL, Microsoft SQL Server, and Postgres.
|
||||
|
||||
If sensitive values (like private keys) should be stored in an encrypted format, set the `encryption_passphrase` option.
|
||||
|
||||
### `debug`
|
||||
- **Default:** `false`
|
||||
- **Description:** If `true`, logs all database statements (verbose).
|
||||
|
||||
### `slow_query_threshold`
|
||||
- **Default:** 0
|
||||
- **Description:** A time threshold (e.g., `100ms`) above which queries are considered slow and logged as warnings. If empty or zero, slow query logging is disabled. Format uses `s`, `ms` for seconds, milliseconds, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
||||
- **Default:** "0"
|
||||
- **Description:** A time threshold (e.g., `100ms`) above which queries are considered slow and logged as warnings. If zero, slow query logging is disabled. Format uses `s`, `ms` for seconds, milliseconds, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration). The value must be a string.
|
||||
|
||||
### `type`
|
||||
- **Default:** `sqlite`
|
||||
@@ -234,6 +240,12 @@ Supported databases include SQLite, MySQL, Microsoft SQL Server, and Postgres.
|
||||
user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
|
||||
```
|
||||
|
||||
### `encryption_passphrase`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Passphrase for encrypting sensitive values such as private keys in the database. Encryption is only applied if this passphrase is set.
|
||||
**Important:** Once you enable encryption by setting this passphrase, you cannot disable it or change it afterward.
|
||||
New or updated records will be encrypted; existing data remains in plaintext until it’s next modified.
|
||||
|
||||
---
|
||||
|
||||
## Statistics
|
||||
@@ -274,7 +286,7 @@ Controls how WireGuard Portal collects and reports usage statistics, including p
|
||||
|
||||
### `listening_address`
|
||||
- **Default:** `:8787`
|
||||
- **Description:** Address and port for the integrated Prometheus metric server (e.g., `:8787`).
|
||||
- **Description:** Address and port for the integrated Prometheus metric server (e.g., `:8787` or `127.0.0.1:8787`).
|
||||
|
||||
---
|
||||
|
||||
@@ -568,7 +580,8 @@ Without a valid `external_url`, the login process may fail due to CSRF protectio
|
||||
|
||||
### `listening_address`
|
||||
- **Default:** `:8888`
|
||||
- **Description:** The listening port of the web server.
|
||||
- **Description:** The listening address and port for the web server (e.g., `:8888` to bind on all interfaces or `127.0.0.1:8888` to bind only on the loopback interface).
|
||||
Ensure that access to WireGuard Portal is protected against unauthorized access, especially if binding to all interfaces.
|
||||
|
||||
### `external_url`
|
||||
- **Default:** `http://localhost:8888`
|
||||
@@ -599,6 +612,10 @@ Without a valid `external_url`, the login process may fail due to CSRF protectio
|
||||
- **Default:** `false`
|
||||
- **Description:** Log all HTTP requests.
|
||||
|
||||
### `expose_host_info`
|
||||
- **Default:** `false`
|
||||
- **Description:** Expose the hostname and version of the WireGuard Portal server in an HTTP header. This is useful for debugging but may expose sensitive information.
|
||||
|
||||
### `cert_file`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** (Optional) Path to the TLS certificate file.
|
||||
|
@@ -3,23 +3,31 @@ These binary versions can be manually downloaded and installed.
|
||||
|
||||
## Download
|
||||
|
||||
Make sure that you download the correct binary for your architecture. The available binaries are:
|
||||
|
||||
- `wg-portal_linux_amd64` - Linux x86_64
|
||||
- `wg-portal_linux_arm64` - Linux ARM 64-bit
|
||||
- `wg-portal_linux_arm_v7` - Linux ARM 32-bit
|
||||
|
||||
With `curl`:
|
||||
|
||||
```shell
|
||||
curl -L -o wg-portal https://github.com/h44z/wg-portal/releases/download/${WG_PORTAL_VERSION}/wg-portal_linux_amd64
|
||||
```
|
||||
```shell
|
||||
curl -L -o wg-portal https://github.com/h44z/wg-portal/releases/download/${WG_PORTAL_VERSION}/wg-portal_linux_amd64
|
||||
```
|
||||
|
||||
With `wget`:
|
||||
|
||||
```shell
|
||||
wget -O wg-portal https://github.com/h44z/wg-portal/releases/download/${WG_PORTAL_VERSION}/wg-portal_linux_amd64
|
||||
```
|
||||
```shell
|
||||
wget -O wg-portal https://github.com/h44z/wg-portal/releases/download/${WG_PORTAL_VERSION}/wg-portal_linux_amd64
|
||||
```
|
||||
|
||||
with `gh cli`:
|
||||
|
||||
```shell
|
||||
gh release download ${WG_PORTAL_VERSION} --repo h44z/wg-portal --output wg-portal --pattern '*amd64'
|
||||
```
|
||||
```shell
|
||||
gh release download ${WG_PORTAL_VERSION} --repo h44z/wg-portal --output wg-portal --pattern '*amd64'
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Install
|
||||
|
||||
@@ -28,7 +36,7 @@ sudo mkdir -p /opt/wg-portal
|
||||
sudo install wg-portal /opt/wg-portal/
|
||||
```
|
||||
|
||||
## Unreleased
|
||||
## Unreleased versions (master branch builds)
|
||||
|
||||
Unreleased versions can be fetched directly from the artifacts section of the [GitHub Workflow](https://github.com/h44z/wg-portal/actions/workflows/docker-publish.yml?query=branch%3Amaster).
|
||||
|
||||
Unreleased versions could be downloaded from
|
||||
[GitHub Workflow](https://github.com/h44z/wg-portal/actions/workflows/docker-publish.yml?query=branch%3Amaster) artifacts also.
|
||||
|
@@ -10,10 +10,10 @@ The recommended method for deploying WireGuard Portal is via Docker Compose for
|
||||
A sample docker-compose.yml (managing WireGuard interfaces directly on the host) is provided below:
|
||||
|
||||
```yaml
|
||||
--8<-- "docker-compose.yml::17"
|
||||
--8<-- "docker-compose.yml::19"
|
||||
```
|
||||
|
||||
By default, the webserver is listening on port **8888**.
|
||||
By default, the webserver for the UI is listening on port **8888** on all available interfaces.
|
||||
|
||||
Volumes for `/app/data` and `/app/config` should be used ensure data persistence across container restarts.
|
||||
|
||||
@@ -32,6 +32,8 @@ WireGuard Portal supports managing WireGuard interfaces through three distinct d
|
||||
network_mode: "host"
|
||||
...
|
||||
```
|
||||
> :warning: If host networking is used, the WireGuard Portal UI will be accessible on all the host's IP addresses if the listening address is set to `:8888` in the configuration file.
|
||||
To avoid this, you can bind the listening address to a specific IP address, for example, the loopback address (`127.0.0.1:8888`). It is also possible to deploy firewall rules to restrict access to the WireGuard Portal UI.
|
||||
|
||||
- **Within the WireGuard Portal Docker container**:
|
||||
WireGuard interfaces can be managed directly from within the WireGuard Portal container itself.
|
||||
@@ -39,12 +41,13 @@ WireGuard Portal supports managing WireGuard interfaces through three distinct d
|
||||
```yaml
|
||||
services:
|
||||
wg-portal:
|
||||
image: wgportal/wg-portal:latest
|
||||
image: wgportal/wg-portal:v2
|
||||
container_name: wg-portal
|
||||
...
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
ports:
|
||||
# host port : container port
|
||||
# WireGuard port, needs to match the port in wg-portal interface config (add one port mapping for each interface)
|
||||
- "51820:51820/udp"
|
||||
# Web UI port
|
||||
@@ -52,6 +55,7 @@ WireGuard Portal supports managing WireGuard interfaces through three distinct d
|
||||
sysctls:
|
||||
- net.ipv4.conf.all.src_valid_mark=1
|
||||
volumes:
|
||||
# host path : container path
|
||||
- ./wg/data:/app/data
|
||||
- ./wg/config:/app/config
|
||||
```
|
||||
@@ -63,13 +67,14 @@ WireGuard Portal supports managing WireGuard interfaces through three distinct d
|
||||
```yaml
|
||||
services:
|
||||
wg-portal:
|
||||
image: wgportal/wg-portal:latest
|
||||
image: wgportal/wg-portal:v2
|
||||
container_name: wg-portal
|
||||
...
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
network_mode: "service:wireguard" # So we ensure to stay on the same network as the wireguard container.
|
||||
volumes:
|
||||
# host path : container path
|
||||
- ./wg/etc:/etc/wireguard
|
||||
- ./wg/data:/app/data
|
||||
- ./wg/config:/app/config
|
||||
@@ -81,6 +86,7 @@ WireGuard Portal supports managing WireGuard interfaces through three distinct d
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
ports:
|
||||
# host port : container port
|
||||
- "51820:51820/udp" # WireGuard port, needs to match the port in wg-portal interface config
|
||||
- "8888:8888/tcp" # Noticed that the port of the web UI is exposed in the wireguard container.
|
||||
volumes:
|
||||
@@ -114,11 +120,11 @@ These are official releases of WireGuard Portal. They correspond to the GitHub t
|
||||
|
||||
Once these tags show up in this repository, they will never change.
|
||||
|
||||
For production deployments of WireGuard Portal, we strongly recommend using one of these tags, e.g. **wgportal/wg-portal:1.0.19**, instead of the latest or canary tags.
|
||||
For production deployments of WireGuard Portal, we strongly recommend using one of these tags, e.g. `wgportal/wg-portal:2.0.0`, instead of the latest or canary tags.
|
||||
|
||||
If you only want to stay at the same major or major+minor version, use either `v[MAJOR]` or `[MAJOR].[MINOR]` tags. For example `v1` or `1.0`.
|
||||
If you only want to stay at the same major or major+minor version, use either `v[MAJOR]` or `[MAJOR].[MINOR]` tags. For example `v2` or `2.0`.
|
||||
|
||||
Version **1** is currently **stable**, version **2** is in **development**.
|
||||
Version **2** is the current stable release. Version **1** has moved to legacy status and is no longer recommended.
|
||||
|
||||
#### latest
|
||||
|
||||
@@ -133,7 +139,7 @@ For each commit in the master and the stable branch, a corresponding Docker imag
|
||||
## Configuration
|
||||
|
||||
You can configure WireGuard Portal using a YAML configuration file.
|
||||
The filepath of the YAML configuration file defaults to `/app/config/config.yml`.
|
||||
The filepath of the YAML configuration file defaults to `/app/config/config.yaml`.
|
||||
It is possible to override the configuration filepath using the environment variable **WG_PORTAL_CONFIG**.
|
||||
|
||||
By default, WireGuard Portal uses an SQLite database. The database is stored in `/app/data/sqlite.db`.
|
||||
@@ -145,7 +151,7 @@ You should mount those directories as a volume:
|
||||
|
||||
A detailed description of the configuration options can be found [here](../configuration/overview.md).
|
||||
|
||||
If you want to access configuration files in wg-quick format, you can mount the `/etc/wireguard` directory to a location of your choice.
|
||||
If you want to access configuration files in wg-quick format, you can mount the `/etc/wireguard` directory inside the container to a location of your choice.
|
||||
Also enable the `config_storage_path` option in the configuration file:
|
||||
```yaml
|
||||
advanced:
|
||||
|
98
docs/documentation/getting-started/reverse-proxy.md
Normal file
98
docs/documentation/getting-started/reverse-proxy.md
Normal file
@@ -0,0 +1,98 @@
|
||||
## Reverse Proxy for HTTPS
|
||||
|
||||
For production deployments, always serve the WireGuard Portal over HTTPS. You have two options to secure your connection:
|
||||
|
||||
|
||||
### Reverse Proxy
|
||||
|
||||
Let a front‐end proxy handle HTTPS for you. This also frees you from managing certificates manually and is therefore the preferred option.
|
||||
You can use Nginx, Traefik, Caddy or any other proxy.
|
||||
|
||||
Below is an example using a Docker Compose stack with [Traefik](https://traefik.io/traefik/).
|
||||
It exposes the WireGuard Portal on `https://wg.domain.com` and redirects initial HTTP traffic to HTTPS.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
reverse-proxy:
|
||||
image: traefik:v3.3
|
||||
restart: unless-stopped
|
||||
command:
|
||||
#- '--log.level=DEBUG'
|
||||
- '--providers.docker.endpoint=unix:///var/run/docker.sock'
|
||||
- '--providers.docker.exposedbydefault=false'
|
||||
- '--entrypoints.web.address=:80'
|
||||
- '--entrypoints.websecure.address=:443'
|
||||
- '--entrypoints.websecure.http3'
|
||||
- '--certificatesresolvers.letsencryptresolver.acme.httpchallenge=true'
|
||||
- '--certificatesresolvers.letsencryptresolver.acme.httpchallenge.entrypoint=web'
|
||||
- '--certificatesresolvers.letsencryptresolver.acme.email=your.email@domain.com'
|
||||
- '--certificatesresolvers.letsencryptresolver.acme.storage=/letsencrypt/acme.json'
|
||||
#- '--certificatesresolvers.letsencryptresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory' # just for testing
|
||||
ports:
|
||||
- 80:80 # for HTTP
|
||||
- 443:443/tcp # for HTTPS
|
||||
- 443:443/udp # for HTTP/3
|
||||
volumes:
|
||||
- acme-certs:/letsencrypt
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
labels:
|
||||
- 'traefik.enable=true'
|
||||
# HTTP Catchall for redirecting HTTP -> HTTPS
|
||||
- 'traefik.http.routers.dashboard-catchall.rule=Host(`wg.domain.com`) && PathPrefix(`/`)'
|
||||
- 'traefik.http.routers.dashboard-catchall.entrypoints=web'
|
||||
- 'traefik.http.routers.dashboard-catchall.middlewares=redirect-to-https'
|
||||
- 'traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https'
|
||||
|
||||
wg-portal:
|
||||
image: wgportal/wg-portal:v2
|
||||
container_name: wg-portal
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
ports:
|
||||
# host port : container port
|
||||
# WireGuard port, needs to match the port in wg-portal interface config (add one port mapping for each interface)
|
||||
- "51820:51820/udp"
|
||||
# Web UI port (only available on localhost, Traefik will handle the HTTPS)
|
||||
- "127.0.0.1:8888:8888/tcp"
|
||||
sysctls:
|
||||
- net.ipv4.conf.all.src_valid_mark=1
|
||||
volumes:
|
||||
# host path : container path
|
||||
- ./wg/data:/app/data
|
||||
- ./wg/config:/app/config
|
||||
labels:
|
||||
- 'traefik.enable=true'
|
||||
- 'traefik.http.routers.wgportal.rule=Host(`wg.domain.com`)'
|
||||
- 'traefik.http.routers.wgportal.entrypoints=websecure'
|
||||
- 'traefik.http.routers.wgportal.tls.certresolver=letsencryptresolver'
|
||||
- 'traefik.http.routers.wgportal.service=wgportal'
|
||||
- 'traefik.http.services.wgportal.loadbalancer.server.port=8888'
|
||||
|
||||
volumes:
|
||||
acme-certs:
|
||||
```
|
||||
|
||||
The WireGuard Portal configuration must be updated accordingly so that the correct external URL is set for the web interface:
|
||||
|
||||
```yaml
|
||||
web:
|
||||
external_url: https://wg.domain.com
|
||||
```
|
||||
|
||||
### Built-in TLS
|
||||
|
||||
If you prefer to let WireGuard Portal handle TLS itself, you can use the built-in TLS support.
|
||||
In your `config.yaml`, under the `web` section, point to your certificate and key files:
|
||||
|
||||
```yaml
|
||||
web:
|
||||
cert_file: /path/to/your/fullchain.pem
|
||||
key_file: /path/to/your/privkey.pem
|
||||
```
|
||||
|
||||
The web server will then use these files to serve HTTPS traffic directly instead of HTTP.
|
@@ -22,4 +22,5 @@ make build
|
||||
## Install
|
||||
|
||||
Compiled binary will be available in `./dist` directory.
|
||||
|
||||
For installation instructions, check the [Binaries](./binaries.md) section.
|
||||
|
@@ -13,7 +13,7 @@ By default, WG-Portal exposes Prometheus metrics on port `8787` if interface/pee
|
||||
|
||||
## Prometheus Config
|
||||
|
||||
Add following scrape job to your Prometheus config file:
|
||||
Add the following scrape job to your Prometheus config file:
|
||||
|
||||
```yaml
|
||||
# prometheus.yaml
|
||||
|
@@ -1 +1 @@
|
||||
--8<-- "README.md:20:47"
|
||||
--8<-- "README.md:12:41"
|
||||
|
@@ -1,12 +1,11 @@
|
||||
For production deployments of WireGuard Portal, we strongly recommend using version 1.
|
||||
If you want to use version 2, please be aware that it is still a release candidate and not yet fully stable.
|
||||
Major upgrades between different versions may require special procedures, which are described in the following sections.
|
||||
|
||||
## Upgrade from v1 to v2
|
||||
|
||||
> :warning: Before upgrading from V1, make sure that you have a backup of your currently working configuration files and database!
|
||||
|
||||
To start the upgrade process, start the wg-portal binary with the **-migrateFrom** parameter.
|
||||
The configuration (config.yml) for WireGuard Portal must be updated and valid before starting the upgrade.
|
||||
The configuration (config.yaml) for WireGuard Portal must be updated and valid before starting the upgrade.
|
||||
|
||||
To upgrade from a previous SQLite database, start wg-portal like:
|
||||
|
||||
@@ -14,14 +13,16 @@ To upgrade from a previous SQLite database, start wg-portal like:
|
||||
./wg-portal-amd64 -migrateFrom=old_wg_portal.db
|
||||
```
|
||||
|
||||
You can also specify the database type using the parameter **-migrateFromType**, supported types: mysql, mssql, postgres or sqlite.
|
||||
You can also specify the database type using the parameter **-migrateFromType**.
|
||||
Supported database types: `mysql`, `mssql`, `postgres` or `sqlite`.
|
||||
|
||||
For example:
|
||||
|
||||
```shell
|
||||
./wg-portal-amd64 -migrateFromType=mysql -migrateFrom='user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local'
|
||||
```
|
||||
|
||||
The upgrade will transform the old, existing database and store the values in the new database specified in the **config.yml** configuration file.
|
||||
The upgrade will transform the old, existing database and store the values in the new database specified in the **config.yaml** configuration file.
|
||||
Ensure that the new database does not contain any data!
|
||||
|
||||
If you are using Docker, you can adapt the docker-compose.yml file to start the upgrade process:
|
||||
@@ -29,8 +30,8 @@ If you are using Docker, you can adapt the docker-compose.yml file to start the
|
||||
```yaml
|
||||
services:
|
||||
wg-portal:
|
||||
image: wgportal/wg-portal:latest
|
||||
image: wgportal/wg-portal:v2
|
||||
# ... other settings
|
||||
restart: no
|
||||
command: ["-migrateFrom=/app/data/wg_portal.db"]
|
||||
command: ["-migrateFrom=/app/data/old_wg_portal.db"]
|
||||
```
|
||||
|
20
frontend/package-lock.json
generated
20
frontend/package-lock.json
generated
@@ -29,7 +29,7 @@
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"sass-embedded": "^1.86.3",
|
||||
"vite": "6.3.2"
|
||||
"vite": "6.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
@@ -2012,13 +2012,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz",
|
||||
"integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==",
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
|
||||
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.4.3",
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2043,18 +2043,18 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.2.tgz",
|
||||
"integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==",
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz",
|
||||
"integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.3",
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2",
|
||||
"postcss": "^8.5.3",
|
||||
"rollup": "^4.34.9",
|
||||
"tinyglobby": "^0.2.12"
|
||||
"tinyglobby": "^0.2.13"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
|
@@ -29,6 +29,6 @@
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"sass-embedded": "^1.86.3",
|
||||
"vite": "6.3.2"
|
||||
"vite": "6.3.4"
|
||||
}
|
||||
}
|
||||
|
@@ -85,6 +85,9 @@ const currentYear = ref(new Date().getFullYear())
|
||||
<li v-if="auth.IsAuthenticated && auth.IsAdmin" class="nav-item">
|
||||
<RouterLink :to="{ name: 'users' }" class="nav-link">{{ $t('menu.users') }}</RouterLink>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<RouterLink :to="{ name: 'key-generator' }" class="nav-link">{{ $t('menu.keygen') }}</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="navbar-nav d-flex justify-content-end">
|
||||
|
@@ -5,6 +5,10 @@ $web-font-path: false;
|
||||
@import "bootstrap/scss/bootstrap";
|
||||
@import "bootswatch/dist/lux/bootswatch";
|
||||
|
||||
// fix strange border width bug in bootswatch 5.3
|
||||
:root {
|
||||
--bs-border-width: 1px;
|
||||
}
|
||||
|
||||
// for future use, once bootswatch supports @use
|
||||
/*
|
||||
|
@@ -331,11 +331,11 @@ async function del() {
|
||||
<legend class="mt-4">{{ $t('modals.interface-edit.header-crypto') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.private-key.label') }}</label>
|
||||
<input v-model="formData.PrivateKey" class="form-control" :placeholder="$t('modals.interface-edit.private-key.placeholder')" required type="email">
|
||||
<input v-model="formData.PrivateKey" class="form-control" :placeholder="$t('modals.interface-edit.private-key.placeholder')" required type="text">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.public-key.label') }}</label>
|
||||
<input v-model="formData.PublicKey" class="form-control" :placeholder="$t('modals.interface-edit.public-key.placeholder')" required type="email">
|
||||
<input v-model="formData.PublicKey" class="form-control" :placeholder="$t('modals.interface-edit.public-key.placeholder')" required type="text">
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
|
@@ -323,17 +323,18 @@ async function del() {
|
||||
<legend class="mt-4">{{ $t('modals.peer-edit.header-crypto') }}</legend>
|
||||
<div class="form-group" v-if="selectedInterface.Mode === 'server'">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.private-key.label') }}</label>
|
||||
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.private-key.placeholder')" required
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.private-key.placeholder')" required
|
||||
v-model="formData.PrivateKey">
|
||||
<small id="privateKeyHelp" class="form-text text-muted">{{ $t('modals.peer-edit.private-key.help') }}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.public-key.label') }}</label>
|
||||
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.public-key.placeholder')" required
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.public-key.placeholder')" required
|
||||
v-model="formData.PublicKey">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.preshared-key.label') }}</label>
|
||||
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.preshared-key.placeholder')"
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.preshared-key.placeholder')"
|
||||
v-model="formData.PresharedKey">
|
||||
</div>
|
||||
<div class="form-group" v-if="formData.Mode === 'client'">
|
||||
|
@@ -211,17 +211,18 @@ async function del() {
|
||||
<legend class="mt-4">{{ $t('modals.peer-edit.header-crypto') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.private-key.label') }}</label>
|
||||
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.private-key.placeholder')" required
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.private-key.placeholder')" required
|
||||
v-model="formData.PrivateKey">
|
||||
<small id="privateKeyHelp" class="form-text text-muted">{{ $t('modals.peer-edit.private-key.help') }}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.public-key.label') }}</label>
|
||||
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.public-key.placeholder')" required
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.public-key.placeholder')" required
|
||||
v-model="formData.PublicKey">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.preshared-key.label') }}</label>
|
||||
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.preshared-key.placeholder')"
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.preshared-key.placeholder')"
|
||||
v-model="formData.PresharedKey">
|
||||
</div>
|
||||
</fieldset>
|
||||
|
@@ -26,7 +26,7 @@
|
||||
"placeholder": "Bitte geben Sie Ihren Benutzernamen ein"
|
||||
},
|
||||
"password": {
|
||||
"label": "Kennwort",
|
||||
"label": "Passwort",
|
||||
"placeholder": "Bitte geben Sie Ihr Passwort ein"
|
||||
},
|
||||
"button": "Anmelden"
|
||||
@@ -38,8 +38,10 @@
|
||||
"lang": "Sprache ändern",
|
||||
"profile": "Mein Profil",
|
||||
"settings": "Einstellungen",
|
||||
"audit": "Event Protokoll",
|
||||
"login": "Anmelden",
|
||||
"logout": "Abmelden"
|
||||
"logout": "Abmelden",
|
||||
"keygen": "Schlüsselgenerator"
|
||||
},
|
||||
"home": {
|
||||
"headline": "WireGuard® VPN Portal",
|
||||
@@ -79,77 +81,77 @@
|
||||
},
|
||||
"interfaces": {
|
||||
"headline": "Schnittstellenverwaltung",
|
||||
"headline-peers": "Current VPN Peers",
|
||||
"headline-endpoints": "Current Endpoints",
|
||||
"headline-peers": "Aktuelle VPN-Peers",
|
||||
"headline-endpoints": "Aktuelle Endpunkte",
|
||||
"no-interface": {
|
||||
"default-selection": "No Interface available",
|
||||
"headline": "No interfaces found...",
|
||||
"abstract": "Click the plus button above to create a new WireGuard interface."
|
||||
"default-selection": "Keine Schnittstelle verfügbar",
|
||||
"headline": "Keine Schnittstellen gefunden...",
|
||||
"abstract": "Klicken Sie auf die Plus-Schaltfläche oben, um eine neue WireGuard-Schnittstelle zu erstellen."
|
||||
},
|
||||
"no-peer": {
|
||||
"headline": "No peers available",
|
||||
"abstract": "Currently, there are no peers available for the selected WireGuard interface."
|
||||
"headline": "Keine Peers verfügbar",
|
||||
"abstract": "Derzeit sind keine Peers für die ausgewählte WireGuard-Schnittstelle verfügbar."
|
||||
},
|
||||
"table-heading": {
|
||||
"name": "Name",
|
||||
"user": "User",
|
||||
"user": "Benutzer",
|
||||
"ip": "IP's",
|
||||
"endpoint": "Endpoint",
|
||||
"endpoint": "Endpunkt",
|
||||
"status": "Status"
|
||||
},
|
||||
"interface": {
|
||||
"headline": "Interface status for",
|
||||
"mode": "mode",
|
||||
"key": "Public Key",
|
||||
"endpoint": "Public Endpoint",
|
||||
"port": "Listening Port",
|
||||
"peers": "Enabled Peers",
|
||||
"total-peers": "Total Peers",
|
||||
"endpoints": "Enabled Endpoints",
|
||||
"total-endpoints": "Total Endpoints",
|
||||
"ip": "IP Address",
|
||||
"default-allowed-ip": "Default allowed IPs",
|
||||
"dns": "DNS Servers",
|
||||
"headline": "Schnittstellenstatus für",
|
||||
"mode": "Modus",
|
||||
"key": "Öffentlicher Schlüssel",
|
||||
"endpoint": "Öffentlicher Endpunkt",
|
||||
"port": "Port",
|
||||
"peers": "Aktive Peers",
|
||||
"total-peers": "Gesamtanzahl Peers",
|
||||
"endpoints": "Aktive Endpunkte",
|
||||
"total-endpoints": "Gesamtanzahl Endpunkte",
|
||||
"ip": "IP-Adresse",
|
||||
"default-allowed-ip": "Standard Erlaubte-IPs",
|
||||
"dns": "DNS-Server",
|
||||
"mtu": "MTU",
|
||||
"default-keep-alive": "Default Keepalive Interval",
|
||||
"button-show-config": "Show configuration",
|
||||
"button-download-config": "Download configuration",
|
||||
"button-store-config": "Store configuration for wg-quick",
|
||||
"button-edit": "Edit interface"
|
||||
"default-keep-alive": "Standard Keepalive-Intervall",
|
||||
"button-show-config": "Konfiguration anzeigen",
|
||||
"button-download-config": "Konfiguration herunterladen",
|
||||
"button-store-config": "Konfiguration für wg-quick speichern",
|
||||
"button-edit": "Schnittstelle bearbeiten"
|
||||
},
|
||||
"button-add-interface": "Add Interface",
|
||||
"button-add-peer": "Add Peer",
|
||||
"button-add-peers": "Add Multiple Peers",
|
||||
"button-show-peer": "Show Peer",
|
||||
"button-edit-peer": "Edit Peer",
|
||||
"peer-disabled": "Peer is disabled, reason:",
|
||||
"peer-expiring": "Peer is expiring at",
|
||||
"peer-connected": "Connected",
|
||||
"peer-not-connected": "Not Connected",
|
||||
"peer-handshake": "Last handshake:"
|
||||
"button-add-interface": "Schnittstelle hinzufügen",
|
||||
"button-add-peer": "Peer hinzufügen",
|
||||
"button-add-peers": "Mehrere Peers hinzufügen",
|
||||
"button-show-peer": "Peer anzeigen",
|
||||
"button-edit-peer": "Peer bearbeiten",
|
||||
"peer-disabled": "Peer ist deaktiviert, Grund:",
|
||||
"peer-expiring": "Peer läuft ab am",
|
||||
"peer-connected": "Verbunden",
|
||||
"peer-not-connected": "Nicht verbunden",
|
||||
"peer-handshake": "Letzter Handshake:"
|
||||
},
|
||||
"users": {
|
||||
"headline": "Benutzerverwaltung",
|
||||
"table-heading": {
|
||||
"id": "ID",
|
||||
"email": "E-Mail",
|
||||
"firstname": "Firstname",
|
||||
"lastname": "Lastname",
|
||||
"source": "Source",
|
||||
"firstname": "Vorname",
|
||||
"lastname": "Nachname",
|
||||
"source": "Quelle",
|
||||
"peers": "Peers",
|
||||
"admin": "Admin"
|
||||
},
|
||||
"no-user": {
|
||||
"headline": "No users available",
|
||||
"abstract": "Currently, there are no users registered with WireGuard Portal."
|
||||
"headline": "Keine Benutzer verfügbar",
|
||||
"abstract": "Derzeit sind keine Benutzer im WireGuard-Portal registriert."
|
||||
},
|
||||
"button-add-user": "Add User",
|
||||
"button-show-user": "Show User",
|
||||
"button-edit-user": "Edit User",
|
||||
"user-disabled": "User is disabled, reason:",
|
||||
"user-locked": "Account is locked, reason:",
|
||||
"admin": "User has administrator privileges",
|
||||
"no-admin": "User has no administrator privileges"
|
||||
"button-add-user": "Benutzer hinzufügen",
|
||||
"button-show-user": "Benutzer anzeigen",
|
||||
"button-edit-user": "Benutzer bearbeiten",
|
||||
"user-disabled": "Benutzer ist deaktiviert, Grund:",
|
||||
"user-locked": "Konto ist gesperrt, Grund:",
|
||||
"admin": "Benutzer hat Administratorrechte",
|
||||
"no-admin": "Benutzer hat keine Administratorrechte"
|
||||
},
|
||||
"profile": {
|
||||
"headline": "Meine VPN-Konfigurationen",
|
||||
@@ -157,16 +159,16 @@
|
||||
"name": "Name",
|
||||
"ip": "IP's",
|
||||
"stats": "Status",
|
||||
"interface": "Server Interface"
|
||||
"interface": "Server-Schnittstelle"
|
||||
},
|
||||
"no-peer": {
|
||||
"headline": "No peers available",
|
||||
"abstract": "Currently, there are no peers associated with your user profile."
|
||||
"headline": "Keine Peers verfügbar",
|
||||
"abstract": "Derzeit sind keine Peers mit Ihrem Benutzerprofil verknüpft."
|
||||
},
|
||||
"peer-connected": "Connected",
|
||||
"button-add-peer": "Add Peer",
|
||||
"button-show-peer": "Show Peer",
|
||||
"button-edit-peer": "Edit Peer"
|
||||
"peer-connected": "Verbunden",
|
||||
"button-add-peer": "Peer hinzufügen",
|
||||
"button-show-peer": "Peer anzeigen",
|
||||
"button-edit-peer": "Peer bearbeiten"
|
||||
},
|
||||
"settings": {
|
||||
"headline": "Einstellungen",
|
||||
@@ -188,325 +190,362 @@
|
||||
"api-link": "API Dokumentation"
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
"headline": "Eventprotokoll",
|
||||
"abstract": "Hier finden Sie das Eventprotokoll aller im WireGuard-Portal vorgenommenen Aktionen.",
|
||||
"no-entries": {
|
||||
"headline": "Keine Protokolleinträge verfügbar",
|
||||
"abstract": "Derzeit sind keine Eventprotokolle aufgezeichnet."
|
||||
},
|
||||
"entries-headline": "Protokolleinträge",
|
||||
"table-heading": {
|
||||
"id": "#",
|
||||
"time": "Zeit",
|
||||
"user": "Benutzer",
|
||||
"severity": "Schweregrad",
|
||||
"origin": "Ursprung",
|
||||
"message": "Nachricht"
|
||||
}
|
||||
},
|
||||
"keygen": {
|
||||
"headline": "WireGuard Key Generator",
|
||||
"abstract": "Hier können Sie WireGuard Schlüsselpaare generieren. Die Schlüssel werden lokal auf Ihrem Computer generiert und niemals an den Server gesendet.",
|
||||
"headline-keypair": "Neues Schlüsselpaar",
|
||||
"headline-preshared-key": "Neuer Pre-Shared Key",
|
||||
"button-generate": "Erzeugen",
|
||||
"private-key": {
|
||||
"label": "Privater Schlüssel",
|
||||
"placeholder": "Der private Schlüssel"
|
||||
},
|
||||
"public-key": {
|
||||
"label": "Öffentlicher Schlüssel",
|
||||
"placeholder": "Der öffentliche Schlüssel"
|
||||
},
|
||||
"preshared-key": {
|
||||
"label": "Pre-Shared Key",
|
||||
"placeholder": "Der geteilte Schlüssel"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"user-view": {
|
||||
"headline": "User Account:",
|
||||
"tab-user": "Information",
|
||||
"headline": "Benutzerkonto:",
|
||||
"tab-user": "Informationen",
|
||||
"tab-peers": "Peers",
|
||||
"headline-info": "User Information:",
|
||||
"headline-notes": "Notes:",
|
||||
"headline-info": "Benutzerinformationen:",
|
||||
"headline-notes": "Notizen:",
|
||||
"email": "E-Mail",
|
||||
"firstname": "Firstname",
|
||||
"lastname": "Lastname",
|
||||
"phone": "Phone number",
|
||||
"department": "Department",
|
||||
"disabled": "Account Disabled",
|
||||
"locked": "Account Locked",
|
||||
"no-peers": "User has no associated peers.",
|
||||
"firstname": "Vorname",
|
||||
"lastname": "Nachname",
|
||||
"phone": "Telefonnummer",
|
||||
"department": "Abteilung",
|
||||
"api-enabled": "API-Zugriff",
|
||||
"disabled": "Konto deaktiviert",
|
||||
"locked": "Konto gesperrt",
|
||||
"no-peers": "Benutzer hat keine zugeordneten Peers.",
|
||||
"peers": {
|
||||
"name": "Name",
|
||||
"interface": "Interface",
|
||||
"interface": "Schnittstelle",
|
||||
"ip": "IP's"
|
||||
}
|
||||
},
|
||||
"user-edit": {
|
||||
"headline-edit": "Edit user:",
|
||||
"headline-new": "New user",
|
||||
"header-general": "General",
|
||||
"header-personal": "User Information",
|
||||
"header-notes": "Notes",
|
||||
"header-state": "State",
|
||||
"headline-edit": "Benutzer bearbeiten:",
|
||||
"headline-new": "Neuer Benutzer",
|
||||
"header-general": "Allgemein",
|
||||
"header-personal": "Benutzerinformationen",
|
||||
"header-notes": "Notizen",
|
||||
"header-state": "Status",
|
||||
"identifier": {
|
||||
"label": "Identifier",
|
||||
"placeholder": "The unique user identifier"
|
||||
"label": "Kennung",
|
||||
"placeholder": "Die eindeutige Benutzerkennung"
|
||||
},
|
||||
"source": {
|
||||
"label": "Source",
|
||||
"placeholder": "The user source"
|
||||
"label": "Quelle",
|
||||
"placeholder": "Die Benutzerquelle"
|
||||
},
|
||||
"password": {
|
||||
"label": "Password",
|
||||
"placeholder": "A super secret password",
|
||||
"description": "Leave this field blank to keep current password."
|
||||
"label": "Passwort",
|
||||
"placeholder": "Ein super geheimes Passwort",
|
||||
"description": "Lassen Sie dieses Feld leer, um das aktuelle Passwort beizubehalten."
|
||||
},
|
||||
"email": {
|
||||
"label": "Email",
|
||||
"placeholder": "The email address"
|
||||
"label": "E-Mail",
|
||||
"placeholder": "Die E-Mail-Adresse"
|
||||
},
|
||||
"phone": {
|
||||
"label": "Phone",
|
||||
"placeholder": "The phone number"
|
||||
"label": "Telefon",
|
||||
"placeholder": "Die Telefonnummer"
|
||||
},
|
||||
"department": {
|
||||
"label": "Department",
|
||||
"placeholder": "The department"
|
||||
"label": "Abteilung",
|
||||
"placeholder": "Die Abteilung"
|
||||
},
|
||||
"firstname": {
|
||||
"label": "Firstname",
|
||||
"placeholder": "Firstname"
|
||||
"label": "Vorname",
|
||||
"placeholder": "Vorname"
|
||||
},
|
||||
"lastname": {
|
||||
"label": "Lastname",
|
||||
"placeholder": "Lastname"
|
||||
"label": "Nachname",
|
||||
"placeholder": "Nachname"
|
||||
},
|
||||
"notes": {
|
||||
"label": "Notes",
|
||||
"label": "Notizen",
|
||||
"placeholder": ""
|
||||
},
|
||||
"disabled": {
|
||||
"label": "Disabled (no WireGuard connection and no login possible)"
|
||||
"label": "Deaktiviert (keine WireGuard-Verbindung und kein Login möglich)"
|
||||
},
|
||||
"locked": {
|
||||
"label": "Locked (no login possible, WireGuard connections still work)"
|
||||
"label": "Gesperrt (kein Login möglich, WireGuard-Verbindungen funktionieren weiterhin)"
|
||||
},
|
||||
"admin": {
|
||||
"label": "Is Admin"
|
||||
"label": "Ist Administrator"
|
||||
}
|
||||
},
|
||||
"interface-view": {
|
||||
"headline": "Config for Interface:"
|
||||
"headline": "Konfiguration für Schnittstelle:"
|
||||
},
|
||||
"interface-edit": {
|
||||
"headline-edit": "Edit Interface:",
|
||||
"headline-new": "New Interface",
|
||||
"tab-interface": "Interface",
|
||||
"tab-peerdef": "Peer Defaults",
|
||||
"header-general": "General",
|
||||
"header-network": "Network",
|
||||
"header-crypto": "Cryptography",
|
||||
"header-hooks": "Interface Hooks",
|
||||
"headline-edit": "Schnittstelle bearbeiten:",
|
||||
"headline-new": "Neue Schnittstelle",
|
||||
"tab-interface": "Schnittstelle",
|
||||
"tab-peerdef": "Peer-Standardeinstellungen",
|
||||
"header-general": "Allgemein",
|
||||
"header-network": "Netzwerk",
|
||||
"header-crypto": "Kryptografie",
|
||||
"header-hooks": "Schnittstellen-Hooks",
|
||||
"header-peer-hooks": "Hooks",
|
||||
"header-state": "State",
|
||||
"header-state": "Status",
|
||||
"identifier": {
|
||||
"label": "Identifier",
|
||||
"placeholder": "The unique interface identifier"
|
||||
"label": "Kennung",
|
||||
"placeholder": "Die eindeutige Schnittstellenkennung"
|
||||
},
|
||||
"mode": {
|
||||
"label": "Interface Mode",
|
||||
"server": "Server Mode",
|
||||
"client": "Client Mode",
|
||||
"any": "Unknown Mode"
|
||||
"label": "Schnittstellenmodus",
|
||||
"server": "Server-Modus",
|
||||
"client": "Client-Modus",
|
||||
"any": "Unbekannter Modus"
|
||||
},
|
||||
"display-name": {
|
||||
"label": "Display Name",
|
||||
"placeholder": "The descriptive name for the interface"
|
||||
"label": "Anzeigename",
|
||||
"placeholder": "Der beschreibende Name für die Schnittstelle"
|
||||
},
|
||||
"private-key": {
|
||||
"label": "Private Key",
|
||||
"placeholder": "The private key"
|
||||
"label": "Privater Schlüssel",
|
||||
"placeholder": "Der private Schlüssel"
|
||||
},
|
||||
"public-key": {
|
||||
"label": "Public Key",
|
||||
"placeholder": "The public key"
|
||||
"label": "Öffentlicher Schlüssel",
|
||||
"placeholder": "Der öffentliche Schlüssel"
|
||||
},
|
||||
"ip": {
|
||||
"label": "IP Addresses",
|
||||
"placeholder": "IP Addresses (CIDR format)"
|
||||
"label": "IP-Adressen",
|
||||
"placeholder": "IP-Adressen (CIDR-Format)"
|
||||
},
|
||||
"listen-port": {
|
||||
"label": "Listen Port",
|
||||
"placeholder": "The listening port"
|
||||
"label": "Port",
|
||||
"placeholder": "Der Port der WireGuard Schnittstelle"
|
||||
},
|
||||
"dns": {
|
||||
"label": "DNS Server",
|
||||
"placeholder": "The DNS servers that should be used"
|
||||
"label": "DNS-Server",
|
||||
"placeholder": "Die zu verwendenden DNS-Server"
|
||||
},
|
||||
"dns-search": {
|
||||
"label": "DNS Search Domains",
|
||||
"placeholder": "DNS search prefixes"
|
||||
"label": "DNS-Suchdomänen",
|
||||
"placeholder": "DNS-Suchpräfixe"
|
||||
},
|
||||
"mtu": {
|
||||
"label": "MTU",
|
||||
"placeholder": "The interface MTU (0 = keep default)"
|
||||
"placeholder": "Die Schnittstellen-MTU (0 = Standard beibehalten)"
|
||||
},
|
||||
"firewall-mark": {
|
||||
"label": "Firewall Mark",
|
||||
"placeholder": "Firewall mark that is applied to outgoing traffic. (0 = automatic)"
|
||||
"label": "Firewall-Markierung",
|
||||
"placeholder": "Firewall-Markierung, die auf ausgehenden Datenverkehr angewendet wird. (0 = automatisch)"
|
||||
},
|
||||
"routing-table": {
|
||||
"label": "Routing Table",
|
||||
"placeholder": "The routing table ID",
|
||||
"description": "Special cases: off = do not manage routes, 0 = automatic"
|
||||
"label": "Routing-Tabelle",
|
||||
"placeholder": "Die Routing-Tabellen-ID",
|
||||
"description": "Spezialfälle: off = Routen nicht verwalten, 0 = automatisch"
|
||||
},
|
||||
"pre-up": {
|
||||
"label": "Pre-Up",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||
},
|
||||
"post-up": {
|
||||
"label": "Post-Up",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||
},
|
||||
"pre-down": {
|
||||
"label": "Pre-Down",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||
},
|
||||
"post-down": {
|
||||
"label": "Post-Down",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||
},
|
||||
"disabled": {
|
||||
"label": "Interface Disabled"
|
||||
"label": "Schnittstelle deaktiviert"
|
||||
},
|
||||
"save-config": {
|
||||
"label": "Automatically save wg-quick config"
|
||||
"label": "wg-quick Konfiguration automatisch speichern"
|
||||
},
|
||||
"defaults": {
|
||||
"endpoint": {
|
||||
"label": "Endpoint Address",
|
||||
"placeholder": "Endpoint Address",
|
||||
"description": "The endpoint address that peers will connect to. (e.g. wg.example.com or wg.example.com:51820)"
|
||||
"label": "Endpunktadresse",
|
||||
"placeholder": "Endpunktadresse",
|
||||
"description": "Die Endpunktadresse, mit der sich Peers verbinden. (z.B. wg.example.com oder wg.example.com:51820)"
|
||||
},
|
||||
"networks": {
|
||||
"label": "IP Networks",
|
||||
"placeholder": "Network Addresses",
|
||||
"description": "Peers will get IP addresses from those subnets."
|
||||
"label": "IP-Netzwerke",
|
||||
"placeholder": "Netzwerkadressen",
|
||||
"description": "Peers erhalten IP-Adressen aus diesen Subnetzen."
|
||||
},
|
||||
"allowed-ip": {
|
||||
"label": "Allowed IP Addresses",
|
||||
"placeholder": "Default Allowed IP Addresses"
|
||||
"label": "Erlaubte IP-Adressen",
|
||||
"placeholder": "Erlaubte IP-Adressen für Peers"
|
||||
},
|
||||
"mtu": {
|
||||
"label": "MTU",
|
||||
"placeholder": "The client MTU (0 = keep default)"
|
||||
"placeholder": "Die Client-MTU (0 = Standard beibehalten)"
|
||||
},
|
||||
"keep-alive": {
|
||||
"label": "Keep Alive Interval",
|
||||
"placeholder": "Persistent Keepalive (0 = default)"
|
||||
"label": "Keepalive-Intervall",
|
||||
"placeholder": "Persistentes Keepalive (0 = Standard)"
|
||||
}
|
||||
},
|
||||
|
||||
"button-apply-defaults": "Apply Peer Defaults"
|
||||
"button-apply-defaults": "Peer-Standardeinstellungen anwenden"
|
||||
},
|
||||
"peer-view": {
|
||||
"headline-peer": "Peer:",
|
||||
"headline-endpoint": "Endpoint:",
|
||||
"section-info": "Peer Information",
|
||||
"section-status": "Current Status",
|
||||
"section-config": "Configuration",
|
||||
"identifier": "Identifier",
|
||||
"ip": "IP Addresses",
|
||||
"user": "Associated User",
|
||||
"notes": "Notes",
|
||||
"expiry-status": "Expires At",
|
||||
"disabled-status": "Disabled At",
|
||||
"traffic": "Traffic",
|
||||
"connection-status": "Connection Stats",
|
||||
"upload": "Uploaded Bytes (from Server to Peer)",
|
||||
"download": "Downloaded Bytes (from Peer to Server)",
|
||||
"pingable": "Is Pingable",
|
||||
"handshake": "Last Handshake",
|
||||
"connected-since": "Connected since",
|
||||
"endpoint": "Endpoint",
|
||||
"button-download": "Download configuration",
|
||||
"button-email": "Send configuration via E-Mail"
|
||||
"headline-endpoint": "Endpunkt:",
|
||||
"section-info": "Peer-Informationen",
|
||||
"section-status": "Aktueller Status",
|
||||
"section-config": "Konfiguration",
|
||||
"identifier": "Kennung",
|
||||
"ip": "IP-Adressen",
|
||||
"user": "Zugeordneter Benutzer",
|
||||
"notes": "Notizen",
|
||||
"expiry-status": "Läuft ab am",
|
||||
"disabled-status": "Deaktiviert am",
|
||||
"traffic": "Datenverkehr",
|
||||
"connection-status": "Verbindungsstatistiken",
|
||||
"upload": "Hochgeladene Bytes (vom Server zum Peer)",
|
||||
"download": "Heruntergeladene Bytes (vom Peer zum Server)",
|
||||
"pingable": "Pingbar",
|
||||
"handshake": "Letzter Handshake",
|
||||
"connected-since": "Verbunden seit",
|
||||
"endpoint": "Endpunkt",
|
||||
"button-download": "Konfiguration herunterladen",
|
||||
"button-email": "Konfiguration per E-Mail senden"
|
||||
},
|
||||
"peer-edit": {
|
||||
"headline-edit-peer": "Edit peer:",
|
||||
"headline-edit-endpoint": "Edit endpoint:",
|
||||
"headline-new-peer": "Create peer",
|
||||
"headline-new-endpoint": "Create endpoint",
|
||||
"header-general": "General",
|
||||
"header-network": "Network",
|
||||
"header-crypto": "Cryptography",
|
||||
"header-hooks": "Hooks (Executed on Peer)",
|
||||
"header-state": "State",
|
||||
"headline-edit-peer": "Peer bearbeiten:",
|
||||
"headline-edit-endpoint": "Endpunkt bearbeiten:",
|
||||
"headline-new-peer": "Peer erstellen",
|
||||
"headline-new-endpoint": "Endpunkt erstellen",
|
||||
"header-general": "Allgemein",
|
||||
"header-network": "Netzwerk",
|
||||
"header-crypto": "Kryptografie",
|
||||
"header-hooks": "Hooks (beim Peer ausgeführt)",
|
||||
"header-state": "Status",
|
||||
"display-name": {
|
||||
"label": "Display Name",
|
||||
"placeholder": "The descriptive name for the peer"
|
||||
"label": "Anzeigename",
|
||||
"placeholder": "Der beschreibende Name für den Peer"
|
||||
},
|
||||
"linked-user": {
|
||||
"label": "Linked User",
|
||||
"placeholder": "The user account which owns this peer"
|
||||
"label": "Verknüpfter Benutzer",
|
||||
"placeholder": "Das Benutzerkonto, dem dieser Peer gehört"
|
||||
},
|
||||
"private-key": {
|
||||
"label": "Private Key",
|
||||
"placeholder": "The private key"
|
||||
"label": "Privater Schlüssel",
|
||||
"placeholder": "Der private Schlüssel",
|
||||
"help": "Der private Schlüssel wird sicher auf dem Server gespeichert. Wenn der Benutzer bereits eine Kopie besitzt, kann dieses Feld entfallen. Der Server funktioniert auch ausschließlich mit dem öffentlichen Schlüssel des Peers."
|
||||
},
|
||||
"public-key": {
|
||||
"label": "Public Key",
|
||||
"placeholder": "The public key"
|
||||
"label": "Öffentlicher Schlüssel",
|
||||
"placeholder": "Der öffentliche Schlüssel"
|
||||
},
|
||||
"preshared-key": {
|
||||
"label": "Preshared Key",
|
||||
"placeholder": "Optional pre-shared key"
|
||||
"label": "Pre-Shared Key",
|
||||
"placeholder": "Optionaler geteilter Schlüssel"
|
||||
},
|
||||
"endpoint-public-key": {
|
||||
"label": "Endpoint public Key",
|
||||
"placeholder": "The public key of the remote endpoint"
|
||||
"label": "Öffentlicher Endpunktschlüssel",
|
||||
"placeholder": "Der öffentliche Schlüssel des entfernten Endpunkts"
|
||||
},
|
||||
"endpoint": {
|
||||
"label": "Endpoint Address",
|
||||
"placeholder": "The address of the remote endpoint"
|
||||
"label": "Endpunktadresse",
|
||||
"placeholder": "Die Adresse des entfernten Endpunkts"
|
||||
},
|
||||
"ip": {
|
||||
"label": "IP Addresses",
|
||||
"placeholder": "IP Addresses (CIDR format)"
|
||||
"label": "IP-Adressen",
|
||||
"placeholder": "IP-Adressen (CIDR-Format)"
|
||||
},
|
||||
"allowed-ip": {
|
||||
"label": "Allowed IP Addresses",
|
||||
"placeholder": "Allowed IP Addresses (CIDR format)"
|
||||
"label": "Erlaubte IP-Adressen",
|
||||
"placeholder": "Erlaubte IP-Adressen (CIDR-Format)"
|
||||
},
|
||||
"extra-allowed-ip": {
|
||||
"label": "Extra allowed IP Addresses",
|
||||
"placeholder": "Extra allowed IP's (Server Sided)",
|
||||
"description": "Those IP's will be added on the remote WireGuard interface as allowed IP's."
|
||||
"label": "Zusätzliche erlaubte IP-Adressen",
|
||||
"placeholder": "Zusätzliche erlaubte IP's (Server-seitig)",
|
||||
"description": "Diese IPs werden an der entfernten WireGuard-Schnittstelle als erlaubte IPs hinzugefügt."
|
||||
},
|
||||
"dns": {
|
||||
"label": "DNS Server",
|
||||
"placeholder": "The DNS servers that should be used"
|
||||
"label": "DNS-Server",
|
||||
"placeholder": "Die zu verwendenden DNS-Server"
|
||||
},
|
||||
"dns-search": {
|
||||
"label": "DNS Search Domains",
|
||||
"placeholder": "DNS search prefixes"
|
||||
"label": "DNS-Suchdomänen",
|
||||
"placeholder": "DNS-Suchpräfixe"
|
||||
},
|
||||
"keep-alive": {
|
||||
"label": "Keep Alive Interval",
|
||||
"placeholder": "Persistent Keepalive (0 = default)"
|
||||
"label": "Keepalive-Intervall",
|
||||
"placeholder": "Persistentes Keepalive (0 = Standard)"
|
||||
},
|
||||
"mtu": {
|
||||
"label": "MTU",
|
||||
"placeholder": "The client MTU (0 = keep default)"
|
||||
"placeholder": "Die Client-MTU (0 = Standard beibehalten)"
|
||||
},
|
||||
"pre-up": {
|
||||
"label": "Pre-Up",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||
},
|
||||
"post-up": {
|
||||
"label": "Post-Up",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||
},
|
||||
"pre-down": {
|
||||
"label": "Pre-Down",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||
},
|
||||
"post-down": {
|
||||
"label": "Post-Down",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||
},
|
||||
"disabled": {
|
||||
"label": "Peer Disabled"
|
||||
"label": "Peer deaktiviert"
|
||||
},
|
||||
"ignore-global": {
|
||||
"label": "Ignore global settings"
|
||||
"label": "Globale Einstellungen ignorieren"
|
||||
},
|
||||
"expires-at": {
|
||||
"label": "Expiry date"
|
||||
"label": "Ablaufdatum"
|
||||
}
|
||||
},
|
||||
"peer-multi-create": {
|
||||
"headline-peer": "Create multiple peers",
|
||||
"headline-endpoint": "Create multiple endpoints",
|
||||
"headline-peer": "Mehrere Peers erstellen",
|
||||
"headline-endpoint": "Mehrere Endpunkte erstellen",
|
||||
"identifiers": {
|
||||
"label": "User Identifiers",
|
||||
"placeholder": "User Identifiers",
|
||||
"description": "A user identifier (the username) for which a peer should be created."
|
||||
"label": "Benutzerkennungen",
|
||||
"placeholder": "Benutzerkennungen",
|
||||
"description": "Eine Benutzerkennung (der Benutzername), für die ein Peer erstellt werden soll."
|
||||
},
|
||||
"prefix": {
|
||||
"headline-peer": "Peer:",
|
||||
"headline-endpoint": "Endpoint:",
|
||||
"label": "Display Name Prefix",
|
||||
"placeholder": "The prefix",
|
||||
"description": "A prefix that is added to the peers display name."
|
||||
"headline-endpoint": "Endpunkt:",
|
||||
"label": "Anzeigename-Präfix",
|
||||
"placeholder": "Das Präfix",
|
||||
"description": "Ein Präfix, das dem Anzeigenamen des Peers hinzugefügt wird."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -40,7 +40,8 @@
|
||||
"settings": "Settings",
|
||||
"audit": "Audit Log",
|
||||
"login": "Login",
|
||||
"logout": "Logout"
|
||||
"logout": "Logout",
|
||||
"keygen": "Key Generator"
|
||||
},
|
||||
"home": {
|
||||
"headline": "WireGuard® VPN Portal",
|
||||
@@ -206,6 +207,25 @@
|
||||
"message": "Message"
|
||||
}
|
||||
},
|
||||
"keygen": {
|
||||
"headline": "WireGuard Key Generator",
|
||||
"abstract": "Generate a new WireGuard keys. The keys are generated in your local browser and are never sent to the server.",
|
||||
"headline-keypair": "New Key Pair",
|
||||
"headline-preshared-key": "New Preshared Key",
|
||||
"button-generate": "Generate",
|
||||
"private-key": {
|
||||
"label": "Private Key",
|
||||
"placeholder": "The private key"
|
||||
},
|
||||
"public-key": {
|
||||
"label": "Public Key",
|
||||
"placeholder": "The public key"
|
||||
},
|
||||
"preshared-key": {
|
||||
"label": "Preshared Key",
|
||||
"placeholder": "The pre-shared key"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"user-view": {
|
||||
"headline": "User Account:",
|
||||
@@ -439,7 +459,8 @@
|
||||
},
|
||||
"private-key": {
|
||||
"label": "Private Key",
|
||||
"placeholder": "The private key"
|
||||
"placeholder": "The private key",
|
||||
"help": "The private key is stored securely on the server. If the user already holds a copy, you may omit this field. The server still functions exclusively with the peer’s public key."
|
||||
},
|
||||
"public-key": {
|
||||
"label": "Public Key",
|
||||
|
@@ -64,6 +64,14 @@ const router = createRouter({
|
||||
// this generates a separate chunk (About.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('../views/AuditView.vue')
|
||||
},
|
||||
{
|
||||
path: '/key-generator',
|
||||
name: 'key-generator',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (About.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('../views/KeyGeneraterView.vue')
|
||||
}
|
||||
],
|
||||
linkActiveClass: "active",
|
||||
@@ -114,11 +122,11 @@ router.beforeEach(async (to) => {
|
||||
}
|
||||
|
||||
// redirect to login page if not logged in and trying to access a restricted page
|
||||
const publicPages = ['/', '/login']
|
||||
const publicPages = ['/', '/login', '/key-generator']
|
||||
const authRequired = !publicPages.includes(to.path)
|
||||
|
||||
if (authRequired && !auth.IsAuthenticated) {
|
||||
auth.SetReturnUrl(to.fullPath) // store original destination before starting the auth process
|
||||
auth.SetReturnUrl(to.fullPath) // store the original destination before starting the auth process
|
||||
return '/login'
|
||||
}
|
||||
})
|
||||
|
@@ -81,7 +81,7 @@ onMounted(async () => {
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
|
||||
<div class="col-sm-6">
|
||||
<select v-model.number="audit.pageSize" class="form-select" @click="audit.afterPageSizeChange()">
|
||||
<select id="paginationSelector" v-model.number="audit.pageSize" class="form-select" @click="audit.afterPageSizeChange()">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
|
@@ -416,7 +416,7 @@ onMounted(async () => {
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
|
||||
<div class="col-sm-6">
|
||||
<select v-model.number="peers.pageSize" class="form-select" @click="peers.afterPageSizeChange()">
|
||||
<select id="paginationSelector" v-model.number="peers.pageSize" class="form-select" @click="peers.afterPageSizeChange()">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
|
147
frontend/src/views/KeyGeneraterView.vue
Normal file
147
frontend/src/views/KeyGeneraterView.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<script setup>
|
||||
|
||||
import {ref} from "vue";
|
||||
|
||||
const privateKey = ref("")
|
||||
const publicKey = ref("")
|
||||
const presharedKey = ref("")
|
||||
|
||||
/**
|
||||
* Generate an X25519 keypair using the Web Crypto API and return Base64-encoded strings.
|
||||
* @async
|
||||
* @function generateKeypair
|
||||
* @returns {Promise<{ publicKey: string, privateKey: string }>} Resolves with an object containing
|
||||
* - publicKey: the Base64-encoded public key
|
||||
* - privateKey: the Base64-encoded private key
|
||||
*/
|
||||
async function generateKeypair() {
|
||||
// 1. Generate an X25519 key pair
|
||||
const keyPair = await crypto.subtle.generateKey(
|
||||
{ name: 'X25519', namedCurve: 'X25519' },
|
||||
true, // extractable
|
||||
['deriveBits'] // allowed usage for ECDH
|
||||
);
|
||||
|
||||
// 2. Export keys as JWK to access raw key material
|
||||
const pubJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey);
|
||||
const privJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey);
|
||||
|
||||
// 3. Convert Base64URL to standard Base64 with padding
|
||||
return {
|
||||
publicKey: b64urlToB64(pubJwk.x),
|
||||
privateKey: b64urlToB64(privJwk.d)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a 32-byte pre-shared key using crypto.getRandomValues.
|
||||
* @function generatePresharedKey
|
||||
* @returns {Uint8Array} A Uint8Array of length 32 with random bytes.
|
||||
*/
|
||||
function generatePresharedKey() {
|
||||
let privateKey = new Uint8Array(32);
|
||||
window.crypto.getRandomValues(privateKey);
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Base64URL-encoded string to standard Base64 with padding.
|
||||
* @function b64urlToB64
|
||||
* @param {string} input - The Base64URL string.
|
||||
* @returns {string} The padded, standard Base64 string.
|
||||
*/
|
||||
function b64urlToB64(input) {
|
||||
let b64 = input.replace(/-/g, '+').replace(/_/g, '/');
|
||||
while (b64.length % 4) {
|
||||
b64 += '=';
|
||||
}
|
||||
return b64;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an ArrayBuffer or TypedArray buffer to a Base64-encoded string.
|
||||
* @function arrayBufferToBase64
|
||||
* @param {ArrayBuffer|Uint8Array} buffer - The buffer to convert.
|
||||
* @returns {string} Base64-encoded representation of the buffer.
|
||||
*/
|
||||
function arrayBufferToBase64(buffer) {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
let binary = '';
|
||||
for (let i = 0; i < bytes.byteLength; ++i) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
// Window.btoa handles binary → Base64
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new keypair and update the corresponding Vue refs.
|
||||
* @async
|
||||
* @function generateNewKeyPair
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function generateNewKeyPair() {
|
||||
const keypair = await generateKeypair();
|
||||
|
||||
privateKey.value = keypair.privateKey;
|
||||
publicKey.value = keypair.publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new pre-shared key and update the Vue ref.
|
||||
* @function generateNewPresharedKey
|
||||
*/
|
||||
function generateNewPresharedKey() {
|
||||
const rawPsk = generatePresharedKey();
|
||||
presharedKey.value = arrayBufferToBase64(rawPsk);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-header">
|
||||
<h1>{{ $t('keygen.headline') }}</h1>
|
||||
</div>
|
||||
|
||||
<p class="lead">{{ $t('keygen.abstract') }}</p>
|
||||
|
||||
<div class="mt-4 row">
|
||||
<div class="col-12 col-lg-5">
|
||||
<h1>{{ $t('keygen.headline-keypair') }}</h1>
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('keygen.private-key.label') }}</label>
|
||||
<input class="form-control" v-model="privateKey" :placeholder="$t('keygen.private-key.placeholder')" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('keygen.public-key.label') }}</label>
|
||||
<input class="form-control" v-model="publicKey" :placeholder="$t('keygen.private-key.placeholder')" readonly>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<hr class="mt-4">
|
||||
<button class="btn btn-primary mb-4" type="button" @click.prevent="generateNewKeyPair">{{ $t('keygen.button-generate') }}</button>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="col-12 col-lg-2 mt-sm-4">
|
||||
</div>
|
||||
<div class="col-12 col-lg-5">
|
||||
<h1>{{ $t('keygen.headline-preshared-key') }}</h1>
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('keygen.preshared-key.label') }}</label>
|
||||
<input class="form-control" v-model="presharedKey" :placeholder="$t('keygen.preshared-key.placeholder')" readonly>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<hr class="mt-4">
|
||||
<button class="btn btn-primary mb-4" type="button" @click.prevent="generateNewPresharedKey">{{ $t('keygen.button-generate') }}</button>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@@ -178,7 +178,7 @@ onMounted(async () => {
|
||||
{{ $t('general.pagination.size')}}:
|
||||
</label>
|
||||
<div class="col-sm-6">
|
||||
<select v-model.number="profile.pageSize" class="form-select" @click="profile.afterPageSizeChange()">
|
||||
<select id="paginationSelector" v-model.number="profile.pageSize" class="form-select" @click="profile.afterPageSizeChange()">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
|
@@ -116,7 +116,7 @@ onMounted(() => {
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
|
||||
<div class="col-sm-6">
|
||||
<select v-model.number="users.pageSize" class="form-select" @click="users.afterPageSizeChange()">
|
||||
<select id="paginationSelector" v-model.number="users.pageSize" class="form-select" @click="users.afterPageSizeChange()">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
|
203
internal/app/gorm_encryption.go
Normal file
203
internal/app/gorm_encryption.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm/schema"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
// GormEncryptedStringSerializer is a GORM serializer that encrypts and decrypts string values using AES256.
|
||||
// It is used to store sensitive information in the database securely.
|
||||
// If the serializer encounters a value that is not a string, it will return an error.
|
||||
type GormEncryptedStringSerializer struct {
|
||||
useEncryption bool
|
||||
keyPhrase string
|
||||
prefix string
|
||||
}
|
||||
|
||||
// NewGormEncryptedStringSerializer creates a new GormEncryptedStringSerializer.
|
||||
// It needs to be registered with GORM to be used:
|
||||
// schema.RegisterSerializer("encstr", gormEncryptedStringSerializerInstance)
|
||||
// You can then use it in your model like this:
|
||||
//
|
||||
// EncryptedField string `gorm:"serializer:encstr"`
|
||||
func NewGormEncryptedStringSerializer(keyPhrase string) GormEncryptedStringSerializer {
|
||||
return GormEncryptedStringSerializer{
|
||||
useEncryption: keyPhrase != "",
|
||||
keyPhrase: keyPhrase,
|
||||
prefix: "WG_ENC_",
|
||||
}
|
||||
}
|
||||
|
||||
// Scan implements the GORM serializer interface. It decrypts the value after reading it from the database.
|
||||
func (s GormEncryptedStringSerializer) Scan(
|
||||
ctx context.Context,
|
||||
field *schema.Field,
|
||||
dst reflect.Value,
|
||||
dbValue any,
|
||||
) (err error) {
|
||||
var dbStringValue string
|
||||
if dbValue != nil {
|
||||
switch v := dbValue.(type) {
|
||||
case []byte:
|
||||
dbStringValue = string(v)
|
||||
case string:
|
||||
dbStringValue = v
|
||||
default:
|
||||
return fmt.Errorf("unsupported type %T for encrypted field %s", dbValue, field.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if !s.useEncryption {
|
||||
field.ReflectValueOf(ctx, dst).SetString(dbStringValue) // keep the original value
|
||||
return nil
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(dbStringValue, s.prefix) {
|
||||
field.ReflectValueOf(ctx, dst).SetString(dbStringValue) // keep the original value
|
||||
return nil
|
||||
}
|
||||
|
||||
encryptedString := strings.TrimPrefix(dbStringValue, s.prefix)
|
||||
decryptedString, err := DecryptAES256(encryptedString, s.keyPhrase)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decrypt value for field %s: %w", field.Name, err)
|
||||
}
|
||||
|
||||
field.ReflectValueOf(ctx, dst).SetString(decryptedString)
|
||||
return
|
||||
}
|
||||
|
||||
// Value implements the GORM serializer interface. It encrypts the value before storing it in the database.
|
||||
func (s GormEncryptedStringSerializer) Value(
|
||||
_ context.Context,
|
||||
_ *schema.Field,
|
||||
_ reflect.Value,
|
||||
fieldValue any,
|
||||
) (any, error) {
|
||||
if fieldValue == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch v := fieldValue.(type) {
|
||||
case string:
|
||||
if v == "" {
|
||||
return "", nil // empty string, no need to encrypt
|
||||
}
|
||||
if !s.useEncryption {
|
||||
return v, nil // keep the original value
|
||||
}
|
||||
encryptedString, err := EncryptAES256(v, s.keyPhrase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.prefix + encryptedString, nil
|
||||
case domain.PreSharedKey:
|
||||
if v == "" {
|
||||
return "", nil // empty string, no need to encrypt
|
||||
}
|
||||
if !s.useEncryption {
|
||||
return string(v), nil // keep the original value
|
||||
}
|
||||
encryptedString, err := EncryptAES256(string(v), s.keyPhrase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.prefix + encryptedString, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("encryption only supports string values, got %T", fieldValue)
|
||||
}
|
||||
}
|
||||
|
||||
// EncryptAES256 encrypts the given plaintext with the given key using AES256 in CBC mode with PKCS7 padding
|
||||
func EncryptAES256(plaintext, key string) (string, error) {
|
||||
if len(plaintext) == 0 {
|
||||
return "", fmt.Errorf("plaintext must not be empty")
|
||||
}
|
||||
if len(key) == 0 {
|
||||
return "", fmt.Errorf("key must not be empty")
|
||||
}
|
||||
key = trimEncKey(key)
|
||||
iv := key[:aes.BlockSize]
|
||||
|
||||
block, err := aes.NewCipher([]byte(key))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
plain := []byte(plaintext)
|
||||
plain = pkcs7Padding(plain, aes.BlockSize)
|
||||
|
||||
ciphertext := make([]byte, len(plain))
|
||||
|
||||
mode := cipher.NewCBCEncrypter(block, []byte(iv))
|
||||
mode.CryptBlocks(ciphertext, plain)
|
||||
|
||||
b64String := base64.StdEncoding.EncodeToString(ciphertext)
|
||||
|
||||
return b64String, nil
|
||||
}
|
||||
|
||||
// DecryptAES256 decrypts the given ciphertext with the given key using AES256 in CBC mode with PKCS7 padding
|
||||
func DecryptAES256(encrypted, key string) (string, error) {
|
||||
if len(encrypted) == 0 {
|
||||
return "", fmt.Errorf("ciphertext must not be empty")
|
||||
}
|
||||
if len(key) == 0 {
|
||||
return "", fmt.Errorf("key must not be empty")
|
||||
}
|
||||
key = trimEncKey(key)
|
||||
iv := key[:aes.BlockSize]
|
||||
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(encrypted)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(ciphertext)%aes.BlockSize != 0 {
|
||||
return "", fmt.Errorf("invalid ciphertext length, must be a multiple of %d", aes.BlockSize)
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher([]byte(key))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, []byte(iv))
|
||||
mode.CryptBlocks(ciphertext, ciphertext)
|
||||
|
||||
ciphertext = pkcs7UnPadding(ciphertext)
|
||||
|
||||
return string(ciphertext), nil
|
||||
}
|
||||
|
||||
func pkcs7Padding(ciphertext []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(ciphertext)%blockSize
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(ciphertext, padtext...)
|
||||
}
|
||||
|
||||
func pkcs7UnPadding(src []byte) []byte {
|
||||
length := len(src)
|
||||
unpadding := int(src[length-1])
|
||||
return src[:(length - unpadding)]
|
||||
}
|
||||
|
||||
func trimEncKey(key string) string {
|
||||
if len(key) > 32 {
|
||||
return key[:32]
|
||||
}
|
||||
|
||||
if len(key) < 32 {
|
||||
key = key + strings.Repeat("0", 32-len(key))
|
||||
}
|
||||
return key
|
||||
}
|
@@ -174,9 +174,16 @@ func GetConfig() (*Config, error) {
|
||||
|
||||
// override config values from YAML file
|
||||
|
||||
cfgFileName := "config/config.yml"
|
||||
cfgFileName := "config/config.yaml"
|
||||
cfgFileNameFallback := "config/config.yml"
|
||||
if envCfgFileName := os.Getenv("WG_PORTAL_CONFIG"); envCfgFileName != "" {
|
||||
cfgFileName = envCfgFileName
|
||||
cfgFileNameFallback = envCfgFileName
|
||||
}
|
||||
|
||||
// check if the config file exists, otherwise use the fallback file name
|
||||
if _, err := os.Stat(cfgFileName); os.IsNotExist(err) {
|
||||
cfgFileName = cfgFileNameFallback
|
||||
}
|
||||
|
||||
if err := loadConfigFile(cfg, cfgFileName); err != nil {
|
||||
|
@@ -18,11 +18,14 @@ type DatabaseConfig struct {
|
||||
// Debug enables logging of all database statements
|
||||
Debug bool `yaml:"debug"`
|
||||
// SlowQueryThreshold enables logging of slow queries which take longer than the specified duration
|
||||
SlowQueryThreshold time.Duration `yaml:"slow_query_threshold"` // 0 means no logging of slow queries
|
||||
SlowQueryThreshold time.Duration `yaml:"slow_query_threshold"` // "0" means no logging of slow queries
|
||||
// Type is the database type. Supported: mysql, mssql, postgres, sqlite
|
||||
Type SupportedDatabase `yaml:"type"`
|
||||
// DSN is the database connection string.
|
||||
// For SQLite, it is the path to the database file.
|
||||
// For other databases, it is the connection string, see: https://gorm.io/docs/connecting_to_the_database.html
|
||||
DSN string `yaml:"dsn"`
|
||||
// EncryptionPassphrase is the passphrase used to encrypt sensitive data (WireGuard keys) in the database.
|
||||
// If no passphrase is provided, no encryption will be used.
|
||||
EncryptionPassphrase string `yaml:"encryption_passphrase"`
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
type KeyPair struct {
|
||||
PrivateKey string
|
||||
PrivateKey string `gorm:"serializer:encstr"`
|
||||
PublicKey string
|
||||
}
|
||||
|
||||
|
@@ -7,9 +7,9 @@ import (
|
||||
"time"
|
||||
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
)
|
||||
|
||||
type PeerIdentifier string
|
||||
@@ -36,7 +36,7 @@ type Peer struct {
|
||||
EndpointPublicKey ConfigOption[string] `gorm:"embedded;embeddedPrefix:endpoint_pubkey_"` // the endpoint public key
|
||||
AllowedIPsStr ConfigOption[string] `gorm:"embedded;embeddedPrefix:allowed_ips_str_"` // all allowed ip subnets, comma seperated
|
||||
ExtraAllowedIPsStr string // all allowed ip subnets on the server side, comma seperated
|
||||
PresharedKey PreSharedKey // the pre-shared Key of the peer
|
||||
PresharedKey PreSharedKey `gorm:"serializer:encstr"` // the pre-shared Key of the peer
|
||||
PersistentKeepalive ConfigOption[int] `gorm:"embedded;embeddedPrefix:persistent_keep_alive_"` // the persistent keep-alive interval
|
||||
|
||||
// WG Portal specific
|
||||
|
10
mkdocs.yml
10
mkdocs.yml
@@ -30,6 +30,15 @@ plugins:
|
||||
- minify:
|
||||
minify_html: true
|
||||
- swagger-ui-tag
|
||||
- mike:
|
||||
# These fields are all optional; the defaults are as below...
|
||||
alias_type: symlink
|
||||
redirect_template: null
|
||||
deploy_prefix: ''
|
||||
canonical_version: null
|
||||
version_selector: true
|
||||
css_dir: css
|
||||
javascript_dir: js
|
||||
|
||||
extra:
|
||||
version:
|
||||
@@ -65,6 +74,7 @@ nav:
|
||||
- Docker: documentation/getting-started/docker.md
|
||||
- Helm: documentation/getting-started/helm.md
|
||||
- Sources: documentation/getting-started/sources.md
|
||||
- Reverse Proxy (HTTPS): documentation/getting-started/reverse-proxy.md
|
||||
- Configuration:
|
||||
- Overview: documentation/configuration/overview.md
|
||||
- Examples: documentation/configuration/examples.md
|
||||
|
Reference in New Issue
Block a user