Repository: jkroepke/openvpn-auth-oauth2
Pull request #383 (2025-01-01 18:25:48)

Implement client configs
#### 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


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/383?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke) Report
Attention: Patch coverage is `78.26087%` with `20 lines` in your changes missing coverage. Please review.
> Project coverage is 83.07%. Comparing base [(`b9bba17`)](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/commit/b9bba17867088bf507e41a55e0e69356aee7ae7d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke) to head [(`639c569`)](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/commit/639c569a172170cc1019e0dfa109006fc7033de0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke).

| [Files with missing lines](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/383?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke) | Patch % | Lines |
|---|---|---|
| [internal/config/validate.go](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/383?src=pr&el=tree&filepath=internal%2Fconfig%2Fvalidate.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke#diff-aW50ZXJuYWwvY29uZmlnL3ZhbGlkYXRlLmdv) | 10.00% | [6 Missing and 3 partials :warning: ](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/383?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke) |
| [internal/openvpn/callbacks.go](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/383?src=pr&el=tree&filepath=internal%2Fopenvpn%2Fcallbacks.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke#diff-aW50ZXJuYWwvb3BlbnZwbi9jYWxsYmFja3MuZ28=) | 75.75% | [6 Missing and 2 partials :warning: ](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/383?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke) |
| [cmd/daemon/root.go](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/383?src=pr&el=tree&filepath=cmd%2Fdaemon%2Froot.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke#diff-Y21kL2RhZW1vbi9yb290Lmdv) | 62.50% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/383?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #383      +/-   ##
==========================================
- Coverage   83.15%   83.07%   -0.09%     
==========================================
  Files          50       50              
  Lines        2856     2913      +57     
==========================================
+ Hits         2375     2420      +45     
- Misses        367      375       +8     
- Partials      114      118       +4     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/383?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke).

jkroepke: un stale

Patch:
diff --git a/.golangci.yaml b/.golangci.yaml
index 38dc1ad..8a5fdf5 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -133,7 +133,7 @@ linters:
         - l net.Listener
         - t reflect.Type
         - wg sync.WaitGroup
-        - k *koanf.Koanf
+        - sb strings.Builder
         - mu sync.Mutex
         - ts oauth2.TokenSource
         - ca *testcerts.CertificateAuthority
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index 10c2af6..d58cf50 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -70,6 +70,12 @@ nfpms:
           owner: root
           group: openvpn-auth-oauth2
           mode: 0750
+      - dst: /etc/openvpn-auth-oauth2/client-config/
+        type: dir
+        file_info:
+          owner: root
+          group: openvpn-auth-oauth2
+          mode: 0750
       - src: packaging/etc/openvpn-auth-oauth2/config.yaml
         dst: /etc/openvpn-auth-oauth2/config.yaml
         type: "config|noreplace"
diff --git a/docs/Client specific configuration.md b/docs/Client specific configuration.md
new file mode 100644
index 0000000..bdee375
--- /dev/null
+++ b/docs/Client specific configuration.md
@@ -0,0 +1,15 @@
+# Client specific configuration
+
+## Introduction
+
+This document describes the client-specific configuration options of openvpn-auth-oauth2.
+It mimics the client-config-dir capability of OpenVPN.
+But instead the client username, a token claim is used as config identifier.
+
+## Configuration
+
+The feature must be enabled with `--openvpn.client-config.enabled`.
+`--openvpn.client-config.path` points to a directory where the client-specific configuration files are stored.
+
+openvpn-auth-oauth2 looks for a file
+named after the token claim or common name with `.conf` suffix in the client config directory.
diff --git a/docs/Configuration.md b/docs/Configuration.md
index f5e2a5b..1ac8cdf 100644
--- a/docs/Configuration.md
+++ b/docs/Configuration.md
@@ -11,7 +11,7 @@ Take a look at the [FAQ](./FAQ) section, for common questions, issues and soluti
 openvpn-auth-oauth2 supports configuration via a YAML file. The file can be passed via the `--config` flag.
 
 <details>
-<summary>Example</summary>
+<summary>Example config.yaml</summary>
 
 ```yaml
 debug:
@@ -79,6 +79,10 @@ openvpn:
   # - "test"
   # - "test2"
   override-username: false
+  ccd:
+    enabled: false
+    token-claim: ""
+    path: "/etc/openvpn-auth-oauth2/client-config/"
   common-name:
     environment-variable-name: common_name
     mode: plain
@@ -106,30 +110,30 @@ Usage of openvpn-auth-oauth2:
     	listen address for go profiling endpoint (env: CONFIG_DEBUG_LISTEN) (default ":9001")
   --debug.pprof
     	Enables go profiling endpoint. This should be never exposed. (env: CONFIG_DEBUG_PPROF)
-  --http.assets-path string
+  --http.assets-path value
     	Custom path to the assets directory. Files in this directory will be served under /assets/ and having an higher priority than the embedded assets. (env: CONFIG_HTTP_ASSETS__PATH)
-  --http.baseurl string
-    	listen addr for client listener (env: CONFIG_HTTP_BASEURL) (default "http://localhost:9000")
+  --http.baseurl value
+    	listen addr for client listener (env: CONFIG_HTTP_BASEURL) (default http://localhost:9000)
   --http.cert string
-    	Path to tls server certificate (env: CONFIG_HTTP_CERT)
+    	Path to tls server certificate used for TLS listener. (env: CONFIG_HTTP_CERT)
   --http.check.ipaddr
     	Check if client IP in http and VPN is equal (env: CONFIG_HTTP_CHECK_IPADDR)
   --http.enable-proxy-headers
     	Use X-Forward-For http header for client ips (env: CONFIG_HTTP_ENABLE__PROXY__HEADERS)
   --http.key string
-    	Path to tls server key (env: CONFIG_HTTP_KEY)
+    	Path to tls server key used for TLS listener. (env: CONFIG_HTTP_KEY)
   --http.listen string
     	listen addr for client listener (env: CONFIG_HTTP_LISTEN) (default ":9000")
   --http.secret value
     	Random generated secret for cookie encryption. Must be 16, 24 or 32 characters. If argument starts with file:// it reads the secret from a file. (env: CONFIG_HTTP_SECRET)
-  --http.template string
+  --http.template value
     	Path to a HTML file which is displayed at the end of the screen. See https://github.com/jkroepke/openvpn-auth-oauth2/wiki/Layout-Customization for more information. (env: CONFIG_HTTP_TEMPLATE)
   --http.tls
     	enable TLS listener (env: CONFIG_HTTP_TLS)
   --log.format string
     	log format. json or console (env: CONFIG_LOG_FORMAT) (default "console")
   --log.level value
-    	log level (env: CONFIG_LOG_LEVEL) (default INFO)
+    	log level. Can be one of: debug, info, warn, error (env: CONFIG_LOG_LEVEL) (default INFO)
   --log.vpn-client-ip
     	log IP of VPN client. Useful to have an identifier between OpenVPN and openvpn-auth-oauth2. (env: CONFIG_LOG_VPN__CLIENT__IP) (default true)
   --oauth2.auth-style value
@@ -144,13 +148,13 @@ Usage of openvpn-auth-oauth2:
     	oauth2 client private key id. If specified, JWT assertions will be generated with the specific kid header. (env: CONFIG_OAUTH2_CLIENT_PRIVATE__KEY__ID)
   --oauth2.client.secret value
     	oauth2 client secret. If argument starts with file:// it reads the secret from a file. (env: CONFIG_OAUTH2_CLIENT_SECRET)
-  --oauth2.endpoint.auth string
+  --oauth2.endpoint.auth value
     	The flag is used to specify a custom OAuth2 authorization endpoint. (env: CONFIG_OAUTH2_ENDPOINT_AUTH)
-  --oauth2.endpoint.discovery string
+  --oauth2.endpoint.discovery value
     	The flag is used to set a custom OAuth2 discovery URL. This URL retrieves the provider's configuration details. (env: CONFIG_OAUTH2_ENDPOINT_DISCOVERY)
-  --oauth2.endpoint.token string
+  --oauth2.endpoint.token value
     	The flag is used to specify a custom OAuth2 token endpoint. (env: CONFIG_OAUTH2_ENDPOINT_TOKEN)
-  --oauth2.issuer string
+  --oauth2.issuer value
     	oauth2 issuer (env: CONFIG_OAUTH2_ISSUER)
   --oauth2.nonce
     	If true, a nonce will be defined on the auth URL which is expected inside the token. (env: CONFIG_OAUTH2_NONCE) (default true)
@@ -184,22 +188,28 @@ Usage of openvpn-auth-oauth2:
     	validate issuer from oidc discovery (env: CONFIG_OAUTH2_VALIDATE_ISSUER) (default true)
   --oauth2.validate.roles value
     	oauth2 required user roles. If multiple role are configured, the user needs to be least in one role. Comma separated list. Example: role1,role2,role3 (env: CONFIG_OAUTH2_VALIDATE_ROLES)
-  --openvpn.addr string
-    	openvpn management interface addr. Must start with unix:// or tcp:// (env: CONFIG_OPENVPN_ADDR) (default "unix:/run/openvpn/server.sock")
+  --openvpn.addr value
+    	openvpn management interface addr. Must start with unix:// or tcp:// (env: CONFIG_OPENVPN_ADDR) (default unix:/run/openvpn/server.sock)
   --openvpn.auth-pending-timeout duration
     	How long OpenVPN server wait until user is authenticated (env: CONFIG_OPENVPN_AUTH__PENDING__TIMEOUT) (default 3m0s)
   --openvpn.auth-token-user
     	Override the username of a session with the username from the token by using auth-token-user, if the client username is empty (env: CONFIG_OPENVPN_AUTH__TOKEN__USER) (default true)
   --openvpn.bypass.common-names value
     	bypass oauth authentication for CNs. Comma separated list. (env: CONFIG_OPENVPN_BYPASS_COMMON__NAMES)
+  --openvpn.client-config.enabled
+    	If true, openvpn-auth-oauth2 will read the CCD directory for additional configuration. This function mimic the client-config-dir directive in OpenVPN. (env: CONFIG_OPENVPN_CLIENT__CONFIG_ENABLED)
+  --openvpn.client-config.path value
+    	Path to the CCD directory. openvpn-auth-oauth2 will look for an file with an .conf suffix and returns the content back. (env: CONFIG_OPENVPN_CLIENT__CONFIG_PATH)
+  --openvpn.client-config.token-claim string
+    	If non-empty, the value of the token claim is used to lookup the configuration file in the CCD directory. If empty, the common name is used. (env: CONFIG_OPENVPN_CLIENT__CONFIG_TOKEN__CLAIM)
   --openvpn.common-name.environment-variable-name string
     	Name of the environment variable in the OpenVPN management interface which contains the common name. If username-as-common-name is enabled, this should be set to 'username' to use the username as common name. Other values like 'X509_0_emailAddress' are supported. See https://openvpn.net/community-resources/reference-manual-for-openvpn-2-6/#environmental-variables for more information. (env: CONFIG_OPENVPN_COMMON__NAME_ENVIRONMENT__VARIABLE__NAME) (default "common_name")
   --openvpn.common-name.mode value
     	If common names are too long, use md5/sha1 to hash them or omit to skip them. If omit, oauth2.validate.common-name does not work anymore. Values: [plain,omit] (env: CONFIG_OPENVPN_COMMON__NAME_MODE) (default plain)
   --openvpn.override-username
     	Requires OpenVPN Server 2.7! If true, openvpn-auth-oauth2 use the override-username command to set the username in OpenVPN connection. This is useful to use real usernames in OpenVPN statistics. The username will be set after client configs are read. Read openvpn man page for limitations of the override-username. (env: CONFIG_OPENVPN_OVERRIDE__USERNAME)
-  --openvpn.pass-through.address string
-    	The address of the pass-through socket. Must start with unix:// or tcp:// (env: CONFIG_OPENVPN_PASS__THROUGH_ADDRESS) (default "unix:/run/openvpn-auth-oauth2/server.sock")
+  --openvpn.pass-through.address value
+    	The address of the pass-through socket. Must start with unix:// or tcp:// (env: CONFIG_OPENVPN_PASS__THROUGH_ADDRESS) (default unix:/run/openvpn-auth-oauth2/server.sock)
   --openvpn.pass-through.enabled
     	If true, openvpn-auth-oauth2 will setup a pass-through socket for the OpenVPN management interface. (env: CONFIG_OPENVPN_PASS__THROUGH_ENABLED)
   --openvpn.pass-through.password value
@@ -325,3 +335,7 @@ See [Layout Customization](Layout%20Customization) for more information
 ## Non-interactive session refresh
 
 See [Non-interactive session refresh](Non-interactive%20session%20refresh) for more information.
+
+## Client specific configuration
+
+See [Client specific configuration](Client%20specific%20configuration) for more information.
diff --git a/internal/config/defaults.go b/internal/config/defaults.go
index 66e7a49..72123e0 100644
--- a/internal/config/defaults.go
+++ b/internal/config/defaults.go
@@ -3,6 +3,7 @@ package config
 import (
 	"log/slog"
 	"net/url"
+	"os"
 	"text/template"
 	"time"
 
@@ -47,12 +48,16 @@ var Defaults = Config{
 		}},
 		AuthTokenUser:      true,
 		AuthPendingTimeout: 3 * time.Minute,
+		ClientConfig: OpenVPNConfig{
+			Enabled: false,
+			Path:    types.FS{FS: os.DirFS("/etc/openvpn-auth-oauth2/client-config-dir/")},
+		},
 		CommonName: OpenVPNCommonName{
 			EnvironmentVariableName: "common_name",
 			Mode:                    CommonNameModePlain,
 		},
 		OverrideUsername: false,
-		Bypass: OpenVpnBypass{
+		Bypass: OpenVPNBypass{
 			CommonNames: make([]string, 0),
 		},
 		Passthrough: OpenVPNPassthrough{
diff --git a/internal/config/flags.go b/internal/config/flags.go
index 1e6ac25..5059c2f 100644
--- a/internal/config/flags.go
+++ b/internal/config/flags.go
@@ -140,6 +140,24 @@ func (c *Config) flagSetOpenVPN(flagSet *flag.FlagSet) {
 		lookupEnvOrDefault("openvpn.bypass.common-names", c.OpenVpn.Bypass.CommonNames),
 		"bypass oauth authentication for CNs. Comma separated list.",
 	)
+	flagSet.BoolVar(
+		&c.OpenVpn.ClientConfig.Enabled,
+		"openvpn.client-config.enabled",
+		lookupEnvOrDefault("openvpn.client-config.enabled", c.OpenVpn.ClientConfig.Enabled),
+		"If true, openvpn-auth-oauth2 will read the CCD directory for additional configuration. This function mimic the client-config-dir directive in OpenVPN.",
+	)
+	flagSet.TextVar(
+		&c.OpenVpn.ClientConfig.Path,
+		"openvpn.client-config.path",
+		lookupEnvOrDefault("openvpn.client-config.path", c.OpenVpn.ClientConfig.Path),
+		"Path to the CCD directory. openvpn-auth-oauth2 will look for an file with an .conf suffix and returns the content back.",
+	)
+	flagSet.StringVar(
+		&c.OpenVpn.ClientConfig.TokenClaim,
+		"openvpn.client-config.token-claim",
+		lookupEnvOrDefault("openvpn.client-config.token-claim", c.OpenVpn.ClientConfig.TokenClaim),
+		"If non-empty, the value of the token claim is used to lookup the configuration file in the CCD directory. If empty, the common name is used.",
+	)
 	flagSet.StringVar(
 		&c.OpenVpn.CommonName.EnvironmentVariableName,
 		"openvpn.common-name.environment-variable-name",
diff --git a/internal/config/types.go b/internal/config/types.go
index e882e99..964e2fe 100644
--- a/internal/config/types.go
+++ b/internal/config/types.go
@@ -51,7 +51,8 @@ type Log struct {
 type OpenVpn struct {
 	Addr               types.URL          `json:"addr"                 yaml:"addr"`
 	Password           types.Secret       `json:"password"             yaml:"password"`
-	Bypass             OpenVpnBypass      `json:"bypass"               yaml:"bypass"`
+	Bypass             OpenVPNBypass      `json:"bypass"               yaml:"bypass"`
+	ClientConfig       OpenVPNConfig      `json:"client-config"        yaml:"client-config"`
 	AuthTokenUser      bool               `json:"auth-token-user"      yaml:"auth-token-user"`
 	AuthPendingTimeout time.Duration      `json:"auth-pending-timeout" yaml:"auth-pending-timeout"`
 	OverrideUsername   bool               `json:"override-username"    yaml:"override-username"`
@@ -60,9 +61,14 @@ type OpenVpn struct {
 	CommandTimeout     time.Duration      `json:"command-timeout"      yaml:"command-timeout"`
 }
 
-type OpenVpnBypass struct {
+type OpenVPNBypass struct {
 	CommonNames types.StringSlice `json:"common-names" yaml:"common-names"`
 }
+type OpenVPNConfig struct {
+	Enabled    bool     `json:"enabled"     yaml:"enabled"`
+	TokenClaim string   `json:"token-claim" yaml:"token-claim"`
+	Path       types.FS `json:"path"        yaml:"path"`
+}
 
 type OpenVPNCommonName struct {
 	EnvironmentVariableName string                `json:"environment-variable-name" yaml:"environment-variable-name"`
diff --git a/internal/config/validate.go b/internal/config/validate.go
index fcf13b3..b01d2b6 100644
--- a/internal/config/validate.go
+++ b/internal/config/validate.go
@@ -107,6 +107,19 @@ func validateOAuth2Config(conf Config) error {
 		}
 	}
 
+	if conf.OpenVpn.ClientConfig.Enabled {
+		if conf.OpenVpn.CommonName.Mode == CommonNameModeOmit {
+			return errors.New("openvpn.common-name.mode: omit is not supported with openvpn.ccd.enabled")
+		}
+
+		file, err := conf.OpenVpn.ClientConfig.Path.Open(".")
+		if err != nil {
+			return fmt.Errorf("openvpn.ccd.path: %w", err)
+		}
+
+		_ = file.Close()
+	}
+
 	return nil
 }
 
diff --git a/internal/oauth2/handler.go b/internal/oauth2/handler.go
index 895cdfb..8c432c6 100644
--- a/internal/oauth2/handler.go
+++ b/internal/oauth2/handler.go
@@ -21,7 +21,7 @@ import (
 )
 
 type openvpnManagementClient interface {
-	AcceptClient(logger *slog.Logger, client state.ClientIdentifier, username string)
+	AcceptClient(logger *slog.Logger, client state.ClientIdentifier, reAuth bool, username string)
 	DenyClient(logger *slog.Logger, client state.ClientIdentifier, reason string)
 }
 
@@ -52,7 +52,7 @@ func (c Client) OAuth2Start() http.Handler {
 			slog.String("ip", fmt.Sprintf("%s:%s", session.IPAddr, session.IPPort)),
 			slog.Uint64("cid", session.Client.CID),
 			slog.Uint64("kid", session.Client.KID),
-			slog.String("common_name", session.CommonName),
+			slog.String("common_name", session.Client.CommonName),
 		)
 
 		if c.conf.HTTP.Check.IPAddr {
@@ -116,7 +116,7 @@ func (c Client) OAuth2Callback() http.Handler {
 			slog.String("ip", fmt.Sprintf("%s:%s", session.IPAddr, session.IPPort)),
 			slog.Uint64("cid", session.Client.CID),
 			slog.Uint64("kid", session.Client.KID),
-			slog.String("common_name", session.CommonName),
+			slog.String("common_name", session.Client.CommonName),
 			slog.String("session_id", session.Client.SessionID),
 			slog.String("session_state", session.SessionState),
 		)
@@ -181,10 +181,10 @@ func (c Client) postCodeExchangeHandler(logger *slog.Logger, session state.State
 
 		username := user.PreferredUsername
 		if username == "" {
-			username = session.CommonName
+			username = session.Client.CommonName
 		}
 
-		c.openvpn.AcceptClient(logger, session.Client, username)
+		c.openvpn.AcceptClient(logger, session.Client, false, username)
 		c.postCodeExchangeHandlerStoreRefreshToken(ctx, logger, session, clientID, tokens)
 		c.writeHTTPSuccess(w, logger)
 	}
@@ -240,7 +240,7 @@ func (c Client) httpErrorHandler(w http.ResponseWriter, httpStatus int, errorTyp
 			slog.String("ip", fmt.Sprintf("%s:%s", session.IPAddr, session.IPPort)),
 			slog.Uint64("cid", session.Client.CID),
 			slog.Uint64("kid", session.Client.KID),
-			slog.String("common_name", session.CommonName),
+			slog.String("common_name", session.Client.CommonName),
 		)
 
 		c.openvpn.DenyClient(logger, session.Client, "client rejected")
diff --git a/internal/oauth2/providers/generic/check.go b/internal/oauth2/providers/generic/check.go
index 3bdb140..f3996c4 100644
--- a/internal/oauth2/providers/generic/check.go
+++ b/internal/oauth2/providers/generic/check.go
@@ -85,7 +85,7 @@ func (p Provider) CheckCommonName(session state.State, tokens *oidc.Tokens[*idto
 		return nil
 	}
 
-	if session.CommonName == "" || session.CommonName == types2.CommonNameModeOmitValue {
+	if session.Client.CommonName == "" || session.Client.CommonName == types2.CommonNameModeOmitValue {
 		return fmt.Errorf("common_name %w: openvpn client is empty", oauth2.ErrMismatch)
 	}
 
@@ -105,13 +105,13 @@ func (p Provider) CheckCommonName(session state.State, tokens *oidc.Tokens[*idto
 	tokenCommonName = utils.TransformCommonName(p.Conf.OpenVpn.CommonName.Mode, tokenCommonName)
 
 	if !p.Conf.OAuth2.Validate.CommonNameCaseSensitive {
-		session.CommonName = strings.ToLower(session.CommonName)
+		session.Client.CommonName = strings.ToLower(session.Client.CommonName)
 		tokenCommonName = strings.ToLower(tokenCommonName)
 	}
 
-	if tokenCommonName != session.CommonName {
+	if tokenCommonName != session.Client.CommonName {
 		return fmt.Errorf("common_name %w: openvpn client: %s - oidc token: %s",
-			oauth2.ErrMismatch, session.CommonName, tokenCommonName)
+			oauth2.ErrMismatch, session.Client.CommonName, tokenCommonName)
 	}
 
 	return nil
diff --git a/internal/oauth2/refresh.go b/internal/oauth2/refresh.go
index 5cb027e..52a32de 100644
--- a/internal/oauth2/refresh.go
+++ b/internal/oauth2/refresh.go
@@ -55,8 +55,8 @@ func (c Client) RefreshClientAuth(ctx context.Context, logger *slog.Logger, clie
 	}
 
 	session := state.New(
-		state.ClientIdentifier{CID: client.CID, KID: client.KID, SessionID: client.SessionID},
-		client.IPAddr, client.IPPort, client.CommonName, client.SessionState,
+		state.ClientIdentifier{CID: client.CID, KID: client.KID, SessionID: client.SessionID, CommonName: client.CommonName},
+		client.IPAddr, client.IPPort, client.SessionState,
 	)
 
 	user, err := c.provider.GetUser(ctx, logger, tokens)
diff --git a/internal/openvpn/callbacks.go b/internal/openvpn/callbacks.go
index db37664..e500b06 100644
--- a/internal/openvpn/callbacks.go
+++ b/internal/openvpn/callbacks.go
@@ -3,19 +3,48 @@ package openvpn
 import (
 	"encoding/base64"
 	"fmt"
+	"io"
 	"log/slog"
+	"strings"
 
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
 )
 
-func (c *Client) AcceptClient(logger *slog.Logger, client state.ClientIdentifier, username string) {
-	logger.Info(fmt.Sprintf("accept OpenVPN client cid %d, kid %d", client.CID, client.KID))
+// AcceptClient accepts an OpenVPN client connection.
+// It reads the client configuration from the CCD path if enabled.
+func (c *Client) AcceptClient(logger *slog.Logger, client state.ClientIdentifier, reAuth bool, username string) {
+	if reAuth {
+		logger.Info("client re-authentication")
 
+		if _, err := c.SendCommandf(`client-auth-nt %d %d`, client.CID, client.KID); err != nil {
+			logger.Warn("failed to accept client",
+				slog.Any("error", err),
+			)
+		}
+
+		return
+	}
+
+	c.acceptClientAuth(logger, client, username)
+}
+
+//nolint:cyclop
+func (c *Client) acceptClientAuth(logger *slog.Logger, client state.ClientIdentifier, username string) {
 	var (
 		err           error
 		tokenUsername string
 	)
 
+	logger.Info("client authentication")
+
+	clientConfig, err := c.readClientConfig(username)
+	if err != nil {
+		logger.Debug("failed to read client config",
+			slog.String("username", username),
+			slog.Any("error", err),
+		)
+	}
+
 	if c.conf.OpenVpn.AuthTokenUser && client.UsernameIsDefined == 0 {
 		tokenUsername = base64.StdEncoding.EncodeToString([]byte(username))
 		if tokenUsername == "" {
@@ -24,13 +53,27 @@ func (c *Client) AcceptClient(logger *slog.Logger, client state.ClientIdentifier
 		}
 	}
 
-	switch {
-	case tokenUsername == "":
+	if c.conf.OpenVpn.OverrideUsername && username != "" {
+		clientConfig = append(clientConfig, fmt.Sprintf(`override-username "%s"`, username))
+	} else if tokenUsername != "" {
+		clientConfig = append(clientConfig, fmt.Sprintf(`push "auth-token-user %s"`, tokenUsername))
+	}
+
+	if len(clientConfig) == 0 {
 		_, err = c.SendCommandf(`client-auth-nt %d %d`, client.CID, client.KID)
-	case c.conf.OpenVpn.OverrideUsername:
-		_, err = c.SendCommandf("client-auth %d %d\r\noverride-username \"%s\"\r\nEND", client.CID, client.KID, username)
-	default:
-		_, err = c.SendCommandf("client-auth %d %d\r\npush \"auth-token-user %s\"\r\nEND", client.CID, client.KID, tokenUsername)
+	} else {
+		sb := strings.Builder{}
+
+		sb.WriteString(fmt.Sprintf("client-auth %d %d\r\n", client.CID, client.KID))
+
+		for _, line := range clientConfig {
+			sb.WriteString(strings.TrimSpace(line))
+			sb.WriteString("\r\n")
+		}
+
+		sb.WriteString("END")
+
+		_, err = c.SendCommand(sb.String(), false)
 	}
 
 	if err != nil {
@@ -50,3 +93,21 @@ func (c *Client) DenyClient(logger *slog.Logger, client state.ClientIdentifier,
 		)
 	}
 }
+
+func (c *Client) readClientConfig(username string) ([]string, error) {
+	if !c.conf.OpenVpn.ClientConfig.Enabled || c.conf.OpenVpn.ClientConfig.Path.IsEmpty() || len(username) == 0 {
+		return make([]string, 0), nil
+	}
+
+	clientConfigFile, err := c.conf.OpenVpn.ClientConfig.Path.Open(username + ".conf")
+	if err != nil {
+		return make([]string, 0), fmt.Errorf("failed to open client config file: %w", err)
+	}
+
+	clientConfigBytes, err := io.ReadAll(clientConfigFile)
+	if err != nil {
+		return make([]string, 0), fmt.Errorf("failed to read client config file: %w", err)
+	}
+
+	return strings.Split(strings.TrimSpace(strings.ReplaceAll(string(clientConfigBytes), "\r", "")), "\n"), nil
+}
diff --git a/internal/openvpn/client.go b/internal/openvpn/client.go
index 1705c2b..38f0313 100644
--- a/internal/openvpn/client.go
+++ b/internal/openvpn/client.go
@@ -49,7 +49,7 @@ func (c *Client) handleClientAuthentication(ctx context.Context, logger *slog.Lo
 	// Check if the client is allowed to bypass authentication. If so, accept the client.
 	if c.checkAuthBypass(client) {
 		logger.LogAttrs(ctx, slog.LevelInfo, "client bypass authentication")
-		c.AcceptClient(logger, state.ClientIdentifier{CID: client.CID, KID: client.KID}, client.CommonName)
+		c.AcceptClient(logger, state.ClientIdentifier{CID: client.CID, KID: client.KID}, true, client.CommonName)
 
 		return
 	}
@@ -65,8 +65,7 @@ func (c *Client) handleClientAuthentication(ctx context.Context, logger *slog.Lo
 
 	// Check if the client is already authenticated and refresh the client's authentication if enabled.
 	// If the client is successfully re-authenticated, accept the client.
-	ok, err := c.silentReAuthentication(ctx, logger, client)
-	if err != nil {
+	if ok, err := c.silentReAuthentication(ctx, logger, client); err != nil {
 		c.DenyClient(logger, state.ClientIdentifier{CID: client.CID, KID: client.KID}, ReasonStateExpiredOrInvalid)
 
 		logger.LogAttrs(ctx, slog.LevelError, "error refreshing client auth",
@@ -75,7 +74,7 @@ func (c *Client) handleClientAuthentication(ctx context.Context, logger *slog.Lo
 
 		return
 	} else if ok {
-		c.AcceptClient(logger, state.ClientIdentifier{CID: client.CID, KID: client.KID}, client.CommonName)
+		c.AcceptClient(logger, state.ClientIdentifier{CID: client.CID, KID: client.KID}, true, client.CommonName)
 
 		return
 	}
@@ -94,14 +93,15 @@ func (c *Client) handleClientAuthentication(ctx context.Context, logger *slog.Lo
 // startClientAuth initiates the authentication process for the client.
 // The openvpn-auth-oauth2 plugin will send a client-pending-auth command to the OpenVPN management interface.
 func (c *Client) startClientAuth(ctx context.Context, logger *slog.Logger, client connection.Client) error {
+	commonName := utils.TransformCommonName(c.conf.OpenVpn.CommonName.Mode, client.CommonName)
+
 	clientIdentifier := state.ClientIdentifier{
-		CID:       client.CID,
-		KID:       client.KID,
-		SessionID: client.SessionID,
+		CID:        client.CID,
+		KID:        client.KID,
+		SessionID:  client.SessionID,
+		CommonName: commonName,
 	}
 
-	commonName := utils.TransformCommonName(c.conf.OpenVpn.CommonName.Mode, client.CommonName)
-
 	var (
 		ipAddr string
 		ipPort string
@@ -112,7 +112,7 @@ func (c *Client) startClientAuth(ctx context.Context, logger *slog.Logger, clien
 		ipPort = client.IPPort
 	}
 
-	session := state.New(clientIdentifier, ipAddr, ipPort, commonName, client.SessionState)
+	session := state.New(clientIdentifier, ipAddr, ipPort, client.SessionState)
 
 	encodedSession, err := session.Encode(c.conf.HTTP.Secret.String())
 	if err != nil {
diff --git a/internal/state/state.go b/internal/state/state.go
index 671e41f..e86f429 100644
--- a/internal/state/state.go
+++ b/internal/state/state.go
@@ -11,30 +11,41 @@ import (
 	"github.com/zitadel/oidc/v3/pkg/crypto"
 )
 
+// State represents the context and security information associated with an OAuth2 login flow.
+//
+// The `State` value is passed to the `state` GET parameter during the OAuth2 login flow.
+// It ensures that the client initiating the login flow is the same client completing it,
+// thus preventing CSRF (Cross-Site Request Forgery) attacks. The `State` value is returned
+// by the OAuth2 Identity Provider (IDP) in the redirect URL.
+//
+// To prevent tampering, the `State` is protected using AES encryption.
 type State struct {
-	Client       ClientIdentifier
-	IPAddr       string
-	IPPort       string
-	CommonName   string
-	SessionState string
-	Issued       int64
+	Client       ClientIdentifier // Detailed information about the client initiating the flow.
+	IPAddr       string           // IP address of the client.
+	IPPort       string           // Port used by the client.
+	SessionState string           // Session identifier for tracking the login state.
+	Issued       int64            // Timestamp when the state was created.
 }
 
+// ClientIdentifier holds detailed information about the client initiating an OAuth2 login flow.
+//
+// This struct provides more context for the client and can be passed to [github.com/jkroepke/openvpn-auth-oauth2/internal/openvpn.Client.AcceptClient].
 type ClientIdentifier struct {
-	CID                  uint64
-	KID                  uint64
-	SessionID            string
-	UsernameIsDefined    int
-	AuthFailedReasonFile string
-	AuthControlFile      string
+	CID                  uint64 // Unique identifier for the client.
+	KID                  uint64 // Identifier for cryptographic keys.
+	SessionID            string // Unique session identifier.
+	CommonName           string // Human-readable name for the client.
+	UsernameIsDefined    int    // Flag indicating if the username is defined.
+	AuthFailedReasonFile string // Path or reference explaining authentication failure reasons.
+	AuthControlFile      string // Path or reference for authentication control settings.
 }
 
-func New(client ClientIdentifier, ipAddr, ipPort, commonName, sessionState string) State {
+// New returns a new State.
+func New(client ClientIdentifier, ipAddr, ipPort, sessionState string) State {
 	return State{
 		Client:       client,
 		IPAddr:       ipAddr,
 		IPPort:       ipPort,
-		CommonName:   commonName,
 		SessionState: sessionState,
 		Issued:       time.Now().Round(time.Second).Unix(),
 	}
@@ -66,12 +77,12 @@ func (state *State) Encode(secretKey string) (string, error) {
 	data.WriteString(" ")
 	data.WriteString(strconv.Itoa(state.Client.UsernameIsDefined))
 	data.WriteString(" ")
+	data.WriteString(encodeString(state.Client.CommonName))
+	data.WriteString(" ")
 	data.WriteString(encodeString(state.IPAddr))
 	data.WriteString(" ")
 	data.WriteString(encodeString(state.IPPort))
 	data.WriteString(" ")
-	data.WriteString(encodeString(state.CommonName))
-	data.WriteString(" ")
 	data.WriteString(encodeSessionState(state.SessionState))
 	data.WriteString(" ")
 	data.WriteString(strconv.FormatInt(state.Issued, 10))
@@ -103,9 +114,9 @@ func (state *State) decode(encodedState, secretKey string) error {
 		&state.Client.AuthControlFile,
 		&state.Client.SessionID,
 		&state.Client.UsernameIsDefined,
+		&state.Client.CommonName,
 		&state.IPAddr,
 		&state.IPPort,
-		&state.CommonName,
 		&state.SessionState,
 		&state.Issued,
 	)
@@ -116,9 +127,9 @@ func (state *State) decode(encodedState, secretKey string) error {
 	state.Client.AuthFailedReasonFile = decodeString(state.Client.AuthFailedReasonFile)
 	state.Client.AuthControlFile = decodeString(state.Client.AuthControlFile)
 	state.Client.SessionID = decodeString(state.Client.SessionID)
+	state.Client.CommonName = decodeString(state.Client.CommonName)
 	state.IPAddr = decodeString(state.IPAddr)
 	state.IPPort = decodeString(state.IPPort)
-	state.CommonName = decodeString(state.CommonName)
 	state.SessionState = decodeSessionState(state.SessionState)
 
 	issuedSince := time.Since(time.Unix(state.Issued, 0))
diff --git a/packaging/etc/openvpn-auth-oauth2/config.yaml b/packaging/etc/openvpn-auth-oauth2/config.yaml
index 4150cf5..e40fcfe 100644
--- a/packaging/etc/openvpn-auth-oauth2/config.yaml
+++ b/packaging/etc/openvpn-auth-oauth2/config.yaml
@@ -63,6 +63,10 @@
 #    common-names:
 #      - "test"
 #      - "test2"
+#  ccd:
+#    enabled: false
+#    path: "/etc/openvpn-auth-oauth2/client-config/"
+#    token-claim: ""
 #  common-name:
 #    environment-variable-name: common_name
 #    mode: plain


Test patch:
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 2f86bdf..38e8d4e 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -109,6 +109,10 @@ openvpn:
         common-names:
         - "test"
         - "test2"
+    client-config:
+        enabled: true
+        token-claim: sub
+        path: "."
     common-name:
         environment-variable-name: X509_0_emailAddress
         mode: omit
@@ -168,9 +172,19 @@ http:
 						Path:     "/run/openvpn/server2.sock",
 						OmitHost: false,
 					}},
-					Bypass: config.OpenVpnBypass{
+					Bypass: config.OpenVPNBypass{
 						CommonNames: []string{"test", "test2"},
 					},
+					ClientConfig: config.OpenVPNConfig{
+						Enabled:    true,
+						TokenClaim: "sub",
+						Path: func() types.FS {
+							dirFS, err := types.NewFS(".")
+							require.NoError(t, err)
+
+							return dirFS
+						}(),
+					},
 					Password:           "1jd93h5b6s82lf03jh5b2hf9",
 					AuthTokenUser:      true,
 					AuthPendingTimeout: 2 * time.Minute,
diff --git a/internal/oauth2/handler_test.go b/internal/oauth2/handler_test.go
index 9994f52..7110753 100644
--- a/internal/oauth2/handler_test.go
+++ b/internal/oauth2/handler_test.go
@@ -7,6 +7,7 @@ import (
 	"io"
 	"net/http"
 	"testing"
+	"testing/fstest"
 	"time"
 
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
@@ -47,7 +48,7 @@ func TestHandler(t *testing.T) {
 
 				return conf
 			}(),
-			state.New(state.ClientIdentifier{CID: 0, KID: 1}, "127.0.0.1", "12345", "name", ""),
+			state.New(state.ClientIdentifier{CID: 0, KID: 1, CommonName: "name"}, "127.0.0.1", "12345", ""),
 			false,
 			"",
 			true,
@@ -71,7 +72,7 @@ func TestHandler(t *testing.T) {
 
 				return conf
 			}(),
-			state.New(state.ClientIdentifier{CID: 0, KID: 1, UsernameIsDefined: 1}, "127.0.0.1", "12345", "name", ""),
+			state.New(state.ClientIdentifier{CID: 0, KID: 1, UsernameIsDefined: 1, CommonName: "name"}, "127.0.0.1", "12345", ""),
 			false,
 			"",
 			true,
@@ -98,7 +99,7 @@ func TestHandler(t *testing.T) {
 
 				return conf
 			}(),
-			state.New(state.ClientIdentifier{CID: 0, KID: 1}, "127.0.0.1", "12345", "name", ""),
+			state.New(state.ClientIdentifier{CID: 0, KID: 1, CommonName: "name"}, "127.0.0.1", "12345", ""),
 			false,
 			"",
 			true,
@@ -126,7 +127,7 @@ func TestHandler(t *testing.T) {
 
 				return conf
 			}(),
-			state.New(state.ClientIdentifier{CID: 0, KID: 1}, "127.0.0.1", "12345", "name", ""),
+			state.New(state.ClientIdentifier{CID: 0, KID: 1, CommonName: "name"}, "127.0.0.1", "12345", ""),
 			false,
 			"",
 			true,
@@ -150,7 +151,7 @@ func TestHandler(t *testing.T) {
 
 				return conf
 			}(),
-			state.New(state.ClientIdentifier{CID: 0, KID: 1}, "127.0.0.1", "12345", "name", ""),
+			state.New(state.ClientIdentifier{CID: 0, KID: 1, CommonName: "name"}, "127.0.0.1", "12345", ""),
 			false,
 			"",
 			true,
@@ -175,7 +176,7 @@ func TestHandler(t *testing.T) {
 
 				return conf
 			}(),
-			state.New(state.ClientIdentifier{CID: 0, KID: 1}, "127.0.0.2", "12345", "name", ""),
+			state.New(state.ClientIdentifier{CID: 0, KID: 1, CommonName: "name"}, "127.0.0.2", "12345", ""),
 			false,
 			"127.0.0.2",
 			true,
@@ -200,7 +201,7 @@ func TestHandler(t *testing.T) {
 
 				return conf
 			}(),
-			state.New(state.ClientIdentifier{CID: 0, KID: 1}, "127.0.0.2", "12345", "name", ""),
+			state.New(state.ClientIdentifier{CID: 0, KID: 1, CommonName: "name"}, "127.0.0.2", "12345", ""),
 			false,
 			"127.0.0.2",
 			false,
@@ -225,7 +226,69 @@ func TestHandler(t *testing.T) {
 
 				return conf
 			}(),
-			state.New(state.ClientIdentifier{CID: 0, KID: 1}, "127.0.0.2", "12345", "name", ""),
+			state.New(state.ClientIdentifier{CID: 0, KID: 1, CommonName: "name"}, "127.0.0.2", "12345", ""),
+			false,
+			"127.0.0.2, 8.8.8.8",
+			true,
+			true,
+		},
+		{
+			"with client config found",
+			func() config.Config {
+				conf := config.Defaults
+				conf.HTTP.Secret = testutils.Secret
+				conf.HTTP.Check.IPAddr = true
+				conf.HTTP.EnableProxyHeaders = true
+				conf.OAuth2.Provider = generic.Name
+				conf.OAuth2.Endpoints = config.OAuth2Endpoints{}
+				conf.OAuth2.Scopes = []string{"openid", "profile"}
+				conf.OAuth2.Validate.Groups = make([]string, 0)
+				conf.OAuth2.Validate.Roles = make([]string, 0)
+				conf.OAuth2.Validate.Issuer = true
+				conf.OAuth2.Validate.IPAddr = false
+				conf.OpenVpn.Bypass.CommonNames = make([]string, 0)
+				conf.OpenVpn.AuthTokenUser = true
+				conf.OpenVpn.ClientConfig.Enabled = true
+				conf.OpenVpn.ClientConfig.Path = types.FS{
+					FS: fstest.MapFS{
+						"name.conf": &fstest.MapFile{
+							Data: []byte("push \"ping 60\"\npush \"ping-restart 180\"\r\npush \"ping-timer-rem\" 0\r\n"),
+						},
+					},
+				}
+
+				return conf
+			}(),
+			state.New(state.ClientIdentifier{CID: 0, KID: 1, CommonName: "name"}, "127.0.0.2", "12345", ""),
+			false,
+			"127.0.0.2, 8.8.8.8",
+			true,
+			true,
+		},
+		{
+			"with client config not found",
+			func() config.Config {
+				conf := config.Defaults
+				conf.HTTP.Secret = testutils.Secret
+				conf.HTTP.Check.IPAddr = true
+				conf.HTTP.EnableProxyHeaders = true
+				conf.OAuth2.Provider = generic.Name
+				conf.OAuth2.Endpoints = config.OAuth2Endpoints{}
+				conf.OAuth2.Scopes = []string{"openid", "profile"}
+				conf.OAuth2.Validate.Groups = make([]string, 0)
+				conf.OAuth2.Validate.Roles = make([]string, 0)
+				conf.OAuth2.Validate.Issuer = true
+				conf.OAuth2.Validate.IPAddr = false
+				conf.OpenVpn.Bypass.CommonNames = make([]string, 0)
+				conf.OpenVpn.AuthTokenUser = true
+				conf.OpenVpn.ClientConfig.Enabled = true
+				conf.OpenVpn.ClientConfig.Path = types.FS{
+					FS: fstest.MapFS{},
+				}
+
+				return conf
+			}(),
+			state.New(state.ClientIdentifier{CID: 0, KID: 1, CommonName: "client"}, "127.0.0.2", "12345", ""),
 			false,
 			"127.0.0.2, 8.8.8.8",
 			true,
@@ -420,6 +483,19 @@ func TestHandler(t *testing.T) {
 			case tc.state.Client.UsernameIsDefined == 1:
 				testutils.ExpectMessage(t, managementInterfaceConn, reader, "client-auth-nt 0 1")
 				testutils.SendMessage(t, managementInterfaceConn, "SUCCESS: client-auth command succeeded")
+			case tc.conf.OpenVpn.ClientConfig.Enabled:
+				if tc.state.Client.CommonName == "name" {
+					testutils.ExpectMessage(t, managementInterfaceConn, reader, "client-auth 0 1\r\n"+
+						"push \"ping 60\"\r\n"+
+						"push \"ping-restart 180\"\r\n"+
+						"push \"ping-timer-rem\" 0\r\n"+
+						"push \"auth-token-user bmFtZQ==\"\r\n"+
+						"END")
+					testutils.SendMessage(t, managementInterfaceConn, "SUCCESS: client-auth command succeeded")
+				} else {
+					testutils.ExpectMessage(t, managementInterfaceConn, reader, "client-auth 0 1\r\npush \"auth-token-user Y2xpZW50\"\r\nEND")
+					testutils.SendMessage(t, managementInterfaceConn, "SUCCESS: client-auth command succeeded")
+				}
 			default:
 				testutils.ExpectMessage(t, managementInterfaceConn, reader, "client-auth 0 1\r\npush \"auth-token-user bmFtZQ==\"\r\nEND")
 				testutils.SendMessage(t, managementInterfaceConn, "SUCCESS: client-auth command succeeded")
diff --git a/internal/oauth2/providers/generic/check_test.go b/internal/oauth2/providers/generic/check_test.go
index f094db6..dcde219 100644
--- a/internal/oauth2/providers/generic/check_test.go
+++ b/internal/oauth2/providers/generic/check_test.go
@@ -117,7 +117,7 @@ func TestInvalidToken(t *testing.T) {
 			userData, err := provider.GetUser(t.Context(), testutils.NewTestLogger().Logger, tt.token)
 			require.NoError(t, err)
 
-			err = provider.CheckUser(t.Context(), state.State{CommonName: "user"}, userData, tt.token)
+			err = provider.CheckUser(t.Context(), state.State{Client: state.ClientIdentifier{CommonName: "user"}}, userData, tt.token)
 			if tt.err == nil {
 				require.NoError(t, err)
 			} else {
@@ -378,7 +378,7 @@ func TestValidateCommonName(t *testing.T) {
 			}
 
 			session := state.State{
-				CommonName: tt.requiredCommonName,
+				Client: state.ClientIdentifier{CommonName: tt.requiredCommonName},
 			}
 
 			provider, err := generic.NewProvider(t.Context(), tt.conf, http.DefaultClient)
diff --git a/internal/oauth2/refresh_test.go b/internal/oauth2/refresh_test.go
index 858a731..0112d6a 100644
--- a/internal/oauth2/refresh_test.go
+++ b/internal/oauth2/refresh_test.go
@@ -342,31 +342,7 @@ func TestRefreshReAuth(t *testing.T) {
 				return
 			}
 
-			switch {
-			case tc.conf.OpenVpn.OverrideUsername:
-				testutils.ExpectMessage(t, managementInterfaceConn, reader, "client-auth 1 3")
-
-				if tc.clientCommonName == "" {
-					testutils.ExpectMessage(t, managementInterfaceConn, reader, "override-username \"username\"")
-				} else {
-					testutils.ExpectMessage(t, managementInterfaceConn, reader, "override-username \"test\"")
-				}
-
-				testutils.ExpectMessage(t, managementInterfaceConn, reader, "END")
-			case tc.conf.OpenVpn.AuthTokenUser:
-				testutils.ExpectMessage(t, managementInterfaceConn, reader, "client-auth 1 3")
-
-				if tc.clientCommonName == "" {
-					testutils.ExpectMessage(t, managementInterfaceConn, reader, "push \"auth-token-user dXNlcm5hbWUK\"")
-				} else {
-					testutils.ExpectMessage(t, managementInterfaceConn, reader, "push \"auth-token-user dGVzdA==\"")
-				}
-
-				testutils.ExpectMessage(t, managementInterfaceConn, reader, "END")
-			default:
-				testutils.ExpectMessage(t, managementInterfaceConn, reader, "client-auth-nt 1 3")
-			}
-
+			testutils.ExpectMessage(t, managementInterfaceConn, reader, "client-auth-nt 1 3")
 			testutils.SendMessage(t, managementInterfaceConn, "SUCCESS: client-auth command succeeded")
 
 			// Testing ReAuth
@@ -375,31 +351,7 @@ func TestRefreshReAuth(t *testing.T) {
 				tc.clientCommonName,
 			)
 
-			switch {
-			case tc.conf.OpenVpn.OverrideUsername:
-				testutils.ExpectMessage(t, managementInterfaceConn, reader, "client-auth 1 4")
-
-				if tc.clientCommonName == "" {
-					testutils.ExpectMessage(t, managementInterfaceConn, reader, "override-username \"username\"")
-				} else {
-					testutils.ExpectMessage(t, managementInterfaceConn, reader, "override-username \"test\"")
-				}
-
-				testutils.ExpectMessage(t, managementInterfaceConn, reader, "END")
-			case tc.conf.OpenVpn.AuthTokenUser:
-				testutils.ExpectMessage(t, managementInterfaceConn, reader, "client-auth 1 4")
-
-				if tc.clientCommonName == "" {
-					testutils.ExpectMessage(t, managementInterfaceConn, reader, "push \"auth-token-user dXNlcm5hbWUK\"")
-				} else {
-					testutils.ExpectMessage(t, managementInterfaceConn, reader, "push \"auth-token-user dGVzdA==\"")
-				}
-
-				testutils.ExpectMessage(t, managementInterfaceConn, reader, "END")
-			default:
-				testutils.ExpectMessage(t, managementInterfaceConn, reader, "client-auth-nt 1 4")
-			}
-
+			testutils.ExpectMessage(t, managementInterfaceConn, reader, "client-auth-nt 1 4")
 			testutils.SendMessage(t, managementInterfaceConn, "SUCCESS: client-auth command succeeded")
 
 			// Test Disconnect
diff --git a/internal/openvpn/bench_test.go b/internal/openvpn/bench_test.go
index 29e1115..2adeb83 100644
--- a/internal/openvpn/bench_test.go
+++ b/internal/openvpn/bench_test.go
@@ -36,7 +36,7 @@ func BenchmarkOpenVPNHandler(b *testing.B) {
 	conf.HTTP.BaseURL = types.URL{URL: &url.URL{Scheme: "http", Host: "localhost"}}
 	conf.HTTP.Secret = testutils.Secret
 	conf.OpenVpn.Addr = types.URL{URL: &url.URL{Scheme: managementInterface.Addr().Network(), Host: managementInterface.Addr().String()}}
-	conf.OpenVpn.Bypass = config.OpenVpnBypass{CommonNames: make([]string, 0)}
+	conf.OpenVpn.Bypass = config.OpenVPNBypass{CommonNames: make([]string, 0)}
 
 	tokenStorage := tokenstorage.NewInMemory(ctx, testutils.Secret, time.Hour)
 	_, openVPNClient := testutils.SetupOpenVPNOAuth2Clients(ctx, b, conf, logger.Logger, http.DefaultClient, tokenStorage)
diff --git a/internal/openvpn/main_test.go b/internal/openvpn/main_test.go
index de99ef3..31d7770 100644
--- a/internal/openvpn/main_test.go
+++ b/internal/openvpn/main_test.go
@@ -38,7 +38,7 @@ func TestClientInvalidServer(t *testing.T) {
 		},
 		OpenVpn: config.OpenVpn{
 			Addr:   types.URL{URL: &url.URL{Scheme: "tcp", Host: "127.0.0.1:1"}},
-			Bypass: config.OpenVpnBypass{CommonNames: make([]string, 0)},
+			Bypass: config.OpenVPNBypass{CommonNames: make([]string, 0)},
 		},
 	}
 
@@ -67,7 +67,7 @@ func TestClientFull(t *testing.T) {
 				conf.HTTP.BaseURL = types.URL{URL: &url.URL{Scheme: "http", Host: "localhost"}}
 				conf.HTTP.Secret = testutils.Secret
 				conf.OpenVpn.CommonName.EnvironmentVariableName = config.CommonName
-				conf.OpenVpn.Bypass = config.OpenVpnBypass{CommonNames: make([]string, 0)}
+				conf.OpenVpn.Bypass = config.OpenVPNBypass{CommonNames: make([]string, 0)}
 				conf.OAuth2.Validate.IPAddr = true
 
 				return conf
@@ -82,7 +82,7 @@ func TestClientFull(t *testing.T) {
 				conf.HTTP.BaseURL = types.URL{URL: &url.URL{Scheme: "http", Host: "localhost"}}
 				conf.HTTP.Secret = testutils.Secret
 				conf.OpenVpn.CommonName.EnvironmentVariableName = config.CommonName
-				conf.OpenVpn.Bypass = config.OpenVpnBypass{CommonNames: make([]string, 0)}
+				conf.OpenVpn.Bypass = config.OpenVPNBypass{CommonNames: make([]string, 0)}
 				conf.OpenVpn.Password = testutils.Password
 				conf.OAuth2.Validate.IPAddr = true
 
@@ -99,7 +99,7 @@ func TestClientFull(t *testing.T) {
 				conf.HTTP.BaseURL = types.URL{URL: &url.URL{Scheme: "http", Host: "localhost"}}
 				conf.HTTP.Secret = testutils.Secret
 				conf.OpenVpn.CommonName.EnvironmentVariableName = "username"
-				conf.OpenVpn.Bypass = config.OpenVpnBypass{CommonNames: make([]string, 0)}
+				conf.OpenVpn.Bypass = config.OpenVPNBypass{CommonNames: make([]string, 0)}
 				conf.OAuth2.Validate.IPAddr = true
 
 				return conf
@@ -114,7 +114,7 @@ func TestClientFull(t *testing.T) {
 				conf.HTTP.BaseURL = types.URL{URL: &url.URL{Scheme: "http", Host: "localhost"}}
 				conf.HTTP.Secret = "012345678910111"
 				conf.OpenVpn.CommonName.EnvironmentVariableName = config.CommonName
-				conf.OpenVpn.Bypass = config.OpenVpnBypass{CommonNames: make([]string, 0)}
+				conf.OpenVpn.Bypass = config.OpenVPNBypass{CommonNames: make([]string, 0)}
 				conf.OpenVpn.Password = testutils.Password
 
 				return conf
@@ -130,7 +130,7 @@ func TestClientFull(t *testing.T) {
 				conf.HTTP.BaseURL = types.URL{URL: &url.URL{Scheme: "http", Host: "localhost"}}
 				conf.HTTP.Secret = testutils.Secret
 				conf.OpenVpn.CommonName.EnvironmentVariableName = config.CommonName
-				conf.OpenVpn.Bypass = config.OpenVpnBypass{CommonNames: make([]string, 0)}
+				conf.OpenVpn.Bypass = config.OpenVPNBypass{CommonNames: make([]string, 0)}
 				conf.OpenVpn.Password = testutils.Password
 
 				return conf
@@ -146,7 +146,7 @@ func TestClientFull(t *testing.T) {
 				conf.HTTP.BaseURL = types.URL{URL: &url.URL{Scheme: "http", Host: "localhost", Path: strings.Repeat("a", 255)}}
 				conf.HTTP.Secret = testutils.Secret
 				conf.OpenVpn.CommonName.EnvironmentVariableName = config.CommonName
-				conf.OpenVpn.Bypass = config.OpenVpnBypass{CommonNames: make([]string, 0)}
+				conf.OpenVpn.Bypass = config.OpenVPNBypass{CommonNames: make([]string, 0)}
 				conf.OpenVpn.Password = testutils.Password
 
 				return conf
@@ -162,7 +162,7 @@ func TestClientFull(t *testing.T) {
 				conf.HTTP.BaseURL = types.URL{URL: &url.URL{Scheme: "http", Host: "localhost"}}
 				conf.HTTP.Secret = testutils.Secret
 				conf.OpenVpn.CommonName.EnvironmentVariableName = config.CommonName
-				conf.OpenVpn.Bypass = config.OpenVpnBypass{CommonNames: []string{"bypass"}}
+				conf.OpenVpn.Bypass = config.OpenVPNBypass{CommonNames: []string{"bypass"}}
 				conf.OpenVpn.Password = testutils.Password
 				conf.OpenVpn.AuthTokenUser = false
 
@@ -179,7 +179,7 @@ func TestClientFull(t *testing.T) {
 				conf.HTTP.BaseURL = types.URL{URL: &url.URL{Scheme: "http", Host: "localhost"}}
 				conf.HTTP.Secret = testutils.Secret
 				conf.OpenVpn.CommonName.EnvironmentVariableName = config.CommonName
-				conf.OpenVpn.Bypass = config.OpenVpnBypass{CommonNames: []string{"bypass"}}
+				conf.OpenVpn.Bypass = config.OpenVPNBypass{CommonNames: []string{"bypass"}}
 				conf.OpenVpn.Password = testutils.Password
 				conf.OpenVpn.AuthTokenUser = false
 
@@ -196,7 +196,7 @@ func TestClientFull(t *testing.T) {
 				conf.HTTP.BaseURL = types.URL{URL: &url.URL{Scheme: "http", Host: "localhost"}}
 				conf.HTTP.Secret = testutils.Secret
 				conf.OpenVpn.CommonName.EnvironmentVariableName = config.CommonName
-				conf.OpenVpn.Bypass = config.OpenVpnBypass{CommonNames: []string{"bypass"}}
+				conf.OpenVpn.Bypass = config.OpenVPNBypass{CommonNames: []string{"bypass"}}
 				conf.OpenVpn.Password = testutils.Password
 
 				return conf
@@ -212,7 +212,7 @@ func TestClientFull(t *testing.T) {
 				conf.HTTP.BaseURL = types.URL{URL: &url.URL{Scheme: "http", Host: "localhost"}}
 				conf.HTTP.Secret = testutils.Secret
 				conf.OpenVpn.CommonName.EnvironmentVariableName = config.CommonName
-				conf.OpenVpn.Bypass = config.OpenVpnBypass{CommonNames: []string{"bypass"}}
+				conf.OpenVpn.Bypass = config.OpenVPNBypass{CommonNames: []string{"bypass"}}
 				conf.OpenVpn.Password = testutils.Password
 
 				return conf
@@ -228,7 +228,7 @@ func TestClientFull(t *testing.T) {
 				conf.HTTP.BaseURL = types.URL{URL: &url.URL{Scheme: "http", Host: "localhost"}}
 				conf.HTTP.Secret = testutils.Secret
 				conf.OpenVpn.CommonName.EnvironmentVariableName = config.CommonName
-				conf.OpenVpn.Bypass = config.OpenVpnBypass{CommonNames: []string{"bypass"}}
+				conf.OpenVpn.Bypass = config.OpenVPNBypass{CommonNames: []string{"bypass"}}
 				conf.OpenVpn.Password = testutils.Password
 
 				return conf
@@ -245,7 +245,7 @@ func TestClientFull(t *testing.T) {
 				conf.HTTP.BaseURL = types.URL{URL: &url.URL{Scheme: "http", Host: "localhost"}}
 				conf.HTTP.Secret = testutils.Secret
 				conf.OpenVpn.CommonName.EnvironmentVariableName = config.CommonName
-				conf.OpenVpn.Bypass = config.OpenVpnBypass{CommonNames: []string{"bypass"}}
+				conf.OpenVpn.Bypass = config.OpenVPNBypass{CommonNames: []string{"bypass"}}
 				conf.OpenVpn.Password = testutils.Password
 
 				return conf
@@ -337,7 +337,7 @@ func TestClientFull(t *testing.T) {
 
 				require.Equal(t, uint64(1), sessionState.Client.CID)
 				require.Equal(t, uint64(2), sessionState.Client.KID)
-				require.Equal(t, "test", sessionState.CommonName)
+				require.Equal(t, "test", sessionState.Client.CommonName)
 				require.Equal(t, "127.0.0.1", sessionState.IPAddr)
 			}
 		})
@@ -366,7 +366,7 @@ func TestClientInvalidPassword(t *testing.T) {
 		},
 		OpenVpn: config.OpenVpn{
 			Addr:     types.URL{URL: &url.URL{Scheme: managementInterface.Addr().Network(), Host: managementInterface.Addr().String()}},
-			Bypass:   config.OpenVpnBypass{CommonNames: make([]string, 0)},
+			Bypass:   config.OpenVPNBypass{CommonNames: make([]string, 0)},
 			Password: "invalid",
 		},
 	}
@@ -436,7 +436,7 @@ func TestClientInvalidVersion(t *testing.T) {
 			conf := config.Defaults
 			conf.HTTP.BaseURL = types.URL{URL: &url.URL{Scheme: "http", Host: "localhost"}}
 			conf.HTTP.Secret = testutils.Secret
-			conf.OpenVpn.Bypass = config.OpenVpnBypass{CommonNames: []string{"bypass"}}
+			conf.OpenVpn.Bypass = config.OpenVPNBypass{CommonNames: []string{"bypass"}}
 			conf.OpenVpn.Addr = types.URL{URL: &url.URL{Scheme: managementInterface.Addr().Network(), Host: managementInterface.Addr().String()}}
 
 			tokenStorage := tokenstorage.NewInMemory(ctx, testutils.Secret, time.Hour)
@@ -478,7 +478,7 @@ func TestHoldRelease(t *testing.T) {
 			Secret:  testutils.Secret,
 		},
 		OpenVpn: config.OpenVpn{
-			Bypass: config.OpenVpnBypass{CommonNames: make([]string, 0)},
+			Bypass: config.OpenVPNBypass{CommonNames: make([]string, 0)},
 		},
 	}
 
@@ -532,7 +532,7 @@ func TestCommandTimeout(t *testing.T) {
 			Secret:  testutils.Secret,
 		},
 		OpenVpn: config.OpenVpn{
-			Bypass:         config.OpenVpnBypass{CommonNames: make([]string, 0)},
+			Bypass:         config.OpenVPNBypass{CommonNames: make([]string, 0)},
 			CommandTimeout: time.Millisecond * 300,
 		},
 	}
@@ -601,7 +601,7 @@ func TestDeadLocks(t *testing.T) {
 			conf := config.Defaults
 			conf.HTTP.BaseURL = types.URL{URL: &url.URL{Scheme: "http", Host: "localhost"}}
 			conf.HTTP.Secret = testutils.Secret
-			conf.OpenVpn.Bypass = config.OpenVpnBypass{CommonNames: make([]string, 0)}
+			conf.OpenVpn.Bypass = config.OpenVPNBypass{CommonNames: make([]string, 0)}
 
 			managementInterface, err := nettest.NewLocalListener("tcp")
 			require.NoError(t, err)
@@ -667,7 +667,7 @@ func TestInvalidCommandResponses(t *testing.T) {
 					Secret:  testutils.Secret,
 				},
 				OpenVpn: config.OpenVpn{
-					Bypass: config.OpenVpnBypass{CommonNames: make([]string, 0)},
+					Bypass: config.OpenVPNBypass{CommonNames: make([]string, 0)},
 				},
 			}
 
diff --git a/internal/state/bench_test.go b/internal/state/bench_test.go
index 53af60b..6b410b9 100644
--- a/internal/state/bench_test.go
+++ b/internal/state/bench_test.go
@@ -12,7 +12,7 @@ func BenchmarkState(b *testing.B) {
 	b.StopTimer()
 
 	encryptionKey := testutils.Secret
-	token := state.New(state.ClientIdentifier{CID: 9223372036854775807, KID: 2}, "127.0.0.1", "12345", "test", "")
+	token := state.New(state.ClientIdentifier{CID: 9223372036854775807, KID: 2, CommonName: "test"}, "127.0.0.1", "12345", "")
 
 	b.StartTimer()
 
diff --git a/internal/state/state_test.go b/internal/state/state_test.go
index fedf4ba..17c090a 100644
--- a/internal/state/state_test.go
+++ b/internal/state/state_test.go
@@ -16,7 +16,7 @@ func TestState(t *testing.T) {
 	encryptionKey := testutils.Secret
 
 	for i := 1; i < 50; i++ {
-		token := state.New(state.ClientIdentifier{CID: 9223372036854775807, KID: 2}, "127.0.0.1", "12345", "test", "")
+		token := state.New(state.ClientIdentifier{CID: 9223372036854775807, KID: 2, CommonName: "test"}, "127.0.0.1", "12345", "")
 		encodedTokenString, err := token.Encode(encryptionKey)
 		require.NoError(t, err)
 
@@ -25,8 +25,8 @@ func TestState(t *testing.T) {
 
 		assert.Equal(t, token.Client.CID, encodedToken.Client.CID)
 		assert.Equal(t, token.Client.KID, encodedToken.Client.KID)
+		assert.Equal(t, token.Client.CommonName, encodedToken.Client.CommonName)
 		assert.Equal(t, token.IPAddr, encodedToken.IPAddr)
-		assert.Equal(t, token.CommonName, encodedToken.CommonName)
 	}
 }
 
@@ -35,7 +35,7 @@ func TestStateWithEmptyValues(t *testing.T) {
 
 	encryptionKey := testutils.Secret
 
-	token := state.New(state.ClientIdentifier{CID: 1, KID: 2}, "127.0.0.1", "12345", "", "")
+	token := state.New(state.ClientIdentifier{CID: 1, KID: 2, CommonName: ""}, "127.0.0.1", "12345", "")
 	encodedTokenString, err := token.Encode(encryptionKey)
 	require.NoError(t, err)
 
@@ -44,8 +44,7 @@ func TestStateWithEmptyValues(t *testing.T) {
 
 	assert.Equal(t, token.Client.CID, encodedToken.Client.CID)
 	assert.Equal(t, token.Client.KID, encodedToken.Client.KID)
-	assert.Equal(t, token.IPAddr, encodedToken.IPAddr)
-	assert.Equal(t, token.CommonName, encodedToken.CommonName)
+	assert.Equal(t, token.Client.CommonName, encodedToken.Client.CommonName)
 }
 
 func TestStateInvalid_Key(t *testing.T) {
@@ -53,7 +52,7 @@ func TestStateInvalid_Key(t *testing.T) {
 
 	encryptionKey := "01234567891011"
 
-	token := state.New(state.ClientIdentifier{CID: 1, KID: 2}, "127.0.0.1", "12345", "test", "")
+	token := state.New(state.ClientIdentifier{CID: 1, KID: 2, CommonName: "test"}, "127.0.0.1", "12345", "")
 	_, err := token.Encode(encryptionKey)
 
 	require.Error(t, err, "crypto/aes: invalid key size 14")
@@ -64,7 +63,7 @@ func TestState_WithSpace(t *testing.T) {
 
 	encryptionKey := testutils.Secret
 
-	token := state.New(state.ClientIdentifier{CID: 1, KID: 2}, "127.0.0.1", "12345", "t e s t", "")
+	token := state.New(state.ClientIdentifier{CID: 1, KID: 2, CommonName: "t e s t"}, "127.0.0.1", "12345", "")
 
 	encodedTokenString, err := token.Encode(encryptionKey)
 
@@ -75,8 +74,7 @@ func TestState_WithSpace(t *testing.T) {
 
 	assert.Equal(t, token.Client.CID, encodedToken.Client.CID)
 	assert.Equal(t, token.Client.KID, encodedToken.Client.KID)
-	assert.Equal(t, token.IPAddr, encodedToken.IPAddr)
-	assert.Equal(t, token.CommonName, encodedToken.CommonName)
+	assert.Equal(t, token.Client.CommonName, encodedToken.Client.CommonName)
 }
 
 func TestState_WithState(t *testing.T) {
@@ -88,7 +86,7 @@ func TestState_WithState(t *testing.T) {
 		t.Run(sessionState, func(t *testing.T) {
 			t.Parallel()
 
-			token := state.New(state.ClientIdentifier{CID: 1, KID: 2}, "127.0.0.1", "12345", "test", sessionState)
+			token := state.New(state.ClientIdentifier{CID: 1, KID: 2, CommonName: "test"}, "127.0.0.1", "12345", sessionState)
 
 			encodedTokenString, err := token.Encode(encryptionKey)
 
@@ -107,7 +105,7 @@ func TestStateInvalid_Future(t *testing.T) {
 
 	encryptionKey := testutils.Secret
 
-	token := state.New(state.ClientIdentifier{CID: 1, KID: 2}, "127.0.0.1", "12345", "test", "")
+	token := state.New(state.ClientIdentifier{CID: 1, KID: 2, CommonName: "test"}, "127.0.0.1", "12345", "")
 	token.Issued = time.Now().Add(time.Hour).Unix()
 	encodedTokenString, err := token.Encode(encryptionKey)
 
@@ -123,7 +121,7 @@ func TestStateInvalid_TooOld(t *testing.T) {
 
 	encryptionKey := testutils.Secret
 
-	token := state.New(state.ClientIdentifier{CID: 1, KID: 2}, "127.0.0.1", "12345", "test", "")
+	token := state.New(state.ClientIdentifier{CID: 1, KID: 2, CommonName: "test"}, "127.0.0.1", "12345", "")
 	token.Issued = time.Now().Add(-1 * time.Hour).Unix()
 	encodedTokenString, err := token.Encode(encryptionKey)
 
diff --git a/internal/utils/testutils/main.go b/internal/utils/testutils/main.go
index 950440d..4b94952 100644
--- a/internal/utils/testutils/main.go
+++ b/internal/utils/testutils/main.go
@@ -213,6 +213,10 @@ func SetupMockEnvironment(ctx context.Context, tb testing.TB, conf config.Config
 		conf.HTTP.Template = config.Defaults.HTTP.Template
 	}
 
+	if conf.OpenVpn.ClientConfig.Path.IsEmpty() {
+		conf.OpenVpn.ClientConfig.Path = config.Defaults.OpenVpn.ClientConfig.Path
+	}
+
 	conf.OpenVpn.Addr = types.URL{URL: &url.URL{Scheme: managementInterface.Addr().Network(), Host: managementInterface.Addr().String()}}
 
 	if conf.OpenVpn.Bypass.CommonNames == nil {
diff --git a/internal/utils/testutils/openvpn.go b/internal/utils/testutils/openvpn.go
index c3eabb2..3fbe5e9 100644
--- a/internal/utils/testutils/openvpn.go
+++ b/internal/utils/testutils/openvpn.go
@@ -12,8 +12,6 @@ func NewFakeOpenVPNClient() FakeOpenVPNClient {
 	return FakeOpenVPNClient{}
 }
 
-func (FakeOpenVPNClient) AcceptClient(_ *slog.Logger, _ state.ClientIdentifier, _ string) {
-}
+func (FakeOpenVPNClient) AcceptClient(_ *slog.Logger, _ state.ClientIdentifier, _ bool, _ string) {}
 
-func (FakeOpenVPNClient) DenyClient(_ *slog.Logger, _ state.ClientIdentifier, _ string) {
-}
+func (FakeOpenVPNClient) DenyClient(_ *slog.Logger, _ state.ClientIdentifier, _ string) {}
diff --git a/tests/docker-compose.yaml b/tests/docker-compose.yaml
index 23015cf..d2ae24d 100644
--- a/tests/docker-compose.yaml
+++ b/tests/docker-compose.yaml
@@ -63,8 +63,8 @@ services:
           printf password > /etc/openvpn/password.txt
         fi
 
-        if [ ! -d /etc/openvpn/ccd ]; then
-          mkdir /etc/openvpn/ccd
+        if [ ! -d /etc/openvpn/client-config ]; then
+          mkdir /etc/openvpn/client-config
         fi
 
         cat > "/etc/openvpn/openvpn.conf" <<EOF
@@ -102,7 +102,7 @@ services:
         disable-dco
         duplicate-cn
 
-        client-config-dir /etc/openvpn/ccd
+        client-config-dir /etc/openvpn/client-config
 
         management 0.0.0.0 8081 /etc/openvpn/password.txt
         management-hold