Repository: jkroepke/openvpn-auth-oauth2
Pull request #308 (2024-08-18 22:49:35)

chore(deps): bump dependencies
#### What this PR does / why we need it

#### Which issue this PR fixes

*(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close that issue when PR gets merged)*: fixes #

- fixes #

#### Special notes for your reviewer

#### Particularly user-facing changes

#### Checklist

Complete these before marking the PR as `ready to review`:

<!-- [Place an '[x]' (no spaces) in all applicable fields.] -->

- [ ] [DCO](https://github.com/prometheus-community/helm-charts/blob/main/CONTRIBUTING.md#sign-off-your-work) signed
- [ ] The PR title has a summary of the changes
- [ ] The PR body has a summary to reflect any significant (and particularly user-facing) changes introduced by this PR



Patch:
diff --git a/.editorconfig b/.editorconfig
index 9c7031b..e0c8846 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -14,4 +14,7 @@ indent_style = tab
 indent_size = 2
 
 [*.sh]
-indent_size = 2
\ No newline at end of file
+indent_size = 2
+
+[*.md]
+indent_size = 4
\ No newline at end of file
diff --git a/cmd/daemon/root.go b/cmd/daemon/root.go
index ffe55ae..e3881b5 100644
--- a/cmd/daemon/root.go
+++ b/cmd/daemon/root.go
@@ -92,7 +92,7 @@ func Execute(args []string, logWriter io.Writer, version, commit, date string) i
 		}()
 	}
 
-	server := httpserver.NewHTTPServer("server", logger, conf.HTTP, oauth2Client.Handler())
+	server := httpserver.NewHTTPServer(httpserver.ServerNameDefault, logger, conf.HTTP, oauth2Client.Handler())
 
 	wg.Add(1)
 
