Compare commits

..

30 Commits

Author SHA1 Message Date
Christoph Haas
4ccc59c109 Merge branch 'master' into stable
# Conflicts:
#	.github/workflows/docker-publish.yml
#	README.md
#	assets/tpl/admin_index.html
#	assets/tpl/user_index.html
#	cmd/wg-portal/main.go
#	docker-compose.yml
#	go.mod
#	go.sum
#	internal/common/util.go
#	internal/server/docs/docs.go
#	internal/server/handlers_common.go
#	internal/server/server.go
#	internal/wireguard/peermanager.go
2025-05-04 20:38:55 +02:00
Christoph Haas
8271dd7c1f Merge branch 'prepare V2 release' 2025-05-04 20:19:19 +02:00
Christoph Haas
4ca37089bc fix browser warnings, update vite 2025-05-04 20:14:40 +02:00
Christoph Haas
8e5d5138c0 fix bootswatch 5.3.5 issue 2025-05-04 20:05:38 +02:00
Christoph Haas
c73286e11a improve german translations 2025-05-04 19:44:25 +02:00
Christoph Haas
b4aa6f8ef3 fix gorm error if no encryption is used (#427) 2025-05-04 17:42:13 +02:00
Christoph Haas
432c627f9b further improve documentation and examples (#423) 2025-05-04 14:48:34 +02:00
Christoph Haas
cd60761ea7 improve docs 2025-05-04 11:16:46 +02:00
Christoph Haas
2c8304417b prepare for v2 release 2025-05-04 11:00:12 +02:00
Christoph Haas
020ebb64e7 docs: add another listening-address example 2025-05-04 09:26:56 +02:00
Christoph Haas
923d4a6188 docs: add reverse-proxy example, improve docker examples, fix slow_query_threshold documentation; feat: allow config.yml and config.yaml as configuration files 2025-05-03 22:21:56 +02:00
Dominik Lakatoš
2b46dca770 generating WG keypair in browser using Web Crypto API (#422) 2025-05-03 07:58:41 +02:00
Christoph Haas
b9c4ca04f5 allow to encrypt keys in db, add browser-only key generator, add hints that private keys are stored on the server (#420) 2025-05-02 18:48:35 +02:00
Christoph Haas
dddf0c475b build v2 tags for release-candidate versions 2025-05-02 10:51:28 +02:00
Christoph Haas
fe60a5ab9b update documentation for Docker usage (#419) 2025-05-02 10:42:33 +02:00
Christoph Haas
e176e07f7d update documentation for Docker usage (#419), include wireguard-tools in Docker image 2025-05-02 10:29:04 +02:00
Christoph Haas
b06c03ef8e fix missing error check (#419) 2025-05-01 19:12:19 +02:00
Christoph Haas
6b0b78d749 docs: add note about running wireguard in Docker (#156) 2025-04-30 22:42:04 +02:00
Vladimir Dombrovski
62f3c8d4a1 Implement EditableKeys parameter (#417)
Signed-off-by: Vladimir DOMBROVSKI <vladimir.dombrovski@bso.co>
2025-04-30 22:05:40 +02:00
acc0mplish
fbcb22198c Added Korean translations (#414) 2025-04-24 14:54:45 +02:00
Rafael Alexandre
2c443a4a9b add portuguese translations (#412)
Signed-off-by: Rafael Alexandre <r.alexandre99@gmail.com>
2025-04-22 22:44:05 +02:00
Christoph
059234d416 never publish pointer payloads on message bus (#411) 2025-04-21 16:42:35 +02:00
Christoph
e2966d32ea fix user creation (#411) 2025-04-21 15:29:53 +02:00
onyx-flame
e6b01a9903 Feature (v1): add latest handshake data to API response (#203)
* feature: updated handshake-related fields type

* feature: updated handshake representations in templates

* feature: added handshake field to Swagger schema
2023-12-23 12:56:52 +01:00
Christoph Haas
2f79dd04c0 adopt github actions 2023-10-26 11:29:34 +02:00
Christoph Haas
e5ed9736b3 update docker build settings, move to new docker hub repository, use stable branch and major version tags 2023-10-26 11:22:58 +02:00
Christoph Haas
c8353b85ae Merge branch 'replace_ext_lib' into stable 2023-10-26 10:40:06 +02:00
Christoph Haas
6142031387 update gin 2023-10-26 10:39:01 +02:00
Christoph Haas
dd86d0ff49 replace inaccessible external lib 2023-10-26 10:31:29 +02:00
Christoph Haas
bdd426a679 populate peer device type (#170) 2023-10-26 10:20:08 +02:00
46 changed files with 1765 additions and 335 deletions

View File

@@ -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*.*.*"]
@@ -64,10 +64,10 @@ jobs:
# major and major.minor tags are not available for alpha or beta releases
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
# add v{{major}} tag, even for beta releases
type=match,pattern=(v\d),group=1,enable=${{ contains(github.ref, 'beta') }}
# add {{major}} tag, even for beta releases
type=match,pattern=v(\d),group=1,enable=${{ contains(github.ref, 'beta') }}
# add v{{major}} tag, even for beta or release-canidate releases
type=match,pattern=(v\d),group=1,enable=${{ contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
# add {{major}} tag, even for beta releases or release-canidate releases
type=match,pattern=v(\d),group=1,enable=${{ contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
# set latest tag for default branch
type=raw,value=latest,enable={{is_default_branch}}

View File

@@ -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
View File

@@ -33,6 +33,7 @@ ssh.key
wg_portal.db
sqlite.db
/config.yml
/config.yaml
/config/
venv/
.cache/

View File

@@ -52,7 +52,7 @@ COPY --from=builder /build/dist/wg-portal /
######
FROM alpine:3.19
# Install OS-level dependencies
RUN apk add --no-cache bash curl iptables nftables openresolv
RUN apk add --no-cache bash curl iptables nftables openresolv wireguard-tools
# Setup timezone
ENV TZ=UTC
# Copy binaries

View File

@@ -1,4 +1,4 @@
# WireGuard Portal (v2 - testing)
# WireGuard Portal v2
[![Build Status](https://github.com/h44z/wg-portal/actions/workflows/docker-publish.yml/badge.svg?event=push)](https://github.com/h44z/wg-portal/actions/workflows/docker-publish.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://opensource.org/licenses/MIT)
@@ -8,14 +8,6 @@
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/h44z/wg-portal)
[![Docker Pulls](https://img.shields.io/docker/pulls/h44z/wg-portal.svg)](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 -->
![Screenshot](docs/assets/images/screenshot.png)
@@ -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**.

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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 its 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.

View 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) artifacs also.

View File

@@ -1,35 +1,130 @@
## Image Usage
The preferred way to start WireGuard Portal as Docker container is to use Docker Compose.
The WireGuard Portal Docker image is available on both [Docker Hub](https://hub.docker.com/r/wgportal/wg-portal) and [GitHub Container Registry](https://github.com/h44z/wg-portal/pkgs/container/wg-portal).
It is built on the official Alpine Linux base image and comes pre-packaged with all necessary WireGuard dependencies.
A sample docker-compose.yml:
This container allows you to establish WireGuard VPN connections without relying on a host system that supports WireGuard or using the `linuxserver/wireguard` Docker image.
The recommended method for deploying WireGuard Portal is via Docker Compose for ease of configuration and management.
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.
## WireGuard Interface Handling
WireGuard Portal supports managing WireGuard interfaces through three distinct deployment methods, providing flexibility based on your system architecture and operational preferences:
- **Directly on the host system**:
WireGuard Portal can control WireGuard interfaces natively on the host, without using containers.
This setup is ideal for environments where direct access to system networking is preferred.
To use this method, you need to set the network mode to `host` in your docker-compose.yml file.
```yaml
services:
wg-portal:
...
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.
This is the recommended approach when running WireGuard Portal via Docker, as it encapsulates all functionality in a single, portable container without requiring a separate WireGuard host or image.
```yaml
services:
wg-portal:
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
- "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
```
- **Via a separate Docker container**:
WireGuard Portal can interface with and control WireGuard running in another Docker container, such as the [linuxserver/wireguard](https://docs.linuxserver.io/images/docker-wireguard/) image.
This method is useful in setups that already use `linuxserver/wireguard` or where you want to isolate the VPN backend from the portal frontend.
For this, you need to set the network mode to `service:wireguard` in your docker-compose.yml file, `wireguard` is the service name of your WireGuard container.
```yaml
services:
wg-portal:
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
wireguard:
image: lscr.io/linuxserver/wireguard:latest
container_name: wireguard
restart: unless-stopped
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:
- ./wg/etc:/config/wg_confs # We share the configuration (wgx.conf) between wg-portal and wireguard
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
```
As the `linuxserver/wireguard` image uses _wg-quick_ to manage the interfaces, you need to have at least the following configuration set for WireGuard Portal:
```yaml
core:
# The WireGuard container uses wg-quick to manage the WireGuard interfaces - this conflicts with WireGuard Portal during startup.
# To avoid this, we need to set the restore_state option to false so that wg-quick can create the interfaces.
restore_state: false
# Usually, there are no existing interfaces in the WireGuard container, so we can set this to false.
import_existing: false
advanced:
# WireGuard Portal needs to export the WireGuard configuration as wg-quick config files so that the WireGuard container can use them.
config_storage_path: /etc/wireguard/
```
## Image Versioning
All images are hosted on Docker Hub at [https://hub.docker.com/r/wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal).
All images are hosted on Docker Hub at [https://hub.docker.com/r/wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal) or in the [GitHub Container Registry](https://github.com/h44z/wg-portal/pkgs/container/wg-portal).
There are three types of tags in the repository:
#### Semantic versioned tags
For example, `1.0.19`.
For example, `2.0.0-rc.1` or `v2.0.0-rc.1`.
These are official releases of WireGuard Portal. They correspond to the GitHub tags that we make, and you can see the release notes for them here: [https://github.com/h44z/wg-portal/releases](https://github.com/h44z/wg-portal/releases).
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
@@ -43,15 +138,22 @@ 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`.
You can configure WireGuard Portal using a YAML configuration file.
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 a SQLite database. The database is stored in `/app/data/sqlite.db`.
By default, WireGuard Portal uses an SQLite database. The database is stored in `/app/data/sqlite.db`.
You should mount those directories as a volume:
- /app/data
- /app/config
- `/app/data`
- `/app/config`
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 inside the container to a location of your choice.
Also enable the `config_storage_path` option in the configuration file:
```yaml
advanced:
config_storage_path: /etc/wireguard
```

View 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 frontend 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.

View File

@@ -21,4 +21,6 @@ make build
## Install
Compiled binary will be available in `./dist` directory.
Compiled binary will be available in `./dist` directory.
For installation instructions, check the [Binaries](./binaries.md) section.

View File

@@ -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

View File

@@ -1 +1 @@
--8<-- "README.md:20:47"
--8<-- "README.md:12:41"

View File

@@ -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 in beta and not feature complete.
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"]
```

View File

@@ -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"

View File

@@ -29,6 +29,6 @@
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.3",
"sass-embedded": "^1.86.3",
"vite": "6.3.2"
"vite": "6.3.4"
}
}

View File

@@ -48,8 +48,11 @@ const languageFlag = computed(() => {
}
const langMap = {
en: "us",
pt: "pt",
uk: "ua",
zh: "cn",
ko: "kr",
};
return "fi-" + (langMap[lang] || lang);
})
@@ -82,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">
@@ -121,10 +127,13 @@ const currentYear = ref(new Date().getFullYear())
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('de')"><span class="fi fi-de"></span> Deutsch</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('en')"><span class="fi fi-us"></span> English</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('fr')"><span class="fi fi-fr"></span> Français</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('ko')"><span class="fi fi-kr"></span> 한국어</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('pt')"><span class="fi fi-pt"></span> Português</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('ru')"><span class="fi fi-ru"></span> Русский</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('uk')"><span class="fi fi-ua"></span> Українська</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('vi')"><span class="fi fi-vi"></span> Tiếng Việt</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('zh')"><span class="fi fi-cn"></span> 中文</a>
</div>
</div>
</div>

View File

@@ -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
/*

View File

@@ -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>

View File

@@ -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'">

View File

@@ -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>

View File

@@ -2,10 +2,13 @@
import de from './translations/de.json';
import en from './translations/en.json';
import fr from './translations/fr.json';
import ko from './translations/ko.json';
import pt from './translations/pt.json';
import ru from './translations/ru.json';
import uk from './translations/uk.json';
import vi from './translations/vi.json';
import zh from './translations/zh.json';
import {createI18n} from "vue-i18n";
// Create i18n instance with options
@@ -23,6 +26,8 @@ const i18n = createI18n({
"de": de,
"en": en,
"fr": fr,
"ko": ko,
"pt": pt,
"ru": ru,
"uk": uk,
"vi": vi,

View File

@@ -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."
}
}
}

View File

@@ -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 peers public key."
},
"public-key": {
"label": "Public Key",

View File

@@ -0,0 +1,532 @@
{
"languages": {
"ko": "한국어"
},
"general": {
"pagination": {
"size": "항목 수",
"all": "전체 (느림)"
},
"search": {
"placeholder": "검색...",
"button": "검색"
},
"select-all": "모두 선택",
"yes": "예",
"no": "아니오",
"cancel": "취소",
"close": "닫기",
"save": "저장",
"delete": "삭제"
},
"login": {
"headline": "로그인하세요",
"username": {
"label": "사용자 이름",
"placeholder": "사용자 이름을 입력하세요"
},
"password": {
"label": "비밀번호",
"placeholder": "비밀번호를 입력하세요"
},
"button": "로그인"
},
"menu": {
"home": "홈",
"interfaces": "인터페이스",
"users": "사용자",
"lang": "언어 변경",
"profile": "내 프로필",
"settings": "설정",
"audit": "감사 로그",
"login": "로그인",
"logout": "로그아웃"
},
"home": {
"headline": "WireGuard® VPN 포털",
"info-headline": "추가 정보",
"abstract": "WireGuard®는 암호화 기술을 활용하는 매우 간단하면서도 빠르고 현대적인 VPN입니다. IPsec보다 빠르고, 간단하며, 가볍고, 더 유용하면서도 엄청난 골칫거리를 피하는 것을 목표로 합니다. OpenVPN보다 훨씬 더 성능이 뛰어날 것으로 예상됩니다.",
"installation": {
"box-header": "WireGuard 설치",
"headline": "설치",
"content": "클라이언트 소프트웨어 설치 지침은 공식 WireGuard 웹사이트에서 찾을 수 있습니다.",
"button": "지침 열기"
},
"about-wg": {
"box-header": "WireGuard 정보",
"headline": "정보",
"content": "WireGuard®는 암호화 기술을 활용하는 매우 간단하면서도 빠르고 현대적인 VPN입니다.",
"button": "더 보기"
},
"about-portal": {
"box-header": "WireGuard 포털 정보",
"headline": "WireGuard 포털",
"content": "WireGuard 포털은 WireGuard를 위한 간단한 웹 기반 구성 포털입니다.",
"button": "더 보기"
},
"profiles": {
"headline": "VPN 프로필",
"abstract": "사용자 프로필을 통해 개인 VPN 구성에 액세스하고 다운로드할 수 있습니다.",
"content": "구성된 모든 프로필을 찾으려면 아래 버튼을 클릭하세요.",
"button": "내 프로필 열기"
},
"admin": {
"headline": "관리 영역",
"abstract": "관리 영역에서는 WireGuard 피어 및 서버 인터페이스뿐만 아니라 WireGuard 포털에 로그인할 수 있는 사용자도 관리할 수 있습니다.",
"content": "",
"button-admin": "서버 관리 열기",
"button-user": "사용자 관리 열기"
}
},
"interfaces": {
"headline": "인터페이스 관리",
"headline-peers": "현재 VPN 피어",
"headline-endpoints": "현재 엔드포인트",
"no-interface": {
"default-selection": "사용 가능한 인터페이스 없음",
"headline": "인터페이스를 찾을 수 없습니다...",
"abstract": "새 WireGuard 인터페이스를 만들려면 위의 플러스 버튼을 클릭하세요."
},
"no-peer": {
"headline": "사용 가능한 피어 없음",
"abstract": "현재 선택한 WireGuard 인터페이스에 사용 가능한 피어가 없습니다."
},
"table-heading": {
"name": "이름",
"user": "사용자",
"ip": "IP 주소",
"endpoint": "엔드포인트",
"status": "상태"
},
"interface": {
"headline": "인터페이스 상태:",
"mode": "모드",
"key": "공개 키",
"endpoint": "공개 엔드포인트",
"port": "수신 포트",
"peers": "활성화된 피어",
"total-peers": "총 피어 수",
"endpoints": "활성화된 엔드포인트",
"total-endpoints": "총 엔드포인트 수",
"ip": "IP 주소",
"default-allowed-ip": "기본 허용 IP",
"dns": "DNS 서버",
"mtu": "MTU",
"default-keep-alive": "기본 Keepalive 간격",
"button-show-config": "구성 보기",
"button-download-config": "구성 다운로드",
"button-store-config": "wg-quick용 구성 저장",
"button-edit": "인터페이스 편집"
},
"button-add-interface": "인터페이스 추가",
"button-add-peer": "피어 추가",
"button-add-peers": "여러 피어 추가",
"button-show-peer": "피어 보기",
"button-edit-peer": "피어 편집",
"peer-disabled": "피어가 비활성화됨, 이유:",
"peer-expiring": "피어 만료 예정:",
"peer-connected": "연결됨",
"peer-not-connected": "연결되지 않음",
"peer-handshake": "마지막 핸드셰이크:"
},
"users": {
"headline": "사용자 관리",
"table-heading": {
"id": "ID",
"email": "이메일",
"firstname": "이름",
"lastname": "성",
"source": "소스",
"peers": "피어",
"admin": "관리자"
},
"no-user": {
"headline": "사용 가능한 사용자 없음",
"abstract": "현재 WireGuard 포털에 등록된 사용자가 없습니다."
},
"button-add-user": "사용자 추가",
"button-show-user": "사용자 보기",
"button-edit-user": "사용자 편집",
"user-disabled": "사용자가 비활성화됨, 이유:",
"user-locked": "계정이 잠김, 이유:",
"admin": "사용자에게 관리자 권한이 있습니다",
"no-admin": "사용자에게 관리자 권한이 없습니다"
},
"profile": {
"headline": "내 VPN 피어",
"table-heading": {
"name": "이름",
"ip": "IP 주소",
"stats": "상태",
"interface": "서버 인터페이스"
},
"no-peer": {
"headline": "사용 가능한 피어 없음",
"abstract": "현재 사용자 프로필과 연결된 피어가 없습니다."
},
"peer-connected": "연결됨",
"button-add-peer": "피어 추가",
"button-show-peer": "피어 보기",
"button-edit-peer": "피어 편집"
},
"settings": {
"headline": "설정",
"abstract": "여기에서 개인 설정을 변경할 수 있습니다.",
"api": {
"headline": "API 설정",
"abstract": "여기에서 RESTful API 설정을 구성할 수 있습니다.",
"active-description": "현재 사용자 계정에 대해 API가 활성화되어 있습니다. 모든 API 요청은 기본 인증(Basic Auth)으로 인증됩니다. 인증에 다음 자격 증명을 사용하세요.",
"inactive-description": "현재 API가 비활성화되어 있습니다. 활성화하려면 아래 버튼을 누르세요.",
"user-label": "API 사용자 이름:",
"user-placeholder": "API 사용자",
"token-label": "API 비밀번호:",
"token-placeholder": "API 토큰",
"token-created-label": "API 액세스 권한 부여 시각: ",
"button-disable-title": "API를 비활성화합니다. 현재 토큰이 무효화됩니다.",
"button-disable-text": "API 비활성화",
"button-enable-title": "API를 활성화합니다. 새 토큰이 생성됩니다.",
"button-enable-text": "API 활성화",
"api-link": "API 문서"
}
},
"audit": {
"headline": "감사 로그",
"abstract": "여기에서 WireGuard 포털에서 수행된 모든 작업의 감사 로그를 찾을 수 있습니다.",
"no-entries": {
"headline": "로그 항목 없음",
"abstract": "현재 기록된 감사 로그가 없습니다."
},
"entries-headline": "로그 항목",
"table-heading": {
"id": "#",
"time": "시간",
"user": "사용자",
"severity": "심각도",
"origin": "출처",
"message": "메시지"
}
},
"modals": {
"user-view": {
"headline": "사용자 계정:",
"tab-user": "정보",
"tab-peers": "피어",
"headline-info": "사용자 정보:",
"headline-notes": "메모:",
"email": "이메일",
"firstname": "이름",
"lastname": "성",
"phone": "전화번호",
"department": "부서",
"api-enabled": "API 액세스",
"disabled": "계정 비활성화됨",
"locked": "계정 잠김",
"no-peers": "사용자에게 연결된 피어가 없습니다.",
"peers": {
"name": "이름",
"interface": "인터페이스",
"ip": "IP 주소"
}
},
"user-edit": {
"headline-edit": "사용자 편집:",
"headline-new": "새 사용자",
"header-general": "일반",
"header-personal": "사용자 정보",
"header-notes": "메모",
"header-state": "상태",
"identifier": {
"label": "식별자",
"placeholder": "고유한 사용자 식별자"
},
"source": {
"label": "소스",
"placeholder": "사용자 소스"
},
"password": {
"label": "비밀번호",
"placeholder": "매우 비밀스러운 비밀번호",
"description": "현재 비밀번호를 유지하려면 이 필드를 비워 두세요."
},
"email": {
"label": "이메일",
"placeholder": "이메일 주소"
},
"phone": {
"label": "전화번호",
"placeholder": "전화번호"
},
"department": {
"label": "부서",
"placeholder": "부서"
},
"firstname": {
"label": "이름",
"placeholder": "이름"
},
"lastname": {
"label": "성",
"placeholder": "성"
},
"notes": {
"label": "메모",
"placeholder": ""
},
"disabled": {
"label": "비활성화됨 (WireGuard 연결 및 로그인 불가)"
},
"locked": {
"label": "잠김 (로그인 불가, WireGuard 연결은 계속 작동)"
},
"admin": {
"label": "관리자 여부"
}
},
"interface-view": {
"headline": "인터페이스 구성:"
},
"interface-edit": {
"headline-edit": "인터페이스 편집:",
"headline-new": "새 인터페이스",
"tab-interface": "인터페이스",
"tab-peerdef": "피어 기본값",
"header-general": "일반",
"header-network": "네트워크",
"header-crypto": "암호화",
"header-hooks": "인터페이스 후크",
"header-peer-hooks": "후크",
"header-state": "상태",
"identifier": {
"label": "식별자",
"placeholder": "고유한 인터페이스 식별자"
},
"mode": {
"label": "인터페이스 모드",
"server": "서버 모드",
"client": "클라이언트 모드",
"any": "알 수 없는 모드"
},
"display-name": {
"label": "표시 이름",
"placeholder": "인터페이스에 대한 설명적인 이름"
},
"private-key": {
"label": "개인 키",
"placeholder": "개인 키"
},
"public-key": {
"label": "공개 키",
"placeholder": "공개 키"
},
"ip": {
"label": "IP 주소",
"placeholder": "IP 주소 (CIDR 형식)"
},
"listen-port": {
"label": "수신 포트",
"placeholder": "수신 포트"
},
"dns": {
"label": "DNS 서버",
"placeholder": "사용해야 하는 DNS 서버"
},
"dns-search": {
"label": "DNS 검색 도메인",
"placeholder": "DNS 검색 접두사"
},
"mtu": {
"label": "MTU",
"placeholder": "인터페이스 MTU (0 = 기본값 유지)"
},
"firewall-mark": {
"label": "방화벽 표시",
"placeholder": "나가는 트래픽에 적용되는 방화벽 표시. (0 = 자동)"
},
"routing-table": {
"label": "라우팅 테이블",
"placeholder": "라우팅 테이블 ID",
"description": "특수 사례: off = 경로 관리 안 함, 0 = 자동"
},
"pre-up": {
"label": "Pre-Up",
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
},
"post-up": {
"label": "Post-Up",
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
},
"pre-down": {
"label": "Pre-Down",
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
},
"post-down": {
"label": "Post-Down",
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
},
"disabled": {
"label": "인터페이스 비활성화됨"
},
"save-config": {
"label": "wg-quick 구성 자동 저장"
},
"defaults": {
"endpoint": {
"label": "엔드포인트 주소",
"placeholder": "엔드포인트 주소",
"description": "피어가 연결할 엔드포인트 주소. (예: wg.example.com 또는 wg.example.com:51820)"
},
"networks": {
"label": "IP 네트워크",
"placeholder": "네트워크 주소",
"description": "피어는 해당 서브넷에서 IP 주소를 받습니다."
},
"allowed-ip": {
"label": "허용된 IP 주소",
"placeholder": "기본 허용 IP 주소"
},
"mtu": {
"label": "MTU",
"placeholder": "클라이언트 MTU (0 = 기본값 유지)"
},
"keep-alive": {
"label": "Keep Alive 간격",
"placeholder": "영구 Keepalive (0 = 기본값)"
}
},
"button-apply-defaults": "피어 기본값 적용"
},
"peer-view": {
"headline-peer": "피어:",
"headline-endpoint": "엔드포인트:",
"section-info": "피어 정보",
"section-status": "현재 상태",
"section-config": "구성",
"identifier": "식별자",
"ip": "IP 주소",
"user": "연결된 사용자",
"notes": "메모",
"expiry-status": "만료 시각",
"disabled-status": "비활성화 시각",
"traffic": "트래픽",
"connection-status": "연결 통계",
"upload": "업로드된 바이트 (서버에서 피어로)",
"download": "다운로드된 바이트 (피어에서 서버로)",
"pingable": "핑 가능 여부",
"handshake": "마지막 핸드셰이크",
"connected-since": "연결 시작 시각",
"endpoint": "엔드포인트",
"button-download": "구성 다운로드",
"button-email": "이메일로 구성 보내기"
},
"peer-edit": {
"headline-edit-peer": "피어 편집:",
"headline-edit-endpoint": "엔드포인트 편집:",
"headline-new-peer": "피어 생성",
"headline-new-endpoint": "엔드포인트 생성",
"header-general": "일반",
"header-network": "네트워크",
"header-crypto": "암호화",
"header-hooks": "후크 (피어에서 실행됨)",
"header-state": "상태",
"display-name": {
"label": "표시 이름",
"placeholder": "피어에 대한 설명적인 이름"
},
"linked-user": {
"label": "연결된 사용자",
"placeholder": "이 피어를 소유한 사용자 계정"
},
"private-key": {
"label": "개인 키",
"placeholder": "개인 키"
},
"public-key": {
"label": "공개 키",
"placeholder": "공개 키"
},
"preshared-key": {
"label": "사전 공유 키",
"placeholder": "선택적 사전 공유 키"
},
"endpoint-public-key": {
"label": "엔드포인트 공개 키",
"placeholder": "원격 엔드포인트의 공개 키"
},
"endpoint": {
"label": "엔드포인트 주소",
"placeholder": "원격 엔드포인트의 주소"
},
"ip": {
"label": "IP 주소",
"placeholder": "IP 주소 (CIDR 형식)"
},
"allowed-ip": {
"label": "허용된 IP 주소",
"placeholder": "허용된 IP 주소 (CIDR 형식)"
},
"extra-allowed-ip": {
"label": "추가 허용 IP 주소",
"placeholder": "추가 허용 IP (서버 측)",
"description": "이 IP 주소는 원격 WireGuard 인터페이스에 허용된 IP로 추가됩니다."
},
"dns": {
"label": "DNS 서버",
"placeholder": "사용해야 하는 DNS 서버"
},
"dns-search": {
"label": "DNS 검색 도메인",
"placeholder": "DNS 검색 접두사"
},
"keep-alive": {
"label": "Keep Alive 간격",
"placeholder": "영구 Keepalive (0 = 기본값)"
},
"mtu": {
"label": "MTU",
"placeholder": "클라이언트 MTU (0 = 기본값 유지)"
},
"pre-up": {
"label": "Pre-Up",
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
},
"post-up": {
"label": "Post-Up",
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
},
"pre-down": {
"label": "Pre-Down",
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
},
"post-down": {
"label": "Post-Down",
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
},
"disabled": {
"label": "피어 비활성화됨"
},
"ignore-global": {
"label": "전역 설정 무시"
},
"expires-at": {
"label": "만료 날짜"
}
},
"peer-multi-create": {
"headline-peer": "여러 피어 생성",
"headline-endpoint": "여러 엔드포인트 생성",
"identifiers": {
"label": "사용자 식별자",
"placeholder": "사용자 식별자",
"description": "피어를 생성할 사용자 식별자 (사용자 이름)."
},
"prefix": {
"headline-peer": "피어:",
"headline-endpoint": "엔드포인트:",
"label": "표시 이름 접두사",
"placeholder": "접두사",
"description": "피어 표시 이름에 추가되는 접두사."
}
}
}
}

View File

@@ -0,0 +1,182 @@
{
"languages": {
"pt": "Português"
},
"general": {
"pagination": {
"size": "Número de Elementos",
"all": "Todos (lento)"
},
"search": {
"placeholder": "Pesquisar...",
"button": "Pesquisar"
},
"select-all": "Selecionar tudo",
"yes": "Sim",
"no": "Não",
"cancel": "Cancelar",
"close": "Fechar",
"save": "Guardar",
"delete": "Eliminar"
},
"login": {
"headline": "Por favor, inicie sessão",
"username": {
"label": "Nome de utilizador",
"placeholder": "Introduza o seu nome de utilizador"
},
"password": {
"label": "Palavra-passe",
"placeholder": "Introduza a sua palavra-passe"
},
"button": "Iniciar sessão"
},
"menu": {
"home": "Início",
"interfaces": "Interfaces",
"users": "Utilizadores",
"lang": "Alterar idioma",
"profile": "O Meu Perfil",
"settings": "Definições",
"audit": "Registo de Auditoria",
"login": "Iniciar Sessão",
"logout": "Terminar Sessão"
},
"home": {
"title": "Início",
"card": {
"interfaces": "Interfaces",
"users": "Utilizadores"
}
},
"interfaces": {
"title": "Interfaces",
"create": "Criar Interface",
"name": "Nome",
"address": "Endereço",
"listen-port": "Porta de Escuta",
"public-key": "Chave Pública",
"private-key": "Chave Privada",
"actions": "Ações",
"delete-dialog": {
"title": "Eliminar Interface",
"text": "Tem a certeza de que deseja eliminar a interface '{{name}}'?"
},
"form": {
"name": {
"label": "Nome",
"placeholder": "Introduza um nome exclusivo para a interface"
},
"address": {
"label": "Endereço",
"placeholder": "Introduza um endereço válido (ex: 10.0.0.1/24)"
},
"listen-port": {
"label": "Porta de Escuta",
"placeholder": "Introduza a porta onde o WireGuard irá escutar (ex: 51820)"
},
"private-key": {
"label": "Chave Privada",
"placeholder": "Será gerada automaticamente se não for fornecida"
}
}
},
"users": {
"title": "Utilizadores",
"create": "Criar Utilizador",
"name": "Nome",
"email": "Email",
"enabled": "Ativo",
"is-admin": "Administrador",
"actions": "Ações",
"edit": "Editar",
"delete-dialog": {
"title": "Eliminar Utilizador",
"text": "Tem a certeza de que deseja eliminar o utilizador '{{nome}}'?"
},
"form": {
"name": {
"label": "Nome",
"placeholder": "Introduza o nome do utilizador"
},
"email": {
"label": "Email",
"placeholder": "Introduza o email do utilizador"
},
"password": {
"label": "Palavra-passe",
"placeholder": "Deixe em branco para manter a atual"
},
"is-admin": {
"label": "Administrador"
},
"enabled": {
"label": "Ativo"
}
}
},
"peers": {
"title": "Peers",
"create": "Criar Peer",
"public-key": "Chave Pública",
"preshared-key": "Chave Pré-partilhada",
"endpoint": "Endpoint",
"allowed-ips": "IPs Permitidos",
"latest-handshake": "Último Handshake",
"transfer-rx": "Recebido",
"transfer-tx": "Enviado",
"persistent-keepalive": "Keepalive Persistente",
"actions": "Ações",
"edit": "Editar",
"delete-dialog": {
"title": "Eliminar Peer",
"text": "Tem a certeza de que deseja eliminar este peer?"
},
"form": {
"public-key": {
"label": "Chave Pública",
"placeholder": "Introduza a chave pública do peer"
},
"preshared-key": {
"label": "Chave Pré-partilhada",
"placeholder": "Opcional: Chave partilhada adicional para maior segurança"
},
"endpoint": {
"label": "Endpoint",
"placeholder": "Endereço público do peer (ex: 1.2.3.4:51820)"
},
"allowed-ips": {
"label": "IPs Permitidos",
"placeholder": "Lista de IPs (ex: 10.0.0.2/32, 192.168.1.0/24)"
},
"persistent-keepalive": {
"label": "Keepalive Persistente",
"placeholder": "Ex: 25 (em segundos)"
}
}
},
"settings": {
"title": "Definições",
"password": {
"label": "Nova Palavra-passe",
"placeholder": "Deixe em branco para manter a atual"
},
"save": "Guardar Alterações"
},
"audit": {
"title": "Registo de Auditoria",
"username": "Utilizador",
"ip": "Endereço IP",
"method": "Método",
"path": "Caminho",
"status": "Estado",
"timestamp": "Data/Hora"
},
"errors": {
"required": "Este campo é obrigatório",
"invalid-email": "Endereço de email inválido",
"invalid-address": "Endereço inválido",
"invalid-endpoint": "Endpoint inválido",
"invalid-allowed-ips": "Formato de IPs Permitidos inválido"
}
}

View File

@@ -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'
}
})

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -393,8 +393,14 @@ func (r *WgRepo) getOrCreatePeer(deviceId domain.InterfaceIdentifier, id domain.
},
},
})
if err != nil {
return nil, fmt.Errorf("peer create error for %s: %w", id.ToPublicKey(), err)
}
peer, err = r.getPeer(deviceId, id)
if err != nil {
return nil, fmt.Errorf("peer error after create: %w", err)
}
return peer, nil
}

View 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
}

View File

@@ -82,7 +82,7 @@ func (m Manager) RegisterUser(ctx context.Context, user *domain.User) error {
return err
}
m.bus.Publish(app.TopicUserRegistered, createdUser)
m.bus.Publish(app.TopicUserRegistered, *createdUser)
return nil
}
@@ -294,8 +294,8 @@ func (m Manager) ActivateApi(ctx context.Context, id domain.UserIdentifier) (*do
return nil, fmt.Errorf("update failure: %w", err)
}
m.bus.Publish(app.TopicUserUpdated, user)
m.bus.Publish(app.TopicUserApiEnabled, user)
m.bus.Publish(app.TopicUserUpdated, *user)
m.bus.Publish(app.TopicUserApiEnabled, *user)
return user, nil
}
@@ -322,8 +322,8 @@ func (m Manager) DeactivateApi(ctx context.Context, id domain.UserIdentifier) (*
return nil, fmt.Errorf("update failure: %w", err)
}
m.bus.Publish(app.TopicUserUpdated, user)
m.bus.Publish(app.TopicUserApiDisabled, user)
m.bus.Publish(app.TopicUserUpdated, *user)
m.bus.Publish(app.TopicUserApiDisabled, *user)
return user, nil
}
@@ -389,12 +389,14 @@ func (m Manager) validateCreation(ctx context.Context, new *domain.User) error {
return fmt.Errorf("reserved user identifier: %w", domain.ErrInvalidData)
}
if new.Source != domain.UserSourceDatabase {
// Admins are allowed to create users for arbitrary sources.
if new.Source != domain.UserSourceDatabase && !currentUser.IsAdmin {
return fmt.Errorf("invalid user source: %s, only %s is allowed: %w",
new.Source, domain.UserSourceDatabase, domain.ErrInvalidData)
}
if string(new.Password) == "" {
// database users must have a password
if new.Source == domain.UserSourceDatabase && string(new.Password) == "" {
return fmt.Errorf("invalid password: %w", domain.ErrInvalidData)
}
@@ -430,6 +432,8 @@ func (m Manager) validateApiChange(ctx context.Context, user *domain.User) error
}
func (m Manager) runLdapSynchronizationService(ctx context.Context) {
ctx = domain.SetUserInfo(ctx, domain.LdapSyncContextUserInfo()) // switch to service context for LDAP sync
for _, ldapCfg := range m.cfg.Auth.Ldap { // LDAP Auth providers
go func(cfg config.LdapProvider) {
syncInterval := cfg.SyncInterval

View File

@@ -112,7 +112,7 @@ func (m Manager) connectToMessageBus() {
_ = m.bus.Subscribe(app.TopicUserDeleted, m.handleUserDeletedEvent)
}
func (m Manager) handleUserCreationEvent(user *domain.User) {
func (m Manager) handleUserCreationEvent(user domain.User) {
if !m.cfg.Core.CreateDefaultPeerOnCreation {
return
}

View File

@@ -190,7 +190,7 @@ func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
return nil, fmt.Errorf("failed to prepare peer for interface %s: %w", peer.InterfaceIdentifier, err)
}
preparedPeer.OverwriteUserEditableFields(peer)
preparedPeer.OverwriteUserEditableFields(peer, m.cfg)
peer = preparedPeer
}
@@ -278,7 +278,7 @@ func (m Manager) UpdatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
if err != nil {
return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err)
}
originalPeer.OverwriteUserEditableFields(peer)
originalPeer.OverwriteUserEditableFields(peer, m.cfg)
peer = originalPeer
}

View File

@@ -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 {

View File

@@ -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"`
}

View File

@@ -45,6 +45,14 @@ func SystemAdminContextUserInfo() *ContextUserInfo {
}
}
// LdapSyncContextUserInfo returns a context user info for the LDAP syncer.
func LdapSyncContextUserInfo() *ContextUserInfo {
return &ContextUserInfo{
Id: CtxSystemLdapSyncer,
IsAdmin: true,
}
}
// SetUserInfo sets the user info in the context.
func SetUserInfo(ctx context.Context, info *ContextUserInfo) context.Context {
ctx = context.WithValue(ctx, CtxUserInfo, info)

View File

@@ -7,7 +7,7 @@ import (
)
type KeyPair struct {
PrivateKey string
PrivateKey string `gorm:"serializer:encstr"`
PublicKey string
}

View File

@@ -9,6 +9,7 @@ import (
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/config"
)
type PeerIdentifier string
@@ -35,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
@@ -129,16 +130,18 @@ func (p *Peer) GenerateDisplayName(prefix string) {
}
// OverwriteUserEditableFields overwrites the user editable fields of the peer with the values from the userPeer
func (p *Peer) OverwriteUserEditableFields(userPeer *Peer) {
func (p *Peer) OverwriteUserEditableFields(userPeer *Peer, cfg *config.Config) {
p.DisplayName = userPeer.DisplayName
p.Interface.PublicKey = userPeer.Interface.PublicKey
p.Interface.PrivateKey = userPeer.Interface.PrivateKey
if cfg.Core.EditableKeys {
p.Interface.PublicKey = userPeer.Interface.PublicKey
p.Interface.PrivateKey = userPeer.Interface.PrivateKey
p.PresharedKey = userPeer.PresharedKey
}
p.Interface.Mtu = userPeer.Interface.Mtu
p.PersistentKeepalive = userPeer.PersistentKeepalive
p.ExpiresAt = userPeer.ExpiresAt
p.Disabled = userPeer.Disabled
p.DisabledReason = userPeer.DisabledReason
p.PresharedKey = userPeer.PresharedKey
}
type PeerInterfaceConfig struct {

View File

@@ -5,6 +5,7 @@ import (
"time"
"github.com/stretchr/testify/assert"
"github.com/h44z/wg-portal/internal/config"
)
func TestPeer_IsDisabled(t *testing.T) {
@@ -98,7 +99,7 @@ func TestPeer_OverwriteUserEditableFields(t *testing.T) {
DisplayName: "New DisplayName",
}
peer.OverwriteUserEditableFields(userPeer)
peer.OverwriteUserEditableFields(userPeer, &config.Config{})
assert.Equal(t, "New DisplayName", peer.DisplayName)
}

View File

@@ -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