h44z / wg-portal

WireGuard Configuration Portal with LDAP connection
https://wgportal.org/
MIT License
920 stars 126 forks source link

LDAP Login Issue #283

Open captain-parzival opened 1 month ago

captain-parzival commented 1 month ago

I'm having issues with wg-portal and lldap as a LDAPS authentication provider.

When opening the front end, I see this error: image

If I try to sign in with any of the LDAP accounts, an authentication error occurs.

Here's the snag, LDAPS binds properly. I can see on the lldap side the request come through, bind and return the correct users based on the sync filter. I've pulled the database file to see if the user information was synced. Looks like everything came over except password, but I'm assuming that's the correct behavior. For reference, the same LDAP backend is working fine with other services.

On first run with a new database file I see this in the wg-portal log. It continues to cycle starting to synchronize, and fetching the raw users endlessly. It's also not in alignment to the synchronization interval.

TRAC[0010] starting to synchronize users for Example LDAP 

TRAC[0010] fetched 4 raw ldap users...                  

TRAC[0010] handling new user event for userA

INFO[0010] created 0 default peers for user userA

TRAC[0010] handling new user event for userB

INFO[0010] created 0 default peers for user userB

TRAC[0010] handling new user event for wireguard_read_only

INFO[0010] created 0 default peers for user wireguard_read_only

TRAC[0010] handling new user event for userC

INFO[0010] created 0 default peers for user userC

TRAC[0020] starting to synchronize users for Example LDAP 

TRAC[0021] fetched 4 raw ldap users...                  

My docker compose file:

version: '3.6'
services:
  wg-portal:
    image: wgportal/wg-portal:latest
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
    network_mode: "host"
    ports:
      - "8888:8888"
    volumes:
      - /etc/wireguard:/etc/wireguard
      - ./data:/app/data
      - ./config:/app/config

My config,yml

core:
  admin_user: "admin@wgportal.local"
  admin_password: "wgportal"
  create_default_peer: true
  create_default_peer_on_creation: true
  self_provisioning_allowed: true
  editable_keys: true

advanced:
  log_level: trace
  log_pretty: true
  ldap_sync_interval: 10m
  start_listen_port: 51820
  start_cidr_v4: 10.0.8.0/24
  use_ip_v6: false

statistics:
  use_ping_checks: true
  ping_check_workers: 10
  ping_check_interval: 1m
  data_collection_interval: 5m
  collect_interface_data: true
  collect_peer_data: true

mail:
  host: redacted
  port: redacted
  encryption: none
  from: "wireguard@example.com"

web:
  site_title: "Wireguard"
  site_company_name: "Example"
  external_url: http://wireguard.example.com
  request_logging: true

auth:
  callback_url_prefix: "http://wireguard.example.com/api/v0"
  ldap:                                                                                                                                                                                        
  - id: "ldap1"
    provider_name: "Example LDAP"
    display_name: "Login with LDAP"
    url: "ldaps://ldaps.example.com"
    start_tls: false
    cert_validation: false
    base_dn: "dc=example,dc=com"
    bind_user: "uid=wireguard_read_only,ou=people,dc=example,dc=com"
    bind_pass: "randomVerySecurePassword"
    login_filter: "(&(objectClass=person)(|(mail={{login_identifier}})(uid={{login_identifier}})))"
    admin_group: "cn=wireguard_admin,ou=groups,dc=example,dc=com" 
    synchronize: true
    disable_missing: true
    sync_filter: "(&(objectClass=person)(mail=*))"
    registration_enabled: true
    field_map: 
      user_identifier: "uid"
      email: "mail"
      memberof: "memberOf"
      firstname: "first_name"
      lastname: "last_name"
      phone: "mail"
      department: "last_name"

I'm unsure if this issue is a configuration issue on my end or an issue with how LDAP synchronization occurs behind the scenes. I took a quick look at user_manager.go, but based on the flow I would think that the users are synced properly. Not sure why I see the backend connection error when it appears to have been set up properly.

Any thoughts or suggestions as to the source of my issues?

valinet commented 1 month ago

+1

valinet commented 1 month ago

Actually, acording to this and this, ldap config should be under auth, something like this:

auth:
  ldap:
    - id: 1
      ...

But don't bother, that still doesn't work either.

captain-parzival commented 1 month ago

Actually, acording to this and this, ldap config should be under auth, something like this:

auth:
  ldap:
    - id: 1
      ...

But don't bother, that still doesn't work either.

Woops - formatting woes in github hid that line and the callback url. I updated the original comment.

valinet commented 1 month ago

Got it to work eventually myself as well, beware ldap_sync_interval is just for show, it's not mapped to anything in code - once you enable syncronize: true, the LDAP service is queried every 10 seconds as per https://github.com/h44z/wg-portal/blob/master/internal/app/auth/auth.go#L39. What a mess...

captain-parzival commented 1 month ago

How did you get it to work? What backend are you using for LDAP?

Would you be able to post your configuration? I'm hoping it can help me track down my issues.

valinet commented 1 month ago

Here's my working config for v2:

advanced:
  log_level: trace
  ldap_sync_interval: 15m

core:
  admin_user: admin@example.com
  admin_password: password
  create_default_peer: true
  create_default_peer_on_creation: false

web:
  external_url: https://example.com
  request_logging: true

auth:
  callback_url_prefix: https://example.com/api/v0
  ldap:
    - id: ldap1
      provider_name: company ldap
      display_name: Login with</br>LDAP
      url: ldap://example.com:389
      start_tls: true
      bind_user: cn=admin@example.com,ou=users,dc=example,dc=com
      bind_pass: password
      base_dn: ou=users,dc=example,dc=com
      login_filter: (&(objectClass=inetOrgPerson)(memberOf=cn=users_of_wgportal,ou=groups,dc=example,dc=com)(mail={{login_identifier}}))
      admin_group: cn=administrators,ou=users,dc=example,dc=com
      synchronize: false
      sync_filter: (&(objectClass=inetOrgPerson)(memberOf=cn=users_of_wgportal,ou=groups,dc=example,dc=com))
      registration_enabled: true
      field_map: 
        user_identifier: uid
        email: mail
        memberof: memberOf
        firstname: givenName
        lastname: sn
        phone: mobile
        department: title

Since ldap_sync_interval is useless in v2, and syncronization happens every 10 seconds for some insane reason (hardcoded), I have gone to using v1 which also has a hardcoded value, but at least on that it is 1 minute. Here's my config there (docker-compose.yml):

version: '3.6'
services:
  wg-portal:
    image: wgportal/wg-portal:v1
    container_name: wg-portal
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
    network_mode: "host"
    volumes:
      - /etc/wireguard:/etc/wireguard
      - ./data:/app/data
    ports:
      - '8123:8123'
    environment:
      # WireGuard Settings
      - WG_DEVICES=wg0,wg1
      - WG_DEFAULT_DEVICE=wg0
      - WG_CONFIG_PATH=/etc/wireguard
      - SELF_PROVISIONING=true
      # Core Settings
      - EXTERNAL_URL=https://example.com
      - LOGO_URL=https://example.com/logo.png
      - CREATE_DEFAULT_PEER=true
      - WEBSITE_TITLE=Example VPN
      - COMPANY_NAME=Example
      - ADMIN_USER=admin@example.com
      - ADMIN_PASS=password
      # Mail Settings
      - MAIL_FROM=Example VPN <vpn@example.com>
      - EMAIL_HOST=1.1.1.1
      - EMAIL_PORT=25
      # LDAP Settings
      - LDAP_ENABLED=true
      - LDAP_URL=ldap://example.com:389
      - LDAP_STARTTLS=true
      - LDAP_CERT_VALIDATION=false
      - LDAP_BASEDN=ou=users,dc=example,dc=com
      - LDAP_USER=cn=admin@example.com,ou=users,dc=example,dc=com
      - LDAP_PASSWORD=password
      - LDAP_LOGIN_FILTER=(&(objectClass=inetOrgPerson)(memberOf=cn=users_of_wgportal,ou=groups,dc=example,dc=com)(mail={{login_identifier}}))
      - LDAP_SYNC_FILTER=(&(objectClass=inetOrgPerson)(memberOf=cn=users_of_wgportal,ou=groups,dc=example,dc=com))
      - LDAP_SYNC_GROUP_FILTER=(&(objectClass=inetOrgPerson)(ou=groups,dc=example,dc=com))
      - LDAP_ADMIN_GROUP=cn=administrators,ou=groups,dc=example,dc=com
      - LDAP_ATTR_EMAIL=mail
      - LDAP_ATTR_FIRSTNAME=givenName
      - LDAP_ATTR_LASTNAME=sn
      - LDAP_ATTR_PHONE=mobile
      - LDAP_ATTR_GROUPS=memberOf
      - LDAP_CERT_CONN=false
      # Log
      - LOG_LEVEL=trace

Although, to be perfectly honest with you, I don't like the interface of this project at all - it is too complicated for the casual users; I will probably go back to a custom patched wg-easy with an external authenticator. Here is the patch to wg-easy to support the Remote-User header from Authelia (it's in a comment on YouTube):

@WolfgangsChannel Managed to pull it off. It's rather simple actually. The changes are as follows:

  1. In file ansible-easy-vpn/roles/bunkerweb/templates/env.j2, add {{ wireguard_host }}_REVERSE_PROXY_HEADERS=Remote-User $user on the last line.

  2. docker exec -it wg-easy apk add nano.

  3. docker exec -it wg-easy nano lib/Server.js

  4. Ctrl + W, type return WireGuard.getClients(, Enter. Replace the line with return WireGuard.getClients(req.header("Remote-User"));.

  5. Ctrl + W, type return WireGuard.createClient(, Enter. 2 lines above (where function starts), add [req.body.name](javascript:void(0);) = req.header("Remote-User") + "_" + [req.body.name](javascript:void(0););.

  6. Ctrl + W, type return WireGuard.updateClientName(, Enter. 3 lines above (where function starts), add [req.body.name](javascript:void(0);) = req.header("Remote-User") + "_" + [req.body.name](javascript:void(0););.

  7. Ctrl+X, y, Enter to exit nano.

  8. docker exec -it wg-easy nano lib/WireGuard.js

  9. Ctrl + W, type async getClients(remote_user) {. In this method, replace lines 2 and 4 with this, respectively:

    • const clients = Object.entries(config.clients).filter(([clientId, client]) => client.name.startsWith(remote_user + "_")).map(([clientId, client]) => ({
    • name: client.name.substring((remote_user + "_").length, client.name.length),
  10. docker exec -it wg-easy nano www/index.html

  11. Ctrl+W, type <span class="text-sm">New</span>, Enter. After 2 lines, where the div closes, add this:

        <div class="flex-shrink-0">
          <button @click="location.replace('[https://auth.example.com/logout?rd=https%3A%2F%2Fwg.example.com%2F');](javascript:void(0);)"
            class="hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 border-2 border-gray-100 py-2 px-4 rounded inline-flex items-center transition">
            <svg style="transform-box: fill-box; transform-origin: center; transform: rotate(45deg);" class="w-4 mr-2" inline xmlns="[http://www.w3.org/2000/svg](javascript:void(0);)" fill="none" viewBox="0 0 24 24"
              stroke="currentColor">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
            </svg>
            <span class="text-sm">Log out</span>
          </button>
        </div>
  12. cd ansible-easy-vpn/

  13. ansible-playbook run.yml, enter valut password.

  14. reboot

This patch has the effect that the headers sent by Authelia always contain the Remote-User field populated with the LDAP uid of the currently logged in user. On the wg-easy side, what I did was to prefix config files with "username_", and then filter the returned configs to only include those belonging to the currently logged in user. Finally, steps 10-11 add a "Log out" button to the "wg-easy" web page, so users can easily log out. Remember to replace example.com with your actual domain.

I am a bit amazed by the lack of proper options in this area of WireGuard frontend, but I always keep looking since the protocol in itself is SO GOOD compared to anything else I have used.