@@ -167,7 +167,7 @@ func setupDebugListener(ctx context.Context, logger *slog.Logger, conf config.Co
 	mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
 	mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
 
-	server := httpserver.NewHTTPServer("debug", logger, config.HTTP{Listen: conf.Debug.Listen}, mux)
+	server := httpserver.NewHTTPServer(httpserver.ServerNameDebug, logger, config.HTTP{Listen: conf.Debug.Listen}, mux)
 
 	err := server.Listen(ctx)
 	if err != nil {
diff --git a/docs/Configuration.md b/docs/Configuration.md
index 04bed3c..1027683 100644
--- a/docs/Configuration.md
+++ b/docs/Configuration.md
@@ -1,25 +1,8 @@
 # Configuration
 
-The preferred way to configure openvpn-auth-oauth2 is via environment variables. If you install the openvpn-auth-auth2 via
-Linux package, use the file `/etc/sysconfig/openvpn-auth-oauth2` to configure openvpn-auth-oauth2.
-
-## Full configuration example
-
-Configuration openvpn-auth-oauth2 for [zitadel](https://zitadel.com/)
-
-```ini
-# Define the public http endpoint here.
-CONFIG_HTTP_BASEURL=http://<vpn>:9000/
-CONFIG_HTTP_LISTEN=:9000
-# Define a random value with 16 or 24 characters
-CONFIG_HTTP_SECRET=1jd93h5b6s82lf03jh5b2hf9
-CONFIG_OPENVPN_ADDR=unix:///run/openvpn/server.sock
-CONFIG_OPENVPN_PASSWORD=<password from /etc/openvpn/password.txt>
-CONFIG_OAUTH2_ISSUER=https://company.zitadel.cloud
-CONFIG_OAUTH2_SCOPES=openid profile email offline_access
-CONFIG_OAUTH2_CLIENT_ID=34372461928374612@any
-CONFIG_OAUTH2_CLIENT_SECRET=ASDhjgadjhAUYSDGjkhasgdIATWDGJHASDtiwGDJAHSGDutwqdygASJKD12hfva
-```
+To configure openvpn-auth-oauth2, the recommended approach uses a YAML file.
+If installed through a Linux package,
+the file `/etc/sysconfig/openvpn-auth-oauth2` allows configuration of openvpn-auth-oauth2 through environment variables.
 
 ## Configuration file
 
@@ -30,79 +13,79 @@ openvpn-auth-oauth2 supports configuration via a YAML file. The file can be pass
 
 ```yaml
 debug:
-    pprof: false
-    listen: :9001
+  pprof: false
+  listen: ":9001"
 http:
-    assets-path: "" # Example: "/etc/openvpn-auth-oauth2/assets/"
-    baseurl: "http://localhost:9000/"
-    cert: ""
-    check:
-        ipaddr: false
-    enable-proxy-headers: true
-    key: ""
-    listen: ":9000"
-    # secret: ""
-    # template: "" # Path to a HTML file which is displayed at the end of the screen
-    tls: false
+  assets-path: "" # Example: "/etc/openvpn-auth-oauth2/assets/"
+  baseurl: "http://localhost:9000/"
+  cert: ""
+  check:
+    ipaddr: false
+  enable-proxy-headers: true
+  key: ""
+  listen: ":9000"
+  # secret: ""
+  # template: "" # Path to a HTML file which is displayed at the end of the screen
+  tls: false
 log:
-    format: console
-    level: INFO
-    vpn-client-ip: true
+  format: console
+  level: INFO
+  vpn-client-ip: true
 oauth2:
-    authorize-params: "a=c"
-    client:
-        id: "test"
-        secret: "test"
-    endpoint:
-        # discovery: "https://idp/.well-known/openid-configuration"
-        # auth: "https://idp/oauth/auth"
-        # token: "https://idp/oauth/token"
-    issuer: "https://idp"
-    # provider: "generic"
-    # scopes:
-    #  - "openid"
-    #  - "profile"
-    validate:
-        acr:
-        #  - "phr"
-        #  - "phrh"
-        common-name: ""
-        common-name-case-sensitive: false
-        # groups:
-        #  - "test"
-        #  - "test2"
-        # roles:
-        #   - "test"
-        #   - "test2"
-        ipaddr: false
-        issuer: true
-    nonce: true
-    pkce: true
-    auth-style: "AuthStyleInParams"
-    refresh:
-        enabled: false
-        expires: 8h0m0s
-        # secret: ""
-        use-session-id: false
-        validate-user: true
+  authorize-params: "a=c"
+  client:
+    id: "test"
+    secret: "test"
+  endpoint:
+  # discovery: "https://idp/.well-known/openid-configuration"
+  # auth: "https://idp/oauth/auth"
+  # token: "https://idp/oauth/token"
+  issuer: "https://idp"
+  # provider: "generic"
+  # scopes:
+  #  - "openid"
+  #  - "profile"
+  validate:
+    acr:
+    #  - "phr"
+    #  - "phrh"
+    common-name: ""
+    common-name-case-sensitive: false
+    # groups:
+    #  - "test"
+    #  - "test2"
+    # roles:
+    #   - "test"
+    #   - "test2"
+    ipaddr: false
+    issuer: true
+  nonce: true
+  pkce: true
+  auth-style: "AuthStyleInParams"
+  refresh:
+    enabled: false
+    expires: 8h0m0s
+    # secret: ""
+    use-session-id: false
+    validate-user: true
 openvpn:
-    addr: "unix:///run/openvpn/server.sock"
-    auth-token-user: false
-    auth-pending-timeout: 2m
-    bypass:
-        # common-names:
-        # - "test"
-        # - "test2"
-    common-name:
-        environment-variable-name: common_name
-        mode: plain
+  addr: "unix:///run/openvpn/server.sock"
+  auth-token-user: false
+  auth-pending-timeout: 2m
+  bypass:
+  # common-names:
+  # - "test"
+  # - "test2"
+  common-name:
+    environment-variable-name: common_name
+    mode: plain
+  # password: ""
+  pass-through:
+    address: "unix:///run/openvpn/pass-through.sock"
+    enabled: false
     # password: ""
-    pass-through:
-        address: "unix:///run/openvpn/pass-through.sock"
-        enabled: false
-        # password: ""
-        # socket-group: ""
-        # socket-mode: 660
+    # socket-group: ""
+    # socket-mode: 660
 ```
 </details>
 
@@ -221,7 +204,7 @@ Usage of openvpn-auth-oauth2:
 ```
 <!-- END USAGE -->
 
-## Read sensitive data from file
+## Read sensitive data from a file
 
 The following parameter supports sensitive data from the file:
 
@@ -232,35 +215,40 @@ The following parameter supports sensitive data from the file:
 
 To read the sensitive data from the file, use the `file://` prefix, e.g. `file://path/to/secret.txt`.
 
-## Configuration openvpn-auth-oauth2
+### openvpn-auth-oauth2 config
 
-openvpn-auth-oauth2 starts an HTTP listener which needs to be accessible from the OpenVPN client before the VPN connection is established.
-By default, the http listener runs on `:9000`.
+openvpn-auth-oauth2 starts an HTTP listener that the OpenVPN client must access before establishing the VPN connection.
+By default, the HTTP listener operates on `:9000`.
 
-It'd highly recommend putting openvpn-auth-oauth2 behind a reverse proxy which terminates the TLS connections.
-It's important to configure `CONFIG_HTTP_BASE_URL` because openvpn-auth-oauth2 need to know the redirect url.
+It is highly recommended to place openvpn-auth-oauth2 behind a reverse proxy terminates the TLS connections.
+Configuring `CONFIG_HTTP_BASE_URL` remains crucial because openvpn-auth-oauth2 needs to know the redirect URL.
 
 Example:
 
+<table>
+<thead><tr><td>env/sysconfig configuration</td></tr></thead>
+<tbody><tr><td>
+
 ```ini
 # openvpn-auth-oauth2 config file
 CONFIG_HTTP_LISTEN=:9000
 CONFIG_HTTP_BASE_URL=https://login.example.com
 ```
+</td></tr></tbody>
+<thead><tr><td>yaml configuration</td></tr></thead>
+<tbody><tr><td>
 
-### Filesystem Permissions
-
-When started by systemd, openvpn runs with a [dynamic arbitrary UID](https://0pointer.net/blog/dynamic-users-with-systemd.html).
-This means that it may not have access to certain files and directories if the appropriate permissions are not set.
+```yaml
+http:
+  listen: ":9000"
+  baseurl: "https://login.example.com"
+```
+</td></tr></tbody>
+</table>
 
-Any additional files, such as TLS keys, should be placed under the `/etc/openvpn-auth-oauth2/` directory.
-The owner of these files should be `root` and the group should be `openvpn-auth-oauth2`.
-This ensures that openvpn has the necessary permissions to access and use these files.
+### Filesystem Permissions
 
-When installing the openvpn-auth-oauth2 Linux package,
-it will automatically handle the creation of the openvpn-auth-oauth2 system group.
-This group is used to manage access to the necessary files and directories
-and should be used to manage the permissions of any additional files.
+See [Filesystem Permissions](Filesystem%20Permissions) for more information.
 
 ## Setup OpenVPN server
 
@@ -282,60 +270,42 @@ auth-user-pass-optional
 
 # Enable auth-token-gen to allow non-interactive session refresh
 # Mandatory for mobile devices, because auth-token works across disconnects
-# The lifetime of the token must be the same as the refresh token in openvpn-auth-oauth2
+# The lifetime of the token must be the same as the refresh token in openvpn-auth-oauth2.
+# The token can't be extended after it has been generated. The lifetime must be the maximum lifetime of a VPN session.
 # 8 hours = 28800 seconds
 auth-gen-token 28800 external-auth
 ```
 
+### openvpn-auth-oauth2 config
+
+<table>
+<thead><tr><td>env/sysconfig configuration</td></tr></thead>
+<tbody><tr><td>
+
 ```ini
 # openvpn-auth-oauth2 config file
 CONFIG_OPENVPN_ADDR=unix:///run/openvpn/server.sock
 CONFIG_OPENVPN_PASSWORD=<password>
 ```
+</td></tr></tbody>
+<thead><tr><td>yaml configuration</td></tr></thead>
+<tbody><tr><td>
 
-## Setup OIDC Provider
-
-See [Providers](Providers) for more information
-
-## HTTPS Listener
-
-> [!IMPORTANT]
-> Remember to set `CONFIG_HTTP_BASEURL` correctly. It should start with `https://` following your public domain name plus port.
-
-Some SSO Provider like Entra ID requires `https://` based redirect URL.
-In the default configuration, openvpn-auth-oauth2 listen on `http://`.
-There are two common ways to set up an HTTPS listener
-
-### Reverse proxy (nginx, traefik)
-
-You can use one of your favorite http reverse proxies.
-Configure HTTPS on reverse proxy and proxy to an HTTP instance of openvpn-auth-oauth2.
-For beginners, [traefik](https://traefik.io/traefik/) is recommended since it [natively](https://doc.traefik.io/traefik/https/acme/)
-supports [Let's Encrypt](https://letsencrypt.org/) where you can get public SSL certificates for free.
-
-### Using native HTTPS support
-
-openvpn-auth-oauth2 supports HTTPS out of the box.
-
-```ini
-CONFIG_HTTP_TLS=true
-CONFIG_HTTP_KEY=server.key
-CONFIG_HTTP_CERT=server.crt
+```yaml
+openvpn:
+  addr: "unix:///run/openvpn/server.sock"
+  password: "<password>"
 ```
+</td></tr></tbody>
+</table>
 
-To set up a self-signed certificate, you can use the command below:
+## Setup OIDC Provider
 
-```bash
-export DOMAIN_NAME=vpn.example.com
-openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes -keyout server.key -out server.crt -subj "/CN=$DOMAIN_NAME" -addext "subjectAltName=DNS:$DOMAIN_NAME"
-```
+See [Providers](Providers) for more information.
 
-You can also use [Let's Encrypt](https://letsencrypt.org/) to get public SSL certificates for free.
-The [certbot](https://certbot.eff.org/instructions) is a recommended tool to get SSL certificates.
-Alternatively,
-you can use [acme.sh](https://acme.sh/), which is a pure Unix shell script implementing ACME client protocol.
+## HTTPS Listener
 
-openvpn-auth-oauth2 requires a [`SIGHUP` signal](https://en.wikipedia.org/wiki/SIGHUP) to reload the TLS certificate.
+See [HTTPS Listener](HTTPS%20Listener) for more information.
 
 ## Custom Login Templates
 
@@ -343,71 +313,4 @@ See [Layout Customization](Layout%20Customization) for more information
 
 ## Non-interactive session refresh
 
-By default, `openvpn-auth-oauth2` doesn't store user tokens.
-This means users must log in interactively each time they authenticate, including during TLS soft-resets
-(triggered by `reneg-sec`).
-
-However, you can change this behavior by enabling the `oauth2.refresh.enabled=true` setting.
-This allows `openvpn-auth-oauth2` to store either the connection ID or SessionID (`oauth2.refresh.use-session-id=true`),
-accepting connections without additional login checks.
-
-When `oauth2.refresh.validate-user=true` is set, `openvpn-auth-oauth2`
-requests a [refresh token](https://auth0.com/blog/refresh-tokens-what-are-they-and-when-to-use-them/)
-during the initial connection and stores it.
-
-The refresh tokens are stored in an in-memory key-value store and encrypted using AES.
-Each token is tied to either the OpenVPN client ID or OpenVPN session ID.
-
-If a non-interactive login attempt with the refresh token fails against the OIDC provider,
-the system reverts to an interactive login process.
-
-References:
-
-- https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#refresh-the-access-token
-- https://curity.io/resources/learn/oauth-refresh/
-- https://developer.okta.com/docs/guides/refresh-tokens/main/
-
-Here is the corrected version of your text:
-
-### Security Considerations
-
-If `oauth2.refresh.validate-user` is set to `false`, a refresh token is not requested and validated against the OIDC
-provider.
-openvpn-auth-oauth2 assumes the user is still valid and allows the user to connect without further validation.
-
-Example: If the user opens a VPN connection and the user is deleted from the OIDC provider, the connection remains valid
-until the connection lifetime is reached. Restarting the OpenVPN server will invalidate the connection unless
-[non-interactive session refresh across disconnects](#non-interactive-session-refresh-across-disconnects) is configured.
-
-### Non-interactive session refresh across disconnects
-
-To facilitate non-interactive session refresh across disconnects,
-you must enable `auth-token-gen [lifetime] external-auth` on the OpenVPN server.
-
-- `[lifetime]` represents the duration of the token in seconds.
-  Once generated, the token's lifetime cannot be extended.
-  For instance, setting the lifetime to 8 hours means
-  the client will disconnect after 8 hours from the initial authentication and will need to re-authenticate.
-
-- Setting the lifetime to 0 disables the lifetime check,
-  which can be beneficial for mobile devices with unstable connections or during device sleep cycles.
-
-If `auth-gen-token-secret [keyfile]` is configured, OpenVPN access server restarts can verify auth-tokens.
-To generate a new secret, utilize `openvpn --genkey auth-token [keyfile]`.
-
-**Note**:
-Keep the keyfile secret
-as anyone with access to it can generate auth tokens that the OpenVPN server will recognize as valid.
-It's crucial to safeguard this file on the server.
-
-References:
-
-- https://openvpn.net/community-resources/reference-manual-for-openvpn-2-6/#server-options
-
-```ini
-CONFIG_OAUTH2_REFRESH_ENABLED=true
-CONFIG_OAUTH2_REFRESH_EXPIRES=8h
-CONFIG_OAUTH2_REFRESH_SECRET= # a static secret to encrypt token. Must be 16, 24 or 32
-CONFIG_OAUTH2_REFRESH_USE__SESSION__ID=true
-CONFIG_OPENVPN_AUTH__TOKEN__USER=true
-```
+See [Non-interactive session refresh](Non-interactive%20session%20refresh) for more information.
diff --git a/docs/Debugging Errors.md b/docs/Debugging Errors.md
index c3af58d..6df25c5 100644
--- a/docs/Debugging Errors.md
+++ b/docs/Debugging Errors.md
@@ -40,20 +40,21 @@ I assume you are using a Chrome-based Browser.
 Sometimes the client stuck at this stage `Received control message: 'PUSH_REQUEST'` and can't connect, and at the end it will time out like this message `Connection Timeout`, and you can see in the logs like this:
 
 ```
-⏎[Feb 14, 2024, 22:28:57] Session is ACTIVE
-⏎[Feb 14, 2024, 22:28:57] EVENT: GET_CONFIG ⏎[Feb 14, 2024, 22:28:57] Sending PUSH_REQUEST to server...
-⏎[Feb 14, 2024, 22:28:58] Sending PUSH_REQUEST to server...
-⏎[Feb 14, 2024, 22:29:00] Sending PUSH_REQUEST to server...
-⏎[Feb 14, 2024, 22:29:03] Sending PUSH_REQUEST to server...
-⏎[Feb 14, 2024, 22:29:06] Sending PUSH_REQUEST to server...
-⏎[Feb 14, 2024, 22:29:09] Sending PUSH_REQUEST to server...
-⏎[Feb 14, 2024, 22:29:12] Sending PUSH_REQUEST to server...
-⏎[Feb 14, 2024, 22:29:15] Sending PUSH_REQUEST to server...
-⏎[Feb 14, 2024, 22:29:18] Sending PUSH_REQUEST to server...
-⏎[Feb 14, 2024, 22:29:21] Sending PUSH_REQUEST to server...
-⏎[Feb 14, 2024, 22:29:24] Sending PUSH_REQUEST to server...
-⏎[Feb 14, 2024, 22:29:27] Sending PUSH_REQUEST to server...
-⏎[Feb 14, 2024, 22:29:29] Raw stats on disconnect:
+[Feb 14, 2024, 22:28:57] Session is ACTIVE
+[Feb 14, 2024, 22:28:57] EVENT: GET_CONFIG
+[Feb 14, 2024, 22:28:57] Sending PUSH_REQUEST to server...
+[Feb 14, 2024, 22:28:58] Sending PUSH_REQUEST to server...
+[Feb 14, 2024, 22:29:00] Sending PUSH_REQUEST to server...
+[Feb 14, 2024, 22:29:03] Sending PUSH_REQUEST to server...
+[Feb 14, 2024, 22:29:06] Sending PUSH_REQUEST to server...
+[Feb 14, 2024, 22:29:09] Sending PUSH_REQUEST to server...
+[Feb 14, 2024, 22:29:12] Sending PUSH_REQUEST to server...
+[Feb 14, 2024, 22:29:15] Sending PUSH_REQUEST to server...
+[Feb 14, 2024, 22:29:18] Sending PUSH_REQUEST to server...
+[Feb 14, 2024, 22:29:21] Sending PUSH_REQUEST to server...
+[Feb 14, 2024, 22:29:24] Sending PUSH_REQUEST to server...
+[Feb 14, 2024, 22:29:27] Sending PUSH_REQUEST to server...
+[Feb 14, 2024, 22:29:29] Raw stats on disconnect:
  BYTES_IN : 3483
  BYTES_OUT : 4088
  PACKETS_IN : 18
@@ -63,11 +64,28 @@ Sometimes the client stuck at this stage `Received control message: 'PUSH_REQUES
 If you see this message on the Client side (version3),
 you may want to make sure you add these lines to your oauth configuration file
 
+<table>
+<thead><tr><td>env/sysconfig configuration</td></tr></thead>
+<tbody><tr><td>
+
 ```ini
 CONFIG_OAUTH2_REFRESH_ENABLED=true
 CONFIG_OAUTH2_REFRESH_EXPIRES=24h
-CONFIG_OAUTH2_REFRESH_SECRET= # 16 or 24 characters
+CONFIG_OAUTH2_REFRESH_SECRET=... # 16 or 24 characters
+```
+</td></tr></tbody>
+<thead><tr><td>yaml configuration</td></tr></thead>
+<tbody><tr><td>
+
+```yaml
+oauth2:
+  refresh:
+    enabled: true
+    expires: 24h
+    secret: "..." # 16 or 24 characters
 ```
+</td></tr></tbody>
+</table>
 
 For reference, you can read more about these properties in the wiki at this [link](https://github.com/jkroepke/openvpn-auth-oauth2/wiki/Configuration#non-interactive-session-refresh)
 
@@ -83,11 +101,30 @@ auth-token-gen [lifetime] external-auth
 ```
 
 **openvpn-auth-oauth2 Configuration**
+
+<table>
+<thead><tr><td>env/sysconfig configuration</td></tr></thead>
+<tbody><tr><td>
+
 ```ini
 CONFIG_OAUTH2_REFRESH_ENABLED=true
 CONFIG_OAUTH2_REFRESH_EXPIRES=8h
-CONFIG_OAUTH2_REFRESH_SECRET= # a static secret to encrypt token. Must be 16, 24 or 32
+CONFIG_OAUTH2_REFRESH_SECRET=... # a static secret to encrypt token. Must be 16, 24 or 32
 CONFIG_OAUTH2_REFRESH_USE__SESSION__ID=true
 ```
+</td></tr></tbody>
+<thead><tr><td>yaml configuration</td></tr></thead>
+<tbody><tr><td>
+
+```yaml
+oauth2:
+  refresh:
+    enabled: true
+    expires: 8h
+    secret: "..." # 16 or 24 characters
+    use-session-id: true
+```
+</td></tr></tbody>
+</table>
 
 For reference, you can read more about these properties in the wiki at this [link](https://github.com/jkroepke/openvpn-auth-oauth2/wiki/Configuration#non-interactive-session-refresh)
diff --git a/docs/Filesystem Permissions.md b/docs/Filesystem Permissions.md
new file mode 100644
index 0000000..ec99257
--- /dev/null
+++ b/docs/Filesystem Permissions.md
@@ -0,0 +1,12 @@
+# Filesystem Permissions
+
+When started by systemd, openvpn runs with a [dynamic arbitrary UID](https://0pointer.net/blog/dynamic-users-with-systemd.html).
+This means that it may not have access to certain files and directories if the appropriate permissions are not set.
+
+Any additional files, such as TLS keys, should reside in the `/etc/openvpn-auth-oauth2/` directory.
+The ownership of these files should be set to `root`, and the group should be set to `openvpn-auth-oauth2`.
+This setup ensures that openvpn has the necessary permissions to access and utilize these files.
+
+When installing the openvpn-auth-oauth2 Linux package, the system automatically creates the openvpn-auth-oauth2 group.
+This group manages access to the necessary files and directories
+and should be used to control the permissions of any additional files.
diff --git a/docs/HTTPS Listener.md b/docs/HTTPS Listener.md
new file mode 100644
index 0000000..2fd02b3
--- /dev/null
+++ b/docs/HTTPS Listener.md
@@ -0,0 +1,87 @@
+# HTTPS Listener
+
+> [!IMPORTANT]
+> Remember to set `CONFIG_HTTP_BASEURL` correctly. It should start with `https://` following your public domain name plus port.
+
+Some SSO Provider like Entra ID requires `https://` based redirect URL.
+In the default configuration, openvpn-auth-oauth2 listen on `http://`.
+There are two common ways to set up an HTTPS listener
+
+# Reverse proxy (nginx, traefik)
+
+You can use one of your favorite http reverse proxies.
+Configure HTTPS on reverse proxy and proxy to an HTTP instance of openvpn-auth-oauth2.
+For beginners, [traefik](https://traefik.io/traefik/) is recommended since it [natively](https://doc.traefik.io/traefik/https/acme/)
+supports [Let's Encrypt](https://letsencrypt.org/) where you can get public SSL certificates for free.
+
+# Using native HTTPS support
+
+openvpn-auth-oauth2 supports HTTPS out of the box.
+If openvpn-auth-oauth2 runs as systemd service, the HTTPS certificates must place in `/etc/openvpn-auth-oauth2/` with
+the owner `root` and the group `openvpn-auth-oauth2`. See [Filesystem Permissions](Filesystem%20Permissions) for more information.
+
+<table>
+<thead><tr><td>env/sysconfig configuration</td></tr></thead>
+<tbody><tr><td>
+
+```ini
+CONFIG_HTTP_TLS=true
+CONFIG_HTTP_KEY=/etc/openvpn-auth-oauth2/server.key
+CONFIG_HTTP_CERT=/etc/openvpn-auth-oauth2/server.crt
+```
+</td></tr></tbody>
+<thead><tr><td>yaml configuration</td></tr></thead>
+<tbody><tr><td>
+
+```yaml
+http:
+  tls: true
+  key: /etc/openvpn-auth-oauth2/server.key
+  cert: /etc/openvpn-auth-oauth2/server.crt
+```
+</td></tr></tbody>
+</table>
+
+## Self-signed certificate
+
+To set up a self-signed certificate, you can use the command below:
+
+```bash
+export DOMAIN_NAME=vpn.example.com
+openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \
+  -keyout /etc/openvpn-auth-oauth2/server.key \
+  -out /etc/openvpn-auth-oauth2/server.crt \
+  -subj "/CN=$DOMAIN_NAME" -addext "subjectAltName=DNS:$DOMAIN_NAME"
+chown root:openvpn-auth-oauth2 /etc/openvpn-auth-oauth2/server.key /etc/openvpn-auth-oauth2/server.crt
+chmod 640 /etc/openvpn-auth-oauth2/server.key /etc/openvpn-auth-oauth2/server.crt
+```
+
+You can also use [Let's Encrypt](https://letsencrypt.org/) to get public SSL certificates for free.
+The [certbot](https://certbot.eff.org/instructions) is a recommended tool to get SSL certificates.
+Alternatively, can use [acme.sh](https://acme.sh/), which is a pure Unix shell script implementing ACME client protocol.
+
+openvpn-auth-oauth2 requires a [`SIGHUP` signal](https://en.wikipedia.org/wiki/SIGHUP) to reload the TLS certificate.
+
+If you are using certbot, please drop some instructions to setup it.
+
+## Run HTTPS listener on 443 port
+
+Running openvpn-auth-oauth2 on port 443 requires special permissions.
+
+Create a new file `/etc/systemd/system/openvpn-auth-oauth2.service.d/override.conf` with the following content:
+
+```ini
+[Service]
+AmbientCapabilities=CAP_NET_BIND_SERVICE
+CapabilityBoundingSet=CAP_NET_BIND_SERVICE
+PrivateUsers=false
+```
+
+Then, run the following commands:
+
+```bash
+echo "capability net_bind_service," > /etc/apparmor.d/local/usr.bin.openvpn-auth-oauth2
+systemctl restart apparmor
+systemctl daemon-reload
+systemctl restart openvpn-auth-oauth2
+```
diff --git a/docs/Management Interface pass-through.md b/docs/Management Interface pass-through.md
index 0d90a80..e15bfe1 100644
--- a/docs/Management Interface pass-through.md
+++ b/docs/Management Interface pass-through.md
@@ -8,21 +8,46 @@ openvpn-auth-oauth2 acts as a middleman, forwarding commands and responses betwe
 
 ## Configuration
 
-You can configure the pass-through feature via the command line interface (CLI) using the following options:
+You can configure the pass-through feature  using the following options:
 
-```bash
---openvpn.pass-through.enabled
---openvpn.pass-through.address
---openvpn.pass-through.password
---openvpn.pass-through.socket-group
---openvpn.pass-through.socket-mode
-```
-
-For example:
+<table>
+<thead><tr><td>CLI configuration</td></tr></thead>
+<tbody><tr><td>
 
 ```bash
-openvpn-auth-oauth2 --openvpn.pass-through.enabled=true --openvpn.pass-through.address=unix:///run/openvpn/pass-through.sock
+openvpn-auth-oauth2 \
+  --openvpn.pass-through.enabled=true \
+  --openvpn.pass-through.address=unix:///run/openvpn/pass-through.sock  \
+  --openvpn.pass-through.password=secret
+  # --openvpn.pass-through.socket-group=openvpn-auth-oauth2 # optional
+  # --openvpn.pass-through.socket-mode=0660 # optional
+```
+</td></tr></tbody>
+<thead><tr><td>env/sysconfig configuration</td></tr></thead>
+<tbody><tr><td>
+
+```ini
+CONFIG_OPENVPN_PASS__THROUGH_ENABLED=true
+CONFIG_OPENVPN_PASS__THROUGH_ADDRESS=unix:///run/openvpn/pass-through.sock
+CONFIG_OPENVPN_PASS__THROUGH_PASSWORD=secret
+# CONFIG_OPENVPN_PASS__THROUGH_SOCKET__GROUP=openvpn-auth-oauth2 # optional
+# CONFIG_OPENVPN_PASS__THROUGH_SOCKET__MODE=0660 # optional
+```
+</td></tr></tbody>
+<thead><tr><td>yaml configuration</td></tr></thead>
+<tbody><tr><td>
+
+```yaml
+openvpn:
+  pass-through:
+    enabled: true
+    address: "unix:///run/openvpn/pass-through.sock"
+    password: "secret"
+    #socket-group: "openvpn-auth-oauth2" # optional
+    #socket-mode: 660 # optional
 ```
+</td></tr></tbody>
+</table>
 
 ## Command Filtering
 
diff --git a/docs/Non-interactive session refresh.md b/docs/Non-interactive session refresh.md
new file mode 100644
index 0000000..acfc459
--- /dev/null
+++ b/docs/Non-interactive session refresh.md
@@ -0,0 +1,94 @@
+# Non-interactive session refresh
+
+By default, `openvpn-auth-oauth2` doesn't store user tokens.
+This means users must log in interactively each time they authenticate, including during TLS soft-resets
+(triggered by `reneg-sec`).
+
+However, you can change this behavior by enabling the `oauth2.refresh.enabled=true` setting.
+This allows `openvpn-auth-oauth2` to store either the connection ID or SessionID (`oauth2.refresh.use-session-id=true`),
+accepting connections without additional login checks.
+
+When `oauth2.refresh.validate-user=true` is set, `openvpn-auth-oauth2`
+requests a [refresh token](https://auth0.com/blog/refresh-tokens-what-are-they-and-when-to-use-them/)
+during the initial connection and stores it.
+
+The refresh tokens are stored in an in-memory key-value store and encrypted using AES.
+Each token is tied to either the OpenVPN client ID or OpenVPN session ID.
+
+If a non-interactive login attempt with the refresh token fails against the OIDC provider,
+the system reverts to an interactive login process.
+
+References:
+
+- https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#refresh-the-access-token
+- https://curity.io/resources/learn/oauth-refresh/
+- https://developer.okta.com/docs/guides/refresh-tokens/main/
+
+Here is the corrected version of your text:
+
+# Security Considerations
+
+If `oauth2.refresh.validate-user` is set to `false`, a refresh token is not requested and validated against the OIDC
+provider.
+openvpn-auth-oauth2 assumes the user is still valid and allows the user to connect without further validation.
+
+Example: If the user opens a VPN connection and the user is deleted from the OIDC provider, the connection remains valid
+until the connection lifetime is reached. Restarting the OpenVPN server will invalidate the connection unless
+[non-interactive session refresh across disconnects](#non-interactive-session-refresh-across-disconnects) is configured.
+
+# Non-interactive session refresh across disconnects
+
+To facilitate non-interactive session refresh across disconnects,
+you must enable `auth-token-gen [lifetime] external-auth` on the OpenVPN server.
+
+- `[lifetime]` represents the duration of the token in seconds.
+  Once generated, the token's lifetime cannot be extended.
+  It must consider as maximum lifetime of an VPN session.
+  For instance, setting the lifetime to 8 hours means
+  the client will disconnect after 8 hours from the initial authentication and will need to re-authenticate.
+
+- Setting the lifetime to 0 disables the lifetime check,
+  which can be beneficial for mobile devices with unstable connections or during device sleep cycles.
+
+If `auth-gen-token-secret [keyfile]` is configured, OpenVPN access server restarts can verify auth-tokens.
+To generate a new secret, utilize `openvpn --genkey auth-token [keyfile]`.
+
+**Note**:
+Keep the keyfile secret
+as anyone with access to it can generate auth tokens that the OpenVPN server will recognize as valid.
+It's crucial to safeguard this file on the server.
+
+References:
+
+- https://openvpn.net/community-resources/reference-manual-for-openvpn-2-6/#server-options
+
+
+## openvpn-auth-oauth2 config
+
+<table>
+<thead><tr><td>env/sysconfig configuration</td></tr></thead>
+<tbody><tr><td>
+
+```ini
+CONFIG_OAUTH2_REFRESH_ENABLED=true
+CONFIG_OAUTH2_REFRESH_EXPIRES=8h
+CONFIG_OAUTH2_REFRESH_SECRET= # a static secret to encrypt token. Must be 16, 24 or 32
+CONFIG_OAUTH2_REFRESH_USE__SESSION__ID=true
+CONFIG_OPENVPN_AUTH__TOKEN__USER=true
+```
+</td></tr></tbody>
+<thead><tr><td>yaml configuration</td></tr></thead>
+<tbody><tr><td>
+
+```yaml
+oauth2:
+  refresh:
+    enabled: true
+    expires: 8h
+    secret: "..." # 16 or 24 characters
+    use-session-id: true
+openvpn:
+  auth-token-user: true
+```
+</td></tr></tbody>
+</table>
diff --git a/docs/Providers.md b/docs/Providers.md
index 3dc1610..cf944fa 100644
--- a/docs/Providers.md
+++ b/docs/Providers.md
@@ -4,6 +4,9 @@ This page documents the setup at the OIDC provider.
 
 ## Microsoft Entra ID (formerly known as Azure AD)
 
+<details>
+<summary>Expand</summary>
+
 ### Register an app with Microsoft Entra ID
 
 1. Sign in to your admin account on the tenant.
@@ -29,23 +32,57 @@ References:
 
 ### Configuration
 
+<table>
+<thead><tr><td>env/sysconfig configuration</td></tr></thead>
+<tbody><tr><td>
+
 ```ini
 CONFIG_OAUTH2_ISSUER=https://login.microsoftonline.com/$TENANT_ID/v2.0
-CONFIG_OAUTH2_CLIENT_ID=$CLIENT_ID
-CONFIG_OAUTH2_CLIENT_SECRET=$CLIENT_SECRET
+CONFIG_OAUTH2_CLIENT_ID=<client_id>
+CONFIG_OAUTH2_CLIENT_SECRET=<client_secret>
+# The scopes openid profile are required, but configured by default.
+# offline_access is required for non-interactive session refresh.
+# CONFIG_OAUTH2_SCOPES=openid profile offline_access
 ```
+</td></tr></tbody>
+<thead><tr><td>yaml configuration</td></tr></thead>
+<tbody><tr><td>
+
+```yaml
+oauth2:
+  issuer: "https://login.microsoftonline.com/$TENANT_ID/v2.0"
+  client:
+    id: "<client_id>"
+    secret: "<client_secret>"
+  # The scopes openid profile are required, but configured by default.
+  # offline_access is required for non-interactive session refresh.
+  #scopes:
+  #  - "openid"
+  #  - "profile"
+  #  - "offline_access"
+
+```
+</td></tr></tbody>
+</table>
 
 
 ### Restrict auth to specific groups in your directory. (optional)
 
-Restrict login based on groups can be configured inside the App Registration directly. This is generally preferred, since users get the notice from Azure that they are not part of the group and the login would be denied.
+Restrict login based on groups can be configured inside the App Registration directly.
+This is preferred.
+Users get the notice from Azure that they aren’t part of the group, and the login is denied.
 
 Reference: https://learn.microsoft.com/en-us/entra/identity-platform/howto-restrict-your-app-to-a-set-of-users#assign-the-app-to-users-and-groups-to-restrict-access
 
 How require multiple groups, check you could define `CONFIG_OAUTH2_VALIDATE_GROUPS`.
 
+</details>
+
 ## Google Cloud / Google Workspace
 
+<details>
+<summary>Expand</summary>
+
 ### Register an app on google cloud console
 
 1. Login as admin into your [Google console](https://console.cloud.google.com/).
@@ -70,19 +107,53 @@ How require multiple groups, check you could define `CONFIG_OAUTH2_VALIDATE_GROU
 2. Access the [Google Admin Portal](https://admin.google.com/ac/groups) and locate the group that is required for the `openvpn-auth-oauth2` authorization.
 3. The URL of the group page should follow this pattern: `https://admin.google.com/ac/groups/<ID>`. Replace `<ID>` with the actual ID of the group. Make sure to copy this ID for future use. If there are multiple groups, repeat this step for each one.
 4. Insert the copied ID(s) into the `CONFIG_OAUTH2_VALIDATE_GROUPS` configuration setting in your `openvpn-auth-oauth2` setup.
+5. **Optional**: If oauth2 scopes are set in the configuration, the `https://www.googleapis.com/auth/cloud-identity.groups.readonly` scope is required for group validation.
 
 ### Configuration
 
 Set the following variables in your openvpn-auth-oauth2 configuration file:
 
+<table>
+<thead><tr><td>env/sysconfig configuration</td></tr></thead>
+<tbody><tr><td>
+
 ```ini
 CONFIG_OAUTH2_PROVIDER=google
 CONFIG_OAUTH2_ISSUER=https://accounts.google.com
 CONFIG_OAUTH2_CLIENT_ID=162738495-xxxxx.apps.googleusercontent.com
 CONFIG_OAUTH2_CLIENT_SECRET=GOCSPX-xxxxxxxx
 
-# CONFIG_OAUTH2_VALIDATE_GROUPS=03x8tuzt3hqdv5v
+# The scopes openid profile email are required, but configured by default.
+# https://www.googleapis.com/auth/cloud-identity.groups.readonly is mandatory for group validation.
+# Enabled by default, if scopes aren't set in the config.
+#CONFIG_OAUTH2_SCOPES=openid profile email https://www.googleapis.com/auth/cloud-identity.groups.readonly
+#CONFIG_OAUTH2_VALIDATE_GROUPS=03x8tuzt3hqdv5v
 ```
+</td></tr></tbody>
+<thead><tr><td>yaml configuration</td></tr></thead>
+<tbody><tr><td>
+
+```yaml
+oauth2:
+  provider: "google"
+  issuer: "https://accounts.google.com"
+  client:
+    id: "162738495-xxxxx.apps.googleusercontent.com"
+    secret: "GOCSPX-xxxxxxxx"
+  # The scopes openid profile email are required, but configured by default.
+  # https://www.googleapis.com/auth/cloud-identity.groups.readonly is mandatory for group validation.
+  # Enabled by default, if scopes aren't set in the config.
+  #scopes:
+  #  - "openid"
+  #  - "profile"
+  #  - "email"
+  #  - "https://www.googleapis.com/auth/cloud-identity.groups.readonly"
+  validate:
+    groups:
+      - "03x8tuzt3hqdv5v"
+```
+</td></tr></tbody>
+</table>
 
 ### Google consent screen always asking for permission grant
 
@@ -92,8 +163,13 @@ refresh token to validate the user on re-auth.
 
 To avoid this, you can set `oauth2.refresh.validate-user` to `false`. Read more about this in the [Configuration](Configuration.md#non-interactive-session-refresh) page.
 
+</details>
+
 ## Keycloak
 
+<details>
+<summary>Expand</summary>
+
 ### Register an App with Keycloak
 
 1. Sign in to your admin account on the Keycloak admin console.
@@ -114,11 +190,38 @@ To avoid this, you can set `oauth2.refresh.validate-user` to `false`. Read more
 
 Set the following variables in your `openvpn-auth-oauth2` configuration file:
 
+<table>
+<thead><tr><td>env/sysconfig configuration</td></tr></thead>
+<tbody><tr><td>
+
 ```ini
 CONFIG_OAUTH2_ISSUER=https://<keycloak-domain>/auth/realms/<realm-name>
-CONFIG_OAUTH2_CLIENT_ID=openvpn-auth-oauth2
-CONFIG_OAUTH2_CLIENT_SECRET=<client-secret>
+CONFIG_OAUTH2_CLIENT_ID=<client_id>
+CONFIG_OAUTH2_CLIENT_SECRET=<client_secret>
+# The scopes openid profile are required, but configured by default.
+# offline_access is required for non-interactive session refresh.
+# CONFIG_OAUTH2_SCOPES=openid profile offline_access
+```
+</td></tr></tbody>
+<thead><tr><td>yaml configuration</td></tr></thead>
+<tbody><tr><td>
+
+```yaml
+oauth2:
+  issuer: "https://<keycloak-domain>/auth/realms/<realm-name>"
+  client:
+    id: "<client_id>"
+    secret: "<client_secret>"
+  # The scopes openid profile are required, but configured by default.
+  # offline_access is required for non-interactive session refresh.
+  #scopes:
+  #  - "openid"
+  #  - "profile"
+  #  - "offline_access"
+
 ```
+</td></tr></tbody>
+</table>
 
 ### Role Mapping for openvpn-auth-oauth2 (optional)
 
@@ -142,8 +245,14 @@ If you are using Keycloak, you can map the roles to the `roles` claim in the tok
 Currently, there is no known configuration to enrich the token with the client's IP address in Keycloak.
 If you know how to do this, please contribute to the documentation.
 
+</details>
+
 ## GitHub
 
+
+<details>
+<summary>Expand</summary>
+
 ### Caveats
 
 A user must explicitly [request](https://help.github.com/articles/requesting-organization-approval-for-oauth-apps/) an
@@ -161,17 +270,44 @@ After registering the app, you will receive an OAuth2 client ID and secret. Thes
 
 ### Configuration
 
+<table>
+<thead><tr><td>env/sysconfig configuration</td></tr></thead>
+<tbody><tr><td>
+
 ```ini
 CONFIG_OAUTH2_PROVIDER=github
 CONFIG_OAUTH2_ISSUER=https://github.com
-CONFIG_OAUTH2_CLIENT_ID=$CLIENT_ID
-CONFIG_OAUTH2_CLIENT_SECRET=$CLIENT_SECRET
-CONFIG_OAUTH2_VALIDATE_GROUPS=org
-CONFIG_OAUTH2_VALIDATE_ROLES=org:team
+CONFIG_OAUTH2_CLIENT_ID=<client_id>
+CONFIG_OAUTH2_CLIENT_SECRET=<client_secret>
+CONFIG_OAUTH2_VALIDATE_GROUPS=your_github_org_name
+CONFIG_OAUTH2_VALIDATE_ROLES=your_github_org_name:team_name
 ```
+</td></tr></tbody>
+<thead><tr><td>yaml configuration</td></tr></thead>
+<tbody><tr><td>
+
+```yaml
+oauth2:
+  provider: "github"
+  issuer: "https://github.com"
+  client:
+    id: "<client_id>"
+    secret: "<client_secret>"
+  validate:
+    groups: "your_github_org_name"
+    roles: "your_github_org_name:team_name"
+```
+</td></tr></tbody>
+</table>
+
+</details>
 
 ## Digitalocean
 
+
+<details>
+<summary>Expand</summary>
+
 ### Register an application in Digitalocean
 
 Developers must [register their application](https://cloud.digitalocean.com/account/api/applications/new) to use OAuth.
@@ -181,15 +317,44 @@ and only used between the application and the DigitalOcean authorization server
 
 ### Configuration
 
+<table>
+<thead><tr><td>env/sysconfig configuration</td></tr></thead>
+<tbody><tr><td>
+
 ```ini
 CONFIG_OAUTH2_ISSUER=https://cloud.digitalocean.com/
 CONFIG_OAUTH2_SCOPES=read
+CONFIG_OAUTH2_CLIENT_ID=<client_id>
+CONFIG_OAUTH2_CLIENT_SECRET=<client_secret>
 CONFIG_OAUTH2_ENDPOINT_TOKEN=https://cloud.digitalocean.com/v1/oauth/token
 CONFIG_OAUTH2_ENDPOINT_AUTH=https://cloud.digitalocean.com/v1/oauth/authorize
 ```
+</td></tr></tbody>
+<thead><tr><td>yaml configuration</td></tr></thead>
+<tbody><tr><td>
+
+```yaml
+oauth2:
+  issuer: "https://cloud.digitalocean.com/"
+  scopes:
+    - "read"
+  client:
+    id: "<client_id>"
+    secret: "<client_secret>"
+  endpoints:
+    token: "https://cloud.digitalocean.com/v1/oauth/token"
+    auth: "https://cloud.digitalocean.com/v1/oauth/authorize"
+```
+</td></tr></tbody>
+</table>
+
+</details>
 
 ## Zitadel
 
+<details>
+<summary>Expand</summary>
+
 ### Register an application in Zitadel
 
 1. Create a project in Zitadel
@@ -203,14 +368,37 @@ After creating application, on page URLs you can find all links that you need.
 
 ### Configuration
 
+<table>
+<thead><tr><td>env/sysconfig configuration</td></tr></thead>
+<tbody><tr><td>
+
 ```ini
-CONFIG_HTTP_BASEURL=http://<vpn>:9000/
-CONFIG_HTTP_LISTEN=:9000
-CONFIG_HTTP_SECRET=1jd93h5b6s82lf03jh5b2hf9
-CONFIG_OPENVPN_ADDR=unix:///run/openvpn/server.sock
-CONFIG_OPENVPN_PASSWORD=<password from /etc/openvpn/password.txt>
 CONFIG_OAUTH2_ISSUER=https://company.zitadel.cloud
-CONFIG_OAUTH2_SCOPES=openid profile email offline_access
 CONFIG_OAUTH2_CLIENT_ID=<client_id>
 CONFIG_OAUTH2_CLIENT_SECRET=<client_secret>
+# The scopes openid profile email are required, but configured by default.
+# offline_access is required for non-interactive session refresh.
+#CONFIG_OAUTH2_SCOPES=openid profile email offline_access
 ```
+</td></tr></tbody>
+<thead><tr><td>yaml configuration</td></tr></thead>
+<tbody><tr><td>
+
+```yaml
+oauth2:
+  issuer: "https://company.zitadel.cloud"
+  client:
+    id: "<client_id>"
+    secret: "<client_secret>"
+  # The scopes openid profile are required, but configured by default.
+  # offline_access is required for non-interactive session refresh.
+  #scopes:
+  #  - "openid"
+  #  - "profile"
+  #  - "email"
+  #  - "offline_access"
+```
+</td></tr></tbody>
+</table>
+
+</details>
diff --git a/internal/httpserver/main.go b/internal/httpserver/main.go
index 1a85b3e..80b127f 100644
--- a/internal/httpserver/main.go
+++ b/internal/httpserver/main.go
@@ -7,12 +7,19 @@ import (
 	"log/slog"
 	"net"
 	"net/http"
+	"os"
+	"strconv"
 	"sync"
 	"time"
 
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
 )
 
+const (
+	ServerNameDefault = "server"
+	ServerNameDebug   = "debug"
+)
+
 type Server struct {
 	name   string
 	conf   config.HTTP
@@ -54,26 +61,17 @@ func (s *Server) Listen(ctx context.Context) error {
 			return err
 		}
 
-		s.server.TLSConfig = new(tls.Config)
-		s.server.TLSConfig.GetCertificate = s.GetCertificateFunc()
-
-		s.logger.Info(fmt.Sprintf(
-			"start HTTPS %s listener on %s", s.name, s.conf.Listen,
-		))
-
-		go func() {
-			errCh <- s.server.ListenAndServeTLS("", "")
-		}()
-	} else {
-		s.logger.Info(fmt.Sprintf(
-			"start HTTP %s listener on %s", s.name, s.conf.Listen,
-		))
-
-		go func() {
-			errCh <- s.server.ListenAndServe()
-		}()
+		s.server.TLSConfig = &tls.Config{
+			MinVersion:     tls.VersionTLS12,
+			GetCertificate: s.GetCertificateFunc(),
+			NextProtos:     []string{"h2", "http/1.1"},
+		}
 	}
 
+	go func() {
+		errCh <- s.serve()
+	}()
+
 	select {
 	case err := <-errCh:
 		return fmt.Errorf("error http %s listening: %w", s.name, err)
@@ -92,6 +90,52 @@ func (s *Server) Listen(ctx context.Context) error {
 	return nil
 }
 
+func (s *Server) serve() error {
+	if s.server == nil {
+		return fmt.Errorf("http %s server is nil", s.name)
+	}
+
+	var (
+		err      error
+		listener net.Listener
+	)
+
+	if s.name == ServerNameDefault && os.Getenv("LISTEN_PID") == strconv.Itoa(os.Getpid()) {
+		// systemd run
+		listener, err = net.FileListener(os.NewFile(3, "from systemd"))
+		if err != nil {
+			return fmt.Errorf("net.FileListener: %w", err)
+		}
+	} else {
+		listener, err = net.Listen("tcp", s.conf.Listen)
+		if err != nil {
+			return fmt.Errorf("net.Listen: %w", err)
+		}
+	}
+
+	if s.conf.TLS {
+		s.logger.Info(fmt.Sprintf(
+			"start HTTPS %s listener on %s", s.name, listener.Addr().String(),
+		))
+
+		if err = s.server.ServeTLS(listener, "", ""); err != nil {
+			return fmt.Errorf("http.ServeTLS: %w", err)
+		}
+
+		return nil
+	}
+
+	s.logger.Info(fmt.Sprintf(
+		"start HTTP %s listener on %s", s.name, listener.Addr().String(),
+	))
+
+	if err = s.server.Serve(listener); err != nil {
+		return fmt.Errorf("http.Serve: %w", err)
+	}
+
+	return nil
+}
+
 func (s *Server) GetCertificateFunc() func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
 	return func(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
 		s.tlsCertificateMu.RLock()
@@ -126,11 +170,11 @@ func (s *Server) Reload() error {
 
 func (s *Server) shutdown() error {
 	if s.server == nil {
-		return nil
+		return fmt.Errorf("http %s server is nil", s.name)
 	}
 
 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
-	defer cancel()
+	s.server.RegisterOnShutdown(cancel)
 
 	return s.server.Shutdown(ctx) //nolint:wrapcheck
 }
diff --git a/internal/oauth2/providers/google/config.go b/internal/oauth2/providers/google/config.go
index 162085d..cbe2c6c 100644
--- a/internal/oauth2/providers/google/config.go
+++ b/internal/oauth2/providers/google/config.go
@@ -1,6 +1,8 @@
 package google
 
 import (
+	"slices"
+
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/types"
 	"golang.org/x/oauth2"
@@ -24,5 +26,7 @@ func (p *Provider) GetProviderConfig(conf config.Config) (types.ProviderConfig,
 		providerConfig.Scopes = append(providerConfig.Scopes, "https://www.googleapis.com/auth/cloud-identity.groups.readonly")
 	}
 
+	providerConfig.Scopes = slices.Compact(providerConfig.Scopes)
+
 	return providerConfig, nil
 }
diff --git a/pkg/testutils/http.go b/internal/utils/testutils/http.go
similarity index 100%
rename from pkg/testutils/http.go
rename to internal/utils/testutils/http.go
diff --git a/pkg/testutils/logger.go b/internal/utils/testutils/logger.go
similarity index 100%
rename from pkg/testutils/logger.go
rename to internal/utils/testutils/logger.go
diff --git a/pkg/testutils/main.go b/internal/utils/testutils/main.go
similarity index 100%
rename from pkg/testutils/main.go
rename to internal/utils/testutils/main.go
diff --git a/packaging/etc/apparmor.d/usr.bin.openvpn-auth-oauth2 b/packaging/etc/apparmor.d/usr.bin.openvpn-auth-oauth2
index 02407b4..19057d2 100644
--- a/packaging/etc/apparmor.d/usr.bin.openvpn-auth-oauth2
+++ b/packaging/etc/apparmor.d/usr.bin.openvpn-auth-oauth2
@@ -8,6 +8,8 @@ include <tunables/global>
   include <abstractions/base>
   include <abstractions/nameservice>
 
+  @{PROC}/sys/kernel/core_pattern r,
+
   @{etc_ro}/openvpn-auth-oauth2/** r,
 
   @{run}/openvpn{,-server}/* rw,
diff --git a/packaging/usr/lib/systemd/system/openvpn-auth-oauth2.service b/packaging/usr/lib/systemd/system/openvpn-auth-oauth2.service
index 8b42ffa..6ae6f4c 100644
--- a/packaging/usr/lib/systemd/system/openvpn-auth-oauth2.service
+++ b/packaging/usr/lib/systemd/system/openvpn-auth-oauth2.service
@@ -29,7 +29,7 @@ RuntimeDirectory=openvpn-auth-oauth2
 ReadWritePaths=-/run/openvpn -/run/openvpn-server
 
 RestartSec=5s
-Restart=Always
+Restart=always
 
 # We don't need write access anywhere.
 AmbientCapabilities=
diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go
index 3497f09..50225ed 100644
--- a/pkg/plugin/plugin.go
+++ b/pkg/plugin/plugin.go
@@ -88,7 +88,7 @@ func openvpn_plugin_open_v3_go(v3structver C.int, args *C.struct_openvpn_plugin_
 		return C.OPENVPN_PLUGIN_FUNC_ERROR
 	}
 
-	handle.server = httpserver.NewHTTPServer("server", logger, conf.HTTP, provider.Handler())
+	handle.server = httpserver.NewHTTPServer(httpserver.ServerNameDefault, logger, conf.HTTP, provider.Handler())
 
 	go func() {
 		if err := handle.server.Listen(context.Background()); err != nil {
diff --git a/tools/go.mod b/tools/go.mod
index ac11f6a..c790c3e 100644
--- a/tools/go.mod
+++ b/tools/go.mod
@@ -6,10 +6,10 @@ require (
 	github.com/bombsimon/wsl/v4 v4.4.1
 	github.com/catenacyber/perfsprint v0.7.1
 	github.com/daixiang0/gci v0.13.4
-	github.com/golangci/golangci-lint v1.59.1
+	github.com/golangci/golangci-lint v1.60.1
 	github.com/tetafro/godot v1.4.16
 	golang.org/x/tools v0.24.0
-	mvdan.cc/gofumpt v0.6.0
+	mvdan.cc/gofumpt v0.7.0
 )
 
 require (
@@ -20,7 +20,7 @@ require (
 	github.com/Antonboom/errname v0.1.13 // indirect
 	github.com/Antonboom/nilnil v0.1.9 // indirect
 	github.com/Antonboom/testifylint v1.4.3 // indirect
-	github.com/BurntSushi/toml v1.4.0 // indirect
+	github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
 	github.com/Crocmagnon/fatcontext v0.4.0 // indirect
 	github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect
 	github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 // indirect
@@ -67,7 +67,7 @@ require (
 	github.com/gobwas/glob v0.2.3 // indirect
 	github.com/gofrs/flock v0.12.1 // indirect
 	github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect
-	github.com/golangci/gofmt v0.0.0-20240724082148-399ed8a42d42 // indirect
+	github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9 // indirect
 	github.com/golangci/misspell v0.6.0 // indirect
 	github.com/golangci/modinfo v0.3.4 // indirect
 	github.com/golangci/plugin-module-register v0.1.1 // indirect
@@ -120,7 +120,7 @@ require (
 	github.com/pelletier/go-toml/v2 v2.2.2 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/polyfloyd/go-errorlint v1.6.0 // indirect
-	github.com/prometheus/client_golang v1.19.1 // indirect
+	github.com/prometheus/client_golang v1.20.0 // indirect
 	github.com/prometheus/client_model v0.6.1 // indirect
 	github.com/prometheus/common v0.55.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
@@ -159,7 +159,7 @@ require (
 	github.com/tdakkota/asciicheck v0.2.0 // indirect
 	github.com/timakin/bodyclose v0.0.0-20240125160201-f835fa56326a // indirect
 	github.com/timonwong/loggercheck v0.9.4 // indirect
-	github.com/tomarrell/wrapcheck/v2 v2.8.3 // indirect
+	github.com/tomarrell/wrapcheck/v2 v2.9.0 // indirect
 	github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
 	github.com/ultraware/funlen v0.1.0 // indirect
 	github.com/ultraware/whitespace v0.1.1 // indirect
@@ -184,6 +184,6 @@ require (
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	honnef.co/go/tools v0.4.7 // indirect
+	honnef.co/go/tools v0.5.1 // indirect
 	mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect
 )
diff --git a/tools/go.sum b/tools/go.sum
index 7136401..feba7bc 100644
--- a/tools/go.sum
+++ b/tools/go.sum
@@ -12,8 +12,8 @@ github.com/Antonboom/nilnil v0.1.9 h1:eKFMejSxPSA9eLSensFmjW2XTgTwJMjZ8hUHtV4s/S
 github.com/Antonboom/nilnil v0.1.9/go.mod h1:iGe2rYwCq5/Me1khrysB4nwI7swQvjclR8/YRPl5ihQ=
 github.com/Antonboom/testifylint v1.4.3 h1:ohMt6AHuHgttaQ1xb6SSnxCeK4/rnK7KKzbvs7DmEck=
 github.com/Antonboom/testifylint v1.4.3/go.mod h1:+8Q9+AOLsz5ZiQiiYujJKs9mNz398+M6UgslP4qgJLA=
-github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
-github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
+github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
 github.com/Crocmagnon/fatcontext v0.4.0 h1:4ykozu23YHA0JB6+thiuEv7iT6xq995qS1vcuWZq0tg=
 github.com/Crocmagnon/fatcontext v0.4.0/go.mod h1:ZtWrXkgyfsYPzS6K3O88va6t2GEglG93vnII/F94WC0=
 github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM=
@@ -99,6 +99,8 @@ github.com/go-critic/go-critic v0.11.4 h1:O7kGOCx0NDIni4czrkRIXTnit0mkyKOCePh3My
 github.com/go-critic/go-critic v0.11.4/go.mod h1:2QAdo4iuLik5S9YG0rT4wcZ8QxwHYkrr6/2MWAiv/vc=
 github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
 github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
+github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
 github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
 github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
 github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8=
@@ -130,10 +132,10 @@ github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
 github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
 github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM=
 github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=
-github.com/golangci/gofmt v0.0.0-20240724082148-399ed8a42d42 h1:AhUw9DWQccFqRvFyM6Zb7zpG+Co5skgw9YS1V0LFijA=
-github.com/golangci/gofmt v0.0.0-20240724082148-399ed8a42d42/go.mod h1:YUrl2t63y0Ofpy74+wu/O18RfPenFkc9I3ntyYuGCtI=
-github.com/golangci/golangci-lint v1.59.1 h1:CRRLu1JbhK5avLABFJ/OHVSQ0Ie5c4ulsOId1h3TTks=
-github.com/golangci/golangci-lint v1.59.1/go.mod h1:jX5Oif4C7P0j9++YB2MMJmoNrb01NJ8ITqKWNLewThg=
+github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9 h1:/1322Qns6BtQxUZDTAT4SdcoxknUki7IAoK4SAXr8ME=
+github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9/go.mod h1:Oesb/0uFAyWoaw1U1qS5zyjCg5NP9C9iwjnI4tIsXEE=
+github.com/golangci/golangci-lint v1.60.1 h1:DRKNqNTQRLBJZ1il5u4fvgLQCjQc7QFs0DbhksJtVJE=
+github.com/golangci/golangci-lint v1.60.1/go.mod h1:jDIPN1rYaIA+ijp9OZcUmUCoQOtZ76pOlFbi15FlLJY=
 github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs=
 github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo=
 github.com/golangci/modinfo v0.3.4 h1:oU5huX3fbxqQXdfspamej74DFX0kyGLkw1ppvXoJ8GA=
@@ -192,6 +194,8 @@ github.com/kisielk/errcheck v1.7.0 h1:+SbscKmWJ5mOK/bO1zS60F5I9WwZDWOfRsC4RwfwRV
 github.com/kisielk/errcheck v1.7.0/go.mod h1:1kLL+jV4e+CFfueBmI1dSK2ADDyQnlrnrY/FqKluHJQ=
 github.com/kkHAIKE/contextcheck v1.1.5 h1:CdnJh63tcDe53vG+RebdpdXJTc9atMgGqdx8LXxiilg=
 github.com/kkHAIKE/contextcheck v1.1.5/go.mod h1:O930cpht4xb1YQpK+1+AgoM3mFsvxr7uyFptcnWTYUA=
+github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
+github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -272,8 +276,8 @@ github.com/polyfloyd/go-errorlint v1.6.0 h1:tftWV9DE7txiFzPpztTAwyoRLKNj9gpVm2cg
 github.com/polyfloyd/go-errorlint v1.6.0/go.mod h1:HR7u8wuP1kb1NeN1zqTd1ZMlqUKPPHF+Id4vIPvDqVw=
 github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
 github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
-github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
-github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
+github.com/prometheus/client_golang v1.20.0 h1:jBzTZ7B099Rg24tny+qngoynol8LtVYlA2bqx3vEloI=
+github.com/prometheus/client_golang v1.20.0/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
 github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
 github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
 github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
@@ -372,8 +376,8 @@ github.com/timakin/bodyclose v0.0.0-20240125160201-f835fa56326a h1:A6uKudFIfAEpo
 github.com/timakin/bodyclose v0.0.0-20240125160201-f835fa56326a/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460=
 github.com/timonwong/loggercheck v0.9.4 h1:HKKhqrjcVj8sxL7K77beXh0adEm6DLjV/QOGeMXEVi4=
 github.com/timonwong/loggercheck v0.9.4/go.mod h1:caz4zlPcgvpEkXgVnAJGowHAMW2NwHaNlpS8xDbVhTg=
-github.com/tomarrell/wrapcheck/v2 v2.8.3 h1:5ov+Cbhlgi7s/a42BprYoxsr73CbdMUTzE3bRDFASUs=
-github.com/tomarrell/wrapcheck/v2 v2.8.3/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo=
+github.com/tomarrell/wrapcheck/v2 v2.9.0 h1:801U2YCAjLhdN8zhZ/7tdjB3EnAoRlJHt/s+9hijLQ4=
+github.com/tomarrell/wrapcheck/v2 v2.9.0/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo=
 github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw=
 github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=
 github.com/ultraware/funlen v0.1.0 h1:BuqclbkY6pO+cvxoq7OsktIXZpgBSkYTQtmwhAK81vI=
@@ -553,9 +557,9 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-honnef.co/go/tools v0.4.7 h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs=
-honnef.co/go/tools v0.4.7/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0=
-mvdan.cc/gofumpt v0.6.0 h1:G3QvahNDmpD+Aek/bNOLrFR2XC6ZAdo62dZu65gmwGo=
-mvdan.cc/gofumpt v0.6.0/go.mod h1:4L0wf+kgIPZtcCWXynNS2e6bhmj73umwnuXSZarixzA=
+honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I=
+honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs=
+mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU=
+mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo=
 mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f h1:lMpcwN6GxNbWtbpI1+xzFLSW8XzX0u72NttUGVFjO3U=
 mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f/go.mod h1:RSLa7mKKCNeTTMHBw5Hsy2rfJmd6O2ivt9Dw9ZqCQpQ=


Test patch:
diff --git a/cmd/daemon/full_test.go b/cmd/daemon/full_test.go
index 63783fc..373d8cb 100644
--- a/cmd/daemon/full_test.go
+++ b/cmd/daemon/full_test.go
@@ -15,7 +15,7 @@ import (
 	"github.com/jkroepke/openvpn-auth-oauth2/cmd/daemon"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils"
-	"github.com/jkroepke/openvpn-auth-oauth2/pkg/testutils"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils/testutils"
 	"github.com/madflojo/testcerts"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
diff --git a/cmd/daemon/root_test.go b/cmd/daemon/root_test.go
index e40a408..e348d1f 100644
--- a/cmd/daemon/root_test.go
+++ b/cmd/daemon/root_test.go
@@ -6,7 +6,7 @@ import (
 	"testing"
 
 	"github.com/jkroepke/openvpn-auth-oauth2/cmd/daemon"
-	"github.com/jkroepke/openvpn-auth-oauth2/pkg/testutils"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils/testutils"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"golang.org/x/net/nettest"
@@ -77,7 +77,7 @@ func TestExecuteConfigInvalid(t *testing.T) {
 				"", "--config=../../config.example.yaml", "--log.format=console", "--log.level=info", "--http.secret=" + testutils.Secret,
 				"--http.listen=127.0.0.1:100000", "--oauth2.endpoint.token=http://127.0.0.1:10000/token", "--oauth2.endpoint.auth=http://127.0.0.1:10000/auth",
 			},
-			`error http listener: error http server listening: listen tcp: address 100000: invalid port`,
+			`error http listener: error http server listening: net.Listen: listen tcp: address 100000: invalid port`,
 		},
 		{
 			"error http debug listener",
@@ -86,7 +86,7 @@ func TestExecuteConfigInvalid(t *testing.T) {
 				"--debug.pprof=true", "--debug.listen=127.0.0.1:100000", "--oauth2.endpoint.token=http://127.0.0.1:10000/token",
 				"--oauth2.endpoint.auth=http://127.0.0.1:10000/auth",
 			},
-			`error debug http listener: error http debug listening: listen tcp: address 100000: invalid port`,
+			`error debug http listener: error http debug listening: net.Listen: listen tcp: address 100000: invalid port`,
 		},
 	}
 
diff --git a/internal/config/flags_test.go b/internal/config/flags_test.go
index 418c419..5a1eb7b 100644
--- a/internal/config/flags_test.go
+++ b/internal/config/flags_test.go
@@ -9,7 +9,7 @@ import (
 	"testing"
 
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
-	"github.com/jkroepke/openvpn-auth-oauth2/pkg/testutils"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils/testutils"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
diff --git a/internal/httpserver/main_test.go b/internal/httpserver/main_test.go
index 2e3c36e..0681262 100644
--- a/internal/httpserver/main_test.go
+++ b/internal/httpserver/main_test.go
@@ -11,7 +11,7 @@ import (
 
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/httpserver"
-	"github.com/jkroepke/openvpn-auth-oauth2/pkg/testutils"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils/testutils"
 	"github.com/madflojo/testcerts"
 	"github.com/stretchr/testify/require"
 )
@@ -77,7 +77,7 @@ func TestNewHTTPServer(t *testing.T) {
 			mux := gohttp.NewServeMux()
 			mux.Handle("/", gohttp.NotFoundHandler())
 
-			svr := httpserver.NewHTTPServer("server", logger.Logger, tt.conf.HTTP, mux)
+			svr := httpserver.NewHTTPServer(httpserver.ServerNameDefault, logger.Logger, tt.conf.HTTP, mux)
 
 			ctx, cancel := context.WithCancel(context.Background())
 
diff --git a/internal/internal_test.go b/internal/internal_test.go
index a34a60d..6484f58 100644
--- a/internal/internal_test.go
+++ b/internal/internal_test.go
@@ -12,7 +12,7 @@ import (
 	"time"
 
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
-	"github.com/jkroepke/openvpn-auth-oauth2/pkg/testutils"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils/testutils"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
diff --git a/internal/oauth2/handler_test.go b/internal/oauth2/handler_test.go
index fcd574f..f10ba33 100644
--- a/internal/oauth2/handler_test.go
+++ b/internal/oauth2/handler_test.go
@@ -15,7 +15,7 @@ import (
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/openvpn"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
-	"github.com/jkroepke/openvpn-auth-oauth2/pkg/testutils"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils/testutils"
 	"github.com/stretchr/testify/assert"
 )
 
diff --git a/internal/oauth2/provider_test.go b/internal/oauth2/provider_test.go
index ef2be6f..f42c8c3 100644
--- a/internal/oauth2/provider_test.go
+++ b/internal/oauth2/provider_test.go
@@ -14,7 +14,7 @@ import (
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/providers/generic"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/openvpn"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/storage"
-	"github.com/jkroepke/openvpn-auth-oauth2/pkg/testutils"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils/testutils"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"golang.org/x/net/nettest"
diff --git a/internal/oauth2/providers/generic/check_test.go b/internal/oauth2/providers/generic/check_test.go
index 6c9941c..1c50e3c 100644
--- a/internal/oauth2/providers/generic/check_test.go
+++ b/internal/oauth2/providers/generic/check_test.go
@@ -10,7 +10,7 @@ import (
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/idtoken"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/providers/generic"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
-	"github.com/jkroepke/openvpn-auth-oauth2/pkg/testutils"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils/testutils"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/zitadel/oidc/v3/pkg/oidc"
diff --git a/internal/oauth2/providers/github/check_test.go b/internal/oauth2/providers/github/check_test.go
index 37546a7..59b3c39 100644
--- a/internal/oauth2/providers/github/check_test.go
+++ b/internal/oauth2/providers/github/check_test.go
@@ -13,7 +13,7 @@ import (
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/providers/github"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/types"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
-	"github.com/jkroepke/openvpn-auth-oauth2/pkg/testutils"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils/testutils"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/zitadel/oidc/v3/pkg/oidc"
diff --git a/internal/oauth2/providers/github/user_test.go b/internal/oauth2/providers/github/user_test.go
index c1195fb..c5652c8 100644
--- a/internal/oauth2/providers/github/user_test.go
+++ b/internal/oauth2/providers/github/user_test.go
@@ -11,7 +11,7 @@ import (
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/idtoken"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/providers/github"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/types"
-	"github.com/jkroepke/openvpn-auth-oauth2/pkg/testutils"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils/testutils"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/zitadel/oidc/v3/pkg/oidc"
diff --git a/internal/oauth2/providers/google/check_test.go b/internal/oauth2/providers/google/check_test.go
index 170912b..e35f69f 100644
--- a/internal/oauth2/providers/google/check_test.go
+++ b/internal/oauth2/providers/google/check_test.go
@@ -13,7 +13,7 @@ import (
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/providers/google"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/types"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
-	"github.com/jkroepke/openvpn-auth-oauth2/pkg/testutils"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils/testutils"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/zitadel/oidc/v3/pkg/oidc"
diff --git a/internal/oauth2/refresh_test.go b/internal/oauth2/refresh_test.go
index 99fa957..f6e2f3e 100644
--- a/internal/oauth2/refresh_test.go
+++ b/internal/oauth2/refresh_test.go
@@ -15,7 +15,7 @@ import (
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/providers/github"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/providers/google"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/types"
-	"github.com/jkroepke/openvpn-auth-oauth2/pkg/testutils"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils/testutils"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
diff --git a/internal/openvpn/bench_test.go b/internal/openvpn/bench_test.go
index 31edf0a..9aaadc2 100644
--- a/internal/openvpn/bench_test.go
+++ b/internal/openvpn/bench_test.go
@@ -19,7 +19,7 @@ import (
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/openvpn"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/storage"
-	"github.com/jkroepke/openvpn-auth-oauth2/pkg/testutils"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils/testutils"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"golang.org/x/net/nettest"
diff --git a/internal/openvpn/main_test.go b/internal/openvpn/main_test.go
index fd2863f..458c2b3 100644
--- a/internal/openvpn/main_test.go
+++ b/internal/openvpn/main_test.go
@@ -19,7 +19,7 @@ import (
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/openvpn"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/storage"
-	"github.com/jkroepke/openvpn-auth-oauth2/pkg/testutils"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils/testutils"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"golang.org/x/net/nettest"
diff --git a/internal/openvpn/passthrough_unix_test.go b/internal/openvpn/passthrough_unix_test.go
index 134fdb7..452d465 100644
--- a/internal/openvpn/passthrough_unix_test.go
+++ b/internal/openvpn/passthrough_unix_test.go
@@ -23,7 +23,7 @@ import (
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/openvpn"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/storage"
-	"github.com/jkroepke/openvpn-auth-oauth2/pkg/testutils"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils/testutils"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"golang.org/x/net/nettest"
diff --git a/internal/state/bench_test.go b/internal/state/bench_test.go
index 4c9fe88..eda93b4 100644
--- a/internal/state/bench_test.go
+++ b/internal/state/bench_test.go
@@ -4,7 +4,7 @@ import (
 	"testing"
 
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
-	"github.com/jkroepke/openvpn-auth-oauth2/pkg/testutils"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils/testutils"
 	"github.com/stretchr/testify/require"
 )
 
diff --git a/internal/state/state_test.go b/internal/state/state_test.go
index 52b2884..c295678 100644
--- a/internal/state/state_test.go
+++ b/internal/state/state_test.go
@@ -5,7 +5,7 @@ import (
 	"time"
 
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
-	"github.com/jkroepke/openvpn-auth-oauth2/pkg/testutils"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils/testutils"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
diff --git a/internal/storage/storage_test.go b/internal/storage/storage_test.go
index 7db46d5..6459860 100644
--- a/internal/storage/storage_test.go
+++ b/internal/storage/storage_test.go
@@ -6,7 +6,7 @@ import (
 	"time"
 
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/storage"
-	"github.com/jkroepke/openvpn-auth-oauth2/pkg/testutils"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils/testutils"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )