Skip to content

Commit 5d1d221

Browse files
authored
Implement client configs (#383)
1 parent 10f77bd commit 5d1d221

File tree

26 files changed

+365
-170
lines changed

26 files changed

+365
-170
lines changed

.golangci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ linters:
133133
- l net.Listener
134134
- t reflect.Type
135135
- wg sync.WaitGroup
136-
- k *koanf.Koanf
136+
- sb strings.Builder
137137
- mu sync.Mutex
138138
- ts oauth2.TokenSource
139139
- ca *testcerts.CertificateAuthority

.goreleaser.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ nfpms:
7070
owner: root
7171
group: openvpn-auth-oauth2
7272
mode: 0750
73+
- dst: /etc/openvpn-auth-oauth2/client-config/
74+
type: dir
75+
file_info:
76+
owner: root
77+
group: openvpn-auth-oauth2
78+
mode: 0750
7379
- src: packaging/etc/openvpn-auth-oauth2/config.yaml
7480
dst: /etc/openvpn-auth-oauth2/config.yaml
7581
type: "config|noreplace"
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Client specific configuration
2+
3+
## Introduction
4+
5+
This document describes the client-specific configuration options of openvpn-auth-oauth2.
6+
It mimics the client-config-dir capability of OpenVPN.
7+
But instead the client username, a token claim is used as config identifier.
8+
9+
## Configuration
10+
11+
The feature must be enabled with `--openvpn.client-config.enabled`.
12+
`--openvpn.client-config.path` points to a directory where the client-specific configuration files are stored.
13+
14+
openvpn-auth-oauth2 looks for a file
15+
named after the token claim or common name with `.conf` suffix in the client config directory.

docs/Configuration.md

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Take a look at the [FAQ](./FAQ) section, for common questions, issues and soluti
1111
openvpn-auth-oauth2 supports configuration via a YAML file. The file can be passed via the `--config` flag.
1212

1313
<details>
14-
<summary>Example</summary>
14+
<summary>Example config.yaml</summary>
1515

1616
```yaml
1717
debug:
@@ -79,6 +79,10 @@ openvpn:
7979
# - "test"
8080
# - "test2"
8181
override-username: false
82+
ccd:
83+
enabled: false
84+
token-claim: ""
85+
path: "/etc/openvpn-auth-oauth2/client-config/"
8286
common-name:
8387
environment-variable-name: common_name
8488
mode: plain
@@ -106,30 +110,30 @@ Usage of openvpn-auth-oauth2:
106110
listen address for go profiling endpoint (env: CONFIG_DEBUG_LISTEN) (default ":9001")
107111
--debug.pprof
108112
Enables go profiling endpoint. This should be never exposed. (env: CONFIG_DEBUG_PPROF)
109-
--http.assets-path string
113+
--http.assets-path value
110114
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)
111-
--http.baseurl string
112-
listen addr for client listener (env: CONFIG_HTTP_BASEURL) (default "http://localhost:9000")
115+
--http.baseurl value
116+
listen addr for client listener (env: CONFIG_HTTP_BASEURL) (default http://localhost:9000)
113117
--http.cert string
114-
Path to tls server certificate (env: CONFIG_HTTP_CERT)
118+
Path to tls server certificate used for TLS listener. (env: CONFIG_HTTP_CERT)
115119
--http.check.ipaddr
116120
Check if client IP in http and VPN is equal (env: CONFIG_HTTP_CHECK_IPADDR)
117121
--http.enable-proxy-headers
118122
Use X-Forward-For http header for client ips (env: CONFIG_HTTP_ENABLE__PROXY__HEADERS)
119123
--http.key string
120-
Path to tls server key (env: CONFIG_HTTP_KEY)
124+
Path to tls server key used for TLS listener. (env: CONFIG_HTTP_KEY)
121125
--http.listen string
122126
listen addr for client listener (env: CONFIG_HTTP_LISTEN) (default ":9000")
123127
--http.secret value
124128
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)
125-
--http.template string
129+
--http.template value
126130
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)
127131
--http.tls
128132
enable TLS listener (env: CONFIG_HTTP_TLS)
129133
--log.format string
130134
log format. json or console (env: CONFIG_LOG_FORMAT) (default "console")
131135
--log.level value
132-
log level (env: CONFIG_LOG_LEVEL) (default INFO)
136+
log level. Can be one of: debug, info, warn, error (env: CONFIG_LOG_LEVEL) (default INFO)
133137
--log.vpn-client-ip
134138
log IP of VPN client. Useful to have an identifier between OpenVPN and openvpn-auth-oauth2. (env: CONFIG_LOG_VPN__CLIENT__IP) (default true)
135139
--oauth2.auth-style value
@@ -144,13 +148,13 @@ Usage of openvpn-auth-oauth2:
144148
oauth2 client private key id. If specified, JWT assertions will be generated with the specific kid header. (env: CONFIG_OAUTH2_CLIENT_PRIVATE__KEY__ID)
145149
--oauth2.client.secret value
146150
oauth2 client secret. If argument starts with file:// it reads the secret from a file. (env: CONFIG_OAUTH2_CLIENT_SECRET)
147-
--oauth2.endpoint.auth string
151+
--oauth2.endpoint.auth value
148152
The flag is used to specify a custom OAuth2 authorization endpoint. (env: CONFIG_OAUTH2_ENDPOINT_AUTH)
149-
--oauth2.endpoint.discovery string
153+
--oauth2.endpoint.discovery value
150154
The flag is used to set a custom OAuth2 discovery URL. This URL retrieves the provider's configuration details. (env: CONFIG_OAUTH2_ENDPOINT_DISCOVERY)
151-
--oauth2.endpoint.token string
155+
--oauth2.endpoint.token value
152156
The flag is used to specify a custom OAuth2 token endpoint. (env: CONFIG_OAUTH2_ENDPOINT_TOKEN)
153-
--oauth2.issuer string
157+
--oauth2.issuer value
154158
oauth2 issuer (env: CONFIG_OAUTH2_ISSUER)
155159
--oauth2.nonce
156160
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:
184188
validate issuer from oidc discovery (env: CONFIG_OAUTH2_VALIDATE_ISSUER) (default true)
185189
--oauth2.validate.roles value
186190
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)
187-
--openvpn.addr string
188-
openvpn management interface addr. Must start with unix:// or tcp:// (env: CONFIG_OPENVPN_ADDR) (default "unix:/run/openvpn/server.sock")
191+
--openvpn.addr value
192+
openvpn management interface addr. Must start with unix:// or tcp:// (env: CONFIG_OPENVPN_ADDR) (default unix:/run/openvpn/server.sock)
189193
--openvpn.auth-pending-timeout duration
190194
How long OpenVPN server wait until user is authenticated (env: CONFIG_OPENVPN_AUTH__PENDING__TIMEOUT) (default 3m0s)
191195
--openvpn.auth-token-user
192196
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)
193197
--openvpn.bypass.common-names value
194198
bypass oauth authentication for CNs. Comma separated list. (env: CONFIG_OPENVPN_BYPASS_COMMON__NAMES)
199+
--openvpn.client-config.enabled
200+
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)
201+
--openvpn.client-config.path value
202+
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)
203+
--openvpn.client-config.token-claim string
204+
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)
195205
--openvpn.common-name.environment-variable-name string
196206
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")
197207
--openvpn.common-name.mode value
198208
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)
199209
--openvpn.override-username
200210
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)
201-
--openvpn.pass-through.address string
202-
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")
211+
--openvpn.pass-through.address value
212+
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)
203213
--openvpn.pass-through.enabled
204214
If true, openvpn-auth-oauth2 will setup a pass-through socket for the OpenVPN management interface. (env: CONFIG_OPENVPN_PASS__THROUGH_ENABLED)
205215
--openvpn.pass-through.password value
@@ -325,3 +335,7 @@ See [Layout Customization](Layout%20Customization) for more information
325335
## Non-interactive session refresh
326336
327337
See [Non-interactive session refresh](Non-interactive%20session%20refresh) for more information.
338+
339+
## Client specific configuration
340+
341+
See [Client specific configuration](Client%20specific%20configuration) for more information.

internal/config/config_test.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ openvpn:
109109
common-names:
110110
- "test"
111111
- "test2"
112+
client-config:
113+
enabled: true
114+
token-claim: sub
115+
path: "."
112116
common-name:
113117
environment-variable-name: X509_0_emailAddress
114118
mode: omit
@@ -168,9 +172,19 @@ http:
168172
Path: "/run/openvpn/server2.sock",
169173
OmitHost: false,
170174
}},
171-
Bypass: config.OpenVpnBypass{
175+
Bypass: config.OpenVPNBypass{
172176
CommonNames: []string{"test", "test2"},
173177
},
178+
ClientConfig: config.OpenVPNConfig{
179+
Enabled: true,
180+
TokenClaim: "sub",
181+
Path: func() types.FS {
182+
dirFS, err := types.NewFS(".")
183+
require.NoError(t, err)
184+
185+
return dirFS
186+
}(),
187+
},
174188
Password: "1jd93h5b6s82lf03jh5b2hf9",
175189
AuthTokenUser: true,
176190
AuthPendingTimeout: 2 * time.Minute,

internal/config/defaults.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package config
33
import (
44
"log/slog"
55
"net/url"
6+
"os"
67
"text/template"
78
"time"
89

@@ -47,12 +48,16 @@ var Defaults = Config{
4748
}},
4849
AuthTokenUser: true,
4950
AuthPendingTimeout: 3 * time.Minute,
51+
ClientConfig: OpenVPNConfig{
52+
Enabled: false,
53+
Path: types.FS{FS: os.DirFS("/etc/openvpn-auth-oauth2/client-config-dir/")},
54+
},
5055
CommonName: OpenVPNCommonName{
5156
EnvironmentVariableName: "common_name",
5257
Mode: CommonNameModePlain,
5358
},
5459
OverrideUsername: false,
55-
Bypass: OpenVpnBypass{
60+
Bypass: OpenVPNBypass{
5661
CommonNames: make([]string, 0),
5762
},
5863
Passthrough: OpenVPNPassthrough{

internal/config/flags.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,24 @@ func (c *Config) flagSetOpenVPN(flagSet *flag.FlagSet) {
140140
lookupEnvOrDefault("openvpn.bypass.common-names", c.OpenVpn.Bypass.CommonNames),
141141
"bypass oauth authentication for CNs. Comma separated list.",
142142
)
143+
flagSet.BoolVar(
144+
&c.OpenVpn.ClientConfig.Enabled,
145+
"openvpn.client-config.enabled",
146+
lookupEnvOrDefault("openvpn.client-config.enabled", c.OpenVpn.ClientConfig.Enabled),
147+
"If true, openvpn-auth-oauth2 will read the CCD directory for additional configuration. This function mimic the client-config-dir directive in OpenVPN.",
148+
)
149+
flagSet.TextVar(
150+
&c.OpenVpn.ClientConfig.Path,
151+
"openvpn.client-config.path",
152+
lookupEnvOrDefault("openvpn.client-config.path", c.OpenVpn.ClientConfig.Path),
153+
"Path to the CCD directory. openvpn-auth-oauth2 will look for an file with an .conf suffix and returns the content back.",
154+
)
155+
flagSet.StringVar(
156+
&c.OpenVpn.ClientConfig.TokenClaim,
157+
"openvpn.client-config.token-claim",
158+
lookupEnvOrDefault("openvpn.client-config.token-claim", c.OpenVpn.ClientConfig.TokenClaim),
159+
"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.",
160+
)
143161
flagSet.StringVar(
144162
&c.OpenVpn.CommonName.EnvironmentVariableName,
145163
"openvpn.common-name.environment-variable-name",

internal/config/types.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ type Log struct {
5151
type OpenVpn struct {
5252
Addr types.URL `json:"addr" yaml:"addr"`
5353
Password types.Secret `json:"password" yaml:"password"`
54-
Bypass OpenVpnBypass `json:"bypass" yaml:"bypass"`
54+
Bypass OpenVPNBypass `json:"bypass" yaml:"bypass"`
55+
ClientConfig OpenVPNConfig `json:"client-config" yaml:"client-config"`
5556
AuthTokenUser bool `json:"auth-token-user" yaml:"auth-token-user"`
5657
AuthPendingTimeout time.Duration `json:"auth-pending-timeout" yaml:"auth-pending-timeout"`
5758
OverrideUsername bool `json:"override-username" yaml:"override-username"`
@@ -60,9 +61,14 @@ type OpenVpn struct {
6061
CommandTimeout time.Duration `json:"command-timeout" yaml:"command-timeout"`
6162
}
6263

63-
type OpenVpnBypass struct {
64+
type OpenVPNBypass struct {
6465
CommonNames types.StringSlice `json:"common-names" yaml:"common-names"`
6566
}
67+
type OpenVPNConfig struct {
68+
Enabled bool `json:"enabled" yaml:"enabled"`
69+
TokenClaim string `json:"token-claim" yaml:"token-claim"`
70+
Path types.FS `json:"path" yaml:"path"`
71+
}
6672

6773
type OpenVPNCommonName struct {
6874
EnvironmentVariableName string `json:"environment-variable-name" yaml:"environment-variable-name"`

internal/config/validate.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,19 @@ func validateOAuth2Config(conf Config) error {
107107
}
108108
}
109109

110+
if conf.OpenVpn.ClientConfig.Enabled {
111+
if conf.OpenVpn.CommonName.Mode == CommonNameModeOmit {
112+
return errors.New("openvpn.common-name.mode: omit is not supported with openvpn.ccd.enabled")
113+
}
114+
115+
file, err := conf.OpenVpn.ClientConfig.Path.Open(".")
116+
if err != nil {
117+
return fmt.Errorf("openvpn.ccd.path: %w", err)
118+
}
119+
120+
_ = file.Close()
121+
}
122+
110123
return nil
111124
}
112125

internal/oauth2/handler.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import (
2121
)
2222

2323
type openvpnManagementClient interface {
24-
AcceptClient(logger *slog.Logger, client state.ClientIdentifier, username string)
24+
AcceptClient(logger *slog.Logger, client state.ClientIdentifier, reAuth bool, username string)
2525
DenyClient(logger *slog.Logger, client state.ClientIdentifier, reason string)
2626
}
2727

@@ -52,7 +52,7 @@ func (c Client) OAuth2Start() http.Handler {
5252
slog.String("ip", fmt.Sprintf("%s:%s", session.IPAddr, session.IPPort)),
5353
slog.Uint64("cid", session.Client.CID),
5454
slog.Uint64("kid", session.Client.KID),
55-
slog.String("common_name", session.CommonName),
55+
slog.String("common_name", session.Client.CommonName),
5656
)
5757

5858
if c.conf.HTTP.Check.IPAddr {
@@ -116,7 +116,7 @@ func (c Client) OAuth2Callback() http.Handler {
116116
slog.String("ip", fmt.Sprintf("%s:%s", session.IPAddr, session.IPPort)),
117117
slog.Uint64("cid", session.Client.CID),
118118
slog.Uint64("kid", session.Client.KID),
119-
slog.String("common_name", session.CommonName),
119+
slog.String("common_name", session.Client.CommonName),
120120
slog.String("session_id", session.Client.SessionID),
121121
slog.String("session_state", session.SessionState),
122122
)
@@ -181,10 +181,10 @@ func (c Client) postCodeExchangeHandler(logger *slog.Logger, session state.State
181181

182182
username := user.PreferredUsername
183183
if username == "" {
184-
username = session.CommonName
184+
username = session.Client.CommonName
185185
}
186186

187-
c.openvpn.AcceptClient(logger, session.Client, username)
187+
c.openvpn.AcceptClient(logger, session.Client, false, username)
188188
c.postCodeExchangeHandlerStoreRefreshToken(ctx, logger, session, clientID, tokens)
189189
c.writeHTTPSuccess(w, logger)
190190
}
@@ -240,7 +240,7 @@ func (c Client) httpErrorHandler(w http.ResponseWriter, httpStatus int, errorTyp
240240
slog.String("ip", fmt.Sprintf("%s:%s", session.IPAddr, session.IPPort)),
241241
slog.Uint64("cid", session.Client.CID),
242242
slog.Uint64("kid", session.Client.KID),
243-
slog.String("common_name", session.CommonName),
243+
slog.String("common_name", session.Client.CommonName),
244244
)
245245

246246
c.openvpn.DenyClient(logger, session.Client, "client rejected")

0 commit comments

Comments
 (0)