# wg-portal-2 [![Build Status](https://github.com/fedor-git/wg-portal-2/actions/workflows/docker-publish.yml/badge.svg?event=push)](https://github.com/fedor-git/wg-portal-2/actions/workflows/docker-publish.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://opensource.org/licenses/MIT) ![GitHub last commit](https://img.shields.io/github/last-commit/fedor-git/wg-portal-2/master) [![Go Report Card](https://goreportcard.com/badge/github.com/fedor-git/wg-portal-2)](https://goreportcard.com/report/github.com/fedor-git/wg-portal-2) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/fedor-git/wg-portal-2) ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/fedor-git/wg-portal-2) [![Forked from original-repo](https://img.shields.io/badge/Forked%20from-h44z%2Fwg--portal-blue?logo=github)](https://github.com/h44z/wg-portal) ## Recent Enhancements This fork includes several important improvements: ### 🚀 Cluster Mode Support Run multiple instances with a shared database. Synchronization is handled by the **fanout module** for seamless peer and interface updates across all nodes. ### 🔧 Configuration Enhancements New core configuration options for fine-grained control over DNS, routing, and peer lifecycle management. ## Introduction **WireGuard Portal** is a simple, web-based configuration portal for [WireGuard](https://wireguard.com) server management. The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) library to manage existing VPN 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 (Active Directory or OpenLDAP) as a user source for authentication and profile data. ## Features * Self-hosted - the whole application is a single binary * Responsive multi-language web UI written in Vue.js * Automatically selects IP from the network pool assigned to the client * QR-Code for convenient mobile client configuration * Sends email to the client with QR-code and client config * Enable / Disable clients seamlessly * Generation of wg-quick configuration file (`wgX.conf`) if required * User authentication (database, OAuth, or LDAP), Passkey support * IPv6 ready * Docker ready * Can be used with existing WireGuard setups * Support for multiple WireGuard interfaces * Supports multiple WireGuard backends (wgctrl or MikroTik [BETA]) * Peer Expiry Feature * 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 ![Screenshot](docs/assets/images/screenshot.png) ## Configuration Guide ### Core Options The latest version introduces several new core configuration options for production deployments: | Option | Type | Default | Description | |--------|------|---------|-------------| | `admin_user` | string | - | Administrator username/email for initial setup | | `admin_password` | string | - | Administrator password (hashed in database) | | `admin_api_token` | string | - | API token for admin operations and cluster auth | | `restore_state` | boolean | `false` | Restore WireGuard interface state from database on startup | | `import_existing` | boolean | `false` | Import existing WireGuard configuration | | `manage_dns` | boolean | `true` | Enable/disable automatic DNS management (resolv.conf). Set to `false` if managing DNS externally. | | `ignore_main_default_route` | boolean | `false` | When `true`, the main route table won't be modified. Useful in container or complex networking setups. | | `delete_expired_peers` | boolean | `false` | Automatically remove peers after their TTL expires. | | `default_user_ttl` | string/integer | `0` | Default Time-To-Live for peers (e.g., `30d`, `1` for 1 day). `0` means no expiration. | | `sync_on_startup` | boolean | `false` | Synchronize interface state with database on startup | | `force_client_ip_as_allowed_ip` | boolean | `false` | Use client IP as allowed IP instead of calculating from pool | | `master` | boolean | `false` | Mark this instance as master (for failover scenarios) | ### Cluster Mode with Fanout Module The **Fanout module** enables synchronization across multiple WireGuard Portal instances sharing a single database. This is essential for high-availability deployments. #### How It Works 1. Each instance publishes events (peer updates, deletions, interface changes) to other instances 2. All instances listen for updates and apply changes locally 3. Debouncing prevents thundering herd problems 4. Automatic peer kicking on startup ensures consistency #### Fanout Configuration Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `enabled` | boolean | `false` | Enable cluster synchronization | | `self_url` | string | - | This instance's URL (supports `{{ external_url }}` template) | | `peers` | array | - | List of peer node URLs for cluster communication | | `auth_header` | string | `Authorization` | HTTP header name for authentication | | `auth_value` | string | - | Auth value (e.g., `Basic base64{user:token}`) | | `timeout` | duration | `2s` | Request timeout for peer communication | | `debounce` | duration | `300ms` | Time to wait before batching events | | `origin` | string | `wg-portal` | Optional identifier for event origin | | `kick_on_start` | boolean | `true` | Remove stale entries on startup | | `tls_skip_verify` | boolean | `false` | Skip TLS verification for peer URLs (NOT for production!) | | `topics` | array | all | Event topics to synchronize | #### Configuration Example ```yaml core: admin_user: username@example.com admin_password: ... restore_state: false import_existing: false admin_api_token: ... manage_dns: false ignore_main_default_route: true delete_expired_peers: true default_user_ttl: 1d # 1 days default expiration sync_on_startup: true force_client_ip_as_allowed_ip: true master: true fanout: enabled: true self_url: "https://wg-portal-1.example.com" # Use template variable: {{ external_url }} # List of other cluster nodes peers: - "https://wg-portal-2.example.com" - "https://wg-portal-3.example.com" # Communication settings auth_header: "Authorization" auth_value: "Basic base64{admin_user:admin_api_token}" timeout: 2s debounce: 300ms # Wait 300ms before processing event batch origin: "wg-portal" # Optional origin identifier kick_on_start: true # Remove stale peer entries on startup # Event topics to synchronize (all shown by default) topics: - "peers.updated" # When peer list is updated - "peer.save" # Individual peer saved - "peer.delete" # Peer deleted - "interface.save" # Interface configuration changed - "interface.updated"# Interface state changed tls_skip_verify: true advanced: config_storage_path: /etc/wireguard/ rule_prio_offset: 32000 log_level: info log_pretty: true log_json: false expiry_check_interval: 5h web: listening_address: :8888 external_url: https://wg-portal-1.example.com:4848 request_logging: true csrf_secret: ... session_secret: ... statistics: use_ping_checks: false ping_check_workers: 3 ping_unprivileged: false ping_check_interval: 5m data_collection_interval: 60s collect_interface_data: true collect_peer_data: true collect_audit_data: true store_audit_data: false listening_address: ":8787" export_detailed_peer_metrics: false only_export_connected_peers: true ``` #### Deployment Architecture ``` ┌───────────────────────────┐ │ Shared Database │ │ MySQL/PostgreSQL │ └─────────────┬─────────────┘ │ ┌─────────────────┼──────────────────┐ │ │ │ ┌───▼────┐ ┌───▼────┐ ┌────▼───┐ │ Node 1 │◄──────►│ Node 2 │◄──────►│ Node 3 │ │ Portal │ │ Portal │ │ Portal │ └───┬────┘ └───┬────┘ └────┬───┘ │ │ │ [wg0] [wg0] [wg0] │ │ │ ┌───┴─────────────────┼──────────────────┴───┐ │ Network / Load Balancer (HAProxy/nginx) │ └────────────────────────────────────────────┘ ``` ### DNS Management When `manage_dns: false`, the portal won't modify `/etc/resolv.conf`. This is useful when: - Running in containers with custom DNS configuration - Using external DNS management tools - DNS is configured via cloud orchestration (Kubernetes, etc.) ### Route Management Setting `ignore_main_default_route: true` prevents modification of the main route table. This is necessary for: - Complex multi-interface hosts - Containers with restricted capabilities - Environments where routing is managed externally ### Peer Expiration Configure automatic peer cleanup: ```yaml core: delete_expired_peers: true default_user_ttl: 30d # 30 days expiration ``` Individual peers can also have custom TTL values. After expiration: 1. Peer status changes to "expired" 2. Peer is automatically removed if `delete_expired_peers: true` 3. WireGuard configuration is updated automatically ### Advanced Options Fine-tune WireGuard configuration and logging: | Option | Type | Default | Description | |--------|------|---------|-------------| | `config_storage_path` | string | `/etc/wireguard/` | Directory where WireGuard config files are stored | | `rule_prio_offset` | integer | `32000` | Base priority offset for iptables rules | | `log_level` | string | `info` | Logging level: `debug`, `info`, `warn`, `error` | | `log_pretty` | boolean | `false` | Pretty-print logs (human-readable format) | | `log_json` | boolean | `false` | Output logs as JSON for log aggregation | | `expiry_check_interval` | duration | `5h` | How often to check for expired peers | ### Web Interface Options Configure the web portal: | Option | Type | Default | Description | |--------|------|---------|-------------| | `listening_address` | string | `:8080` | HTTP listening address | | `external_url` | string | - | External URL for QR codes and API responses | | `request_logging` | boolean | `true` | Log HTTP requests | | `csrf_secret` | string | - | Secret key for CSRF protection (auto-generated if not set) | | `session_secret` | string | - | Secret key for session encryption (auto-generated if not set) | ### Statistics & Monitoring Options Configure metrics collection and reporting: | Option | Type | Default | Description | |--------|------|---------|-------------| | `use_ping_checks` | boolean | `false` | Enable ping-based peer availability checks | | `ping_check_workers` | integer | `3` | Number of concurrent ping workers | | `ping_unprivileged` | boolean | `false` | Use unprivileged ping (requires ICMP capabilities) | | `ping_check_interval` | duration | `5m` | Interval between ping checks | | `data_collection_interval` | duration | `60s` | How often to collect statistics | | `collect_interface_data` | boolean | `true` | Collect interface metrics (traffic, handshakes) | | `collect_peer_data` | boolean | `true` | Collect peer-level metrics | | `collect_audit_data` | boolean | `false` | Collect audit trail data | | `store_audit_data` | boolean | `false` | Persist audit data to database | | `listening_address` | string | `:8787` | Prometheus metrics listening address | | `export_detailed_peer_metrics` | boolean | `false` | Export per-peer metrics (can be expensive) | | `only_export_connected_peers` | boolean | `true` | Only export metrics for connected peers | ## Documentation For the complete documentation visit [wgportal.org](https://wgportal.org). ## What is out of scope * Automatic generation or application of any `iptables` or `nftables` rules. * Support for operating systems other than linux. * Automatic import of private keys of an existing WireGuard setup. ## Quick Start with Cluster Mode ### Prerequisites - MySQL 5.7+ or PostgreSQL 10+ (shared database) - Linux kernel with WireGuard support - 3+ instances for HA setup (optional, 1 instance works fine for single-node) ### Docker Cluster Deployment ```bash # Create shared network docker network create wg-portal # Start database docker run -d \ --name mysql-wg \ --network wg-portal \ -e MYSQL_ROOT_PASSWORD=secret \ mysql:8.0 # Wait for DB to be ready sleep 10 # Start first portal node docker run -d \ --name wg-portal-1 \ --network wg-portal \ --cap-add=NET_ADMIN \ -e PORTAL_LISTEN_ADDRESS=0.0.0.0:8080 \ -e PORTAL_EXTERNAL_URL=https://wg-portal-1.example.com \ wgportal/wg-portal # Start second portal node docker run -d \ --name wg-portal-2 \ --network wg-portal \ --cap-add=NET_ADMIN \ -e PORTAL_LISTEN_ADDRESS=0.0.0.0:8080 \ -e PORTAL_EXTERNAL_URL=https://wg-portal-2.example.com \ wgportal/wg-portal ``` ### Verify Cluster Communication ```bash # Check logs for fanout initialization docker logs wg-portal-1 | grep -i fanout # Test peer synchronization # Create/modify peer on node 1, should appear on node 2 within debounce time ``` ## Testing & Validation The cluster mode has been tested with: - ✅ MySQL 5.7, 8.0+ - ✅ PostgreSQL 12+ - ✅ Up to 5 concurrent nodes - ✅ Load balancer (HAProxy, nginx) - ✅ Automatic failover scenarios ## Application stack * [wgctrl-go](https://github.com/WireGuard/wgctrl-go) and [netlink](https://github.com/vishvananda/netlink) for interface handling * [Bootstrap](https://getbootstrap.com/), for the HTML templates * [Vue.js](https://vuejs.org/), for the frontend ## License * MIT License. [MIT](LICENSE.txt) or > [!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 **fedor-git/wg-portal-2** to **wgportal/wg-portal**.