diff --git a/.github/workflows/chart.yml b/.github/workflows/chart.yml index 4844f1f..4659351 100644 --- a/.github/workflows/chart.yml +++ b/.github/workflows/chart.yml @@ -44,7 +44,7 @@ jobs: - name: Run chart-testing (lint) run: ct lint --config ct.yaml - - uses: nolar/setup-k3d-k3s@293b8e5822a20bc0d5bcdd4826f1a665e72aba96 # v1.0.9 + - uses: nolar/setup-k3d-k3s@8bf8d22160e8b1d184dcb780e390d6952a7eec65 # v1.0.10 with: github-token: ${{ secrets.GITHUB_TOKEN }} @@ -62,7 +62,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index f771faf..44fc5a1 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -21,10 +21,10 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up QEMU - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Get Version shell: bash @@ -32,14 +32,14 @@ jobs: - name: Login to Docker Hub if: github.event_name != 'pull_request' - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Login to GitHub Container Registry if: github.event_name != 'pull_request' - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -47,7 +47,7 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 with: images: | wgportal/wg-portal @@ -68,7 +68,7 @@ jobs: type=semver,pattern=v{{major}} - name: Build and push Docker image - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: . push: ${{ github.event_name != 'pull_request' }} @@ -80,7 +80,7 @@ jobs: BUILD_VERSION=${{ env.BUILD_VERSION }} - name: Export binaries from images - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: . platforms: linux/amd64,linux/arm64,linux/arm/v7 @@ -110,12 +110,12 @@ jobs: contents: write steps: - name: Download binaries - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: binaries - name: Create GitHub Release - uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 + uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 with: files: 'wg-portal_linux*' generate_release_notes: true diff --git a/cmd/wg-portal/main.go b/cmd/wg-portal/main.go index 0592b82..327e327 100644 --- a/cmd/wg-portal/main.go +++ b/cmd/wg-portal/main.go @@ -80,7 +80,7 @@ func main() { internal.AssertNoError(err) auditRecorder.StartBackgroundJobs(ctx) - userManager, err := users.NewUserManager(cfg, eventBus, database, database) + userManager, err := users.NewUserManager(cfg, eventBus, database, database, database) internal.AssertNoError(err) userManager.StartBackgroundJobs(ctx) diff --git a/docs/documentation/configuration/examples.md b/docs/documentation/configuration/examples.md index bafc3df..7dfdb33 100644 --- a/docs/documentation/configuration/examples.md +++ b/docs/documentation/configuration/examples.md @@ -86,6 +86,9 @@ auth: memberof: memberOf admin_group: CN=WireGuardAdmins,OU=Some-OU,DC=COMPANY,DC=LOCAL registration_enabled: true + # Restrict interface access based on LDAP filters + interface_filter: + wg0: "(memberOf=CN=VPNUsers,OU=Groups,DC=COMPANY,DC=LOCAL)" log_user_info: true ``` diff --git a/docs/documentation/configuration/overview.md b/docs/documentation/configuration/overview.md index 295e362..9fa1f84 100644 --- a/docs/documentation/configuration/overview.md +++ b/docs/documentation/configuration/overview.md @@ -28,6 +28,7 @@ core: backend: default: local + rekey_timeout_interval: 125s local_resolvconf_prefix: tun. advanced: @@ -203,6 +204,13 @@ The current MikroTik backend is in **BETA** and may not support all features. - **Description:** The default backend to use for managing WireGuard interfaces. Valid options are: `local`, or other backend id's configured in the `mikrotik` section. +### `rekey_timeout_interval` +- **Default:** `180s` +- **Environment Variable:** `WG_PORTAL_BACKEND_REKEY_TIMEOUT_INTERVAL` +- **Description:** The interval after which a WireGuard peer is considered disconnected if no handshake updates are received. + This corresponds to the WireGuard rekey timeout setting of 120 seconds plus a 60-second buffer to account for latency or retry handling. + Uses Go duration format (e.g., `10s`, `1m`). If omitted, a default of 180 seconds is used. + ### `local_resolvconf_prefix` - **Default:** `tun.` - **Environment Variable:** `WG_PORTAL_BACKEND_LOCAL_RESOLVCONF_PREFIX` @@ -734,6 +742,16 @@ Below are the properties for each LDAP provider entry inside `auth.ldap`: - **Important**: The `login_filter` must always be a valid LDAP filter. It should at most return one user. If the filter returns multiple or no users, the login will fail. +#### `interface_filter` +- **Default:** *(empty)* +- **Description:** A map of LDAP filters to restrict access to specific WireGuard interfaces. The map keys are the interface identifiers (e.g., `wg0`), and the values are LDAP filters. Only users matching the filter will be allowed to provision peers for the respective interface. + For example: + ```yaml + interface_filter: + wg0: "(memberOf=CN=VPNUsers,OU=Groups,DC=COMPANY,DC=LOCAL)" + wg1: "(description=special-access)" + ``` + #### `admin_group` - **Default:** *(empty)* - **Description:** A specific LDAP group whose members are considered administrators in WireGuard Portal. diff --git a/docs/documentation/getting-started/docker.md b/docs/documentation/getting-started/docker.md index 091aaf7..1176bc1 100644 --- a/docs/documentation/getting-started/docker.md +++ b/docs/documentation/getting-started/docker.md @@ -35,6 +35,14 @@ WireGuard Portal supports managing WireGuard interfaces through three distinct d > :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. + > :warning: If the host is running **systemd-networkd**, routes managed by WireGuard Portal may be removed whenever systemd-networkd restarts, as it will clean up routes it considers "foreign". To prevent this, add the following to your host's network configuration (e.g. `/etc/systemd/networkd.conf` or a drop-in file): + > ```ini + > [Network] + > ManageForeignRoutingPolicyRules=no + > ManageForeignRoutes=no + > ``` + > After editing, reload the configuration with `sudo systemctl restart systemd-networkd`. For more information refer to the [systemd-networkd documentation](https://www.freedesktop.org/software/systemd/man/latest/networkd.conf.html#ManageForeignRoutes=). + - **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. diff --git a/docs/documentation/usage/authentication.md b/docs/documentation/usage/authentication.md index 76a9d67..d02afeb 100644 --- a/docs/documentation/usage/authentication.md +++ b/docs/documentation/usage/authentication.md @@ -147,6 +147,26 @@ You can map users to admin roles based on their group membership in the LDAP ser The `admin_group` property defines the distinguished name of the group that is allowed to log in as admin. All groups that are listed in the `memberof` attribute of the user will be checked against this group. If one of the groups matches, the user is granted admin access. +### Interface-specific Provisioning Filters + +You can restrict which users are allowed to provision peers for specific WireGuard interfaces by setting the `interface_filter` property. +This property is a map where each key corresponds to a WireGuard interface identifier, and the value is an LDAP filter. +A user will only be able to see and provision peers for an interface if they match the specified LDAP filter for that interface. + +Example: +```yaml +auth: + ldap: + - provider_name: "ldap1" + # ... other settings + interface_filter: + wg0: "(memberOf=CN=VPNUsers,OU=Groups,DC=COMPANY,DC=LOCAL)" + wg1: "(department=IT)" +``` + +This feature works by materializing the list of authorized users for each interface during the periodic LDAP synchronization. +Even if a user bypasses the UI, the backend will enforce these restrictions at the service layer. + ## User Synchronization diff --git a/docs/documentation/usage/user-sync.md b/docs/documentation/usage/user-sync.md index b248ffb..e7c4e2a 100644 --- a/docs/documentation/usage/user-sync.md +++ b/docs/documentation/usage/user-sync.md @@ -43,4 +43,12 @@ If you set the `disable_missing` property to `true`, any user that is not found All peers associated with that user will also be disabled. If you want a user and its peers to be automatically re-enabled once they are found in LDAP again, set the `auto_re_enable` property to `true`. -This will only re-enable the user if they were disabled by the synchronization process. Manually disabled users will not be re-enabled. \ No newline at end of file +This will only re-enable the user if they were disabled by the synchronization process. Manually disabled users will not be re-enabled. + +##### Interface-specific Access Materialization + +If `interface_filter` is configured in the LDAP provider, the synchronization process will evaluate these filters for each enabled user. +The results are materialized in the `interfaces` table of the database in a hidden field. +This materialized list is used by the backend to quickly determine if a user has permission to provision peers for a specific interface, without having to query the LDAP server for every request. +The list is refreshed every time the LDAP synchronization runs. +For more details on how to configure these filters, see the [Authentication](./authentication.md#interface-specific-provisioning-filters) section. \ No newline at end of file diff --git a/frontend/src/stores/profile.js b/frontend/src/stores/profile.js index 8b5df8f..632e931 100644 --- a/frontend/src/stores/profile.js +++ b/frontend/src/stores/profile.js @@ -74,6 +74,7 @@ export const profileStore = defineStore('profile', { }, hasStatistics: (state) => state.statsEnabled, CountInterfaces: (state) => state.interfaces.length, + HasInterface: (state) => (id) => state.interfaces.some((i) => i.Identifier === id), }, actions: { afterPageSizeChange() { diff --git a/frontend/src/views/ProfileView.vue b/frontend/src/views/ProfileView.vue index 8f7b3e5..6c91c6c 100644 --- a/frontend/src/views/ProfileView.vue +++ b/frontend/src/views/ProfileView.vue @@ -80,6 +80,8 @@ onMounted(async () => {

{{ $t('profile.headline') }}

+
+
@@ -90,8 +92,8 @@ onMounted(async () => {
-
-
+
+