oidc: allow reading the client secret from a file
Currently the most "secret" way to specify the oidc client secret is via an environment variable `OIDC_CLIENT_SECRET`, which is problematic[1]. Lets allow reading oidc client secret from a file. For extra convenience the path to the secret will resolve the environment variables. [1]: https://systemd.io/CREDENTIALS/
This commit is contained in:
parent
6edac4863a
commit
bafb6791d3
6 changed files with 59 additions and 8 deletions
|
@ -13,6 +13,7 @@
|
|||
- Expire nodes based on OIDC token expiry [#1067](https://github.com/juanfont/headscale/pull/1067)
|
||||
- Remove ephemeral nodes on logout [#1098](https://github.com/juanfont/headscale/pull/1098)
|
||||
- Performance improvements in ACLs [#1129](https://github.com/juanfont/headscale/pull/1129)
|
||||
- OIDC client secret can be passed via a file [#1127](https://github.com/juanfont/headscale/pull/1127)
|
||||
|
||||
## 0.17.1 (2022-12-05)
|
||||
|
||||
|
|
|
@ -276,6 +276,11 @@ unix_socket_permission: "0770"
|
|||
# issuer: "https://your-oidc.issuer.com/path"
|
||||
# client_id: "your-oidc-client-id"
|
||||
# client_secret: "your-oidc-client-secret"
|
||||
# # Alternatively, set `client_secret_path` to read the secret from the file.
|
||||
# # It resolves environment variables, making integration to systemd's
|
||||
# # `LoadCredential` straightforward:
|
||||
# client_secret_path: "${CREDENTIALS_DIRECTORY}/oidc_client_secret"
|
||||
# # client_secret and client_secret_path are mutually exclusive.
|
||||
#
|
||||
# Customize the scopes used in the OIDC flow, defaults to "openid", "profile" and "email" and add custom query
|
||||
# parameters to the Authorize Endpoint request. Scopes default to "openid", "profile" and "email".
|
||||
|
|
18
config.go
18
config.go
|
@ -6,6 +6,7 @@ import (
|
|||
"io/fs"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -26,6 +27,8 @@ const (
|
|||
TextLogFormat = "text"
|
||||
)
|
||||
|
||||
var errOidcMutuallyExclusive = errors.New("oidc_client_secret and oidc_client_secret_path are mutually exclusive")
|
||||
|
||||
// Config contains the initial Headscale configuration.
|
||||
type Config struct {
|
||||
ServerURL string
|
||||
|
@ -528,6 +531,19 @@ func GetHeadscaleConfig() (*Config, error) {
|
|||
Msgf("'ip_prefixes' not configured, falling back to default: %v", prefixes)
|
||||
}
|
||||
|
||||
oidcClientSecret := viper.GetString("oidc.client_secret")
|
||||
oidcClientSecretPath := viper.GetString("oidc.client_secret_path")
|
||||
if oidcClientSecretPath != "" && oidcClientSecret != "" {
|
||||
return nil, errOidcMutuallyExclusive
|
||||
}
|
||||
if oidcClientSecretPath != "" {
|
||||
secretBytes, err := os.ReadFile(os.ExpandEnv(oidcClientSecretPath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oidcClientSecret = string(secretBytes)
|
||||
}
|
||||
|
||||
return &Config{
|
||||
ServerURL: viper.GetString("server_url"),
|
||||
Addr: viper.GetString("listen_addr"),
|
||||
|
@ -580,7 +596,7 @@ func GetHeadscaleConfig() (*Config, error) {
|
|||
),
|
||||
Issuer: viper.GetString("oidc.issuer"),
|
||||
ClientID: viper.GetString("oidc.client_id"),
|
||||
ClientSecret: viper.GetString("oidc.client_secret"),
|
||||
ClientSecret: oidcClientSecret,
|
||||
Scope: viper.GetStringSlice("oidc.scope"),
|
||||
ExtraParams: viper.GetStringMapString("oidc.extra_params"),
|
||||
AllowedDomains: viper.GetStringSlice("oidc.allowed_domains"),
|
||||
|
|
|
@ -20,6 +20,10 @@ oidc:
|
|||
# Specified/generated by your OIDC provider
|
||||
client_id: "your-oidc-client-id"
|
||||
client_secret: "your-oidc-client-secret"
|
||||
# alternatively, set `client_secret_path` to read the secret from the file.
|
||||
# It resolves environment variables, making integration to systemd's
|
||||
# `LoadCredential` straightforward:
|
||||
#client_secret_path: "${CREDENTIALS_DIRECTORY}/oidc_client_secret"
|
||||
|
||||
# Customize the scopes used in the OIDC flow, defaults to "openid", "profile" and "email" and add custom query
|
||||
# parameters to the Authorize Endpoint request. Scopes default to "openid", "profile" and "email".
|
||||
|
|
|
@ -60,7 +60,8 @@ func TestOIDCAuthenticationPingAll(t *testing.T) {
|
|||
oidcMap := map[string]string{
|
||||
"HEADSCALE_OIDC_ISSUER": oidcConfig.Issuer,
|
||||
"HEADSCALE_OIDC_CLIENT_ID": oidcConfig.ClientID,
|
||||
"HEADSCALE_OIDC_CLIENT_SECRET": oidcConfig.ClientSecret,
|
||||
"CREDENTIALS_DIRECTORY_TEST": "/tmp",
|
||||
"HEADSCALE_OIDC_CLIENT_SECRET_PATH": "${CREDENTIALS_DIRECTORY_TEST}/hs_client_oidc_secret",
|
||||
"HEADSCALE_OIDC_STRIP_EMAIL_DOMAIN": fmt.Sprintf("%t", oidcConfig.StripEmaildomain),
|
||||
}
|
||||
|
||||
|
@ -69,6 +70,7 @@ func TestOIDCAuthenticationPingAll(t *testing.T) {
|
|||
hsic.WithTestName("oidcauthping"),
|
||||
hsic.WithConfigEnv(oidcMap),
|
||||
hsic.WithHostnameAsServerURL(),
|
||||
hsic.WithFileInContainer("/tmp/hs_client_oidc_secret", []byte(oidcConfig.ClientSecret)),
|
||||
)
|
||||
if err != nil {
|
||||
t.Errorf("failed to create headscale environment: %s", err)
|
||||
|
|
|
@ -36,6 +36,11 @@ const (
|
|||
|
||||
var errHeadscaleStatusCodeNotOk = errors.New("headscale status code not ok")
|
||||
|
||||
type fileInContainer struct {
|
||||
path string
|
||||
contents []byte
|
||||
}
|
||||
|
||||
type HeadscaleInContainer struct {
|
||||
hostname string
|
||||
|
||||
|
@ -44,11 +49,12 @@ type HeadscaleInContainer struct {
|
|||
network *dockertest.Network
|
||||
|
||||
// optional config
|
||||
port int
|
||||
aclPolicy *headscale.ACLPolicy
|
||||
env map[string]string
|
||||
tlsCert []byte
|
||||
tlsKey []byte
|
||||
port int
|
||||
aclPolicy *headscale.ACLPolicy
|
||||
env map[string]string
|
||||
tlsCert []byte
|
||||
tlsKey []byte
|
||||
filesInContainer []fileInContainer
|
||||
}
|
||||
|
||||
type Option = func(c *HeadscaleInContainer)
|
||||
|
@ -110,6 +116,16 @@ func WithHostnameAsServerURL() Option {
|
|||
}
|
||||
}
|
||||
|
||||
func WithFileInContainer(path string, contents []byte) Option {
|
||||
return func(hsic *HeadscaleInContainer) {
|
||||
hsic.filesInContainer = append(hsic.filesInContainer,
|
||||
fileInContainer{
|
||||
path: path,
|
||||
contents: contents,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func New(
|
||||
pool *dockertest.Pool,
|
||||
network *dockertest.Network,
|
||||
|
@ -129,7 +145,8 @@ func New(
|
|||
pool: pool,
|
||||
network: network,
|
||||
|
||||
env: DefaultConfigEnv(),
|
||||
env: DefaultConfigEnv(),
|
||||
filesInContainer: []fileInContainer{},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
|
@ -214,6 +231,12 @@ func New(
|
|||
}
|
||||
}
|
||||
|
||||
for _, f := range hsic.filesInContainer {
|
||||
if err := hsic.WriteFile(f.path, f.contents); err != nil {
|
||||
return nil, fmt.Errorf("failed to write %q: %w", f.path, err)
|
||||
}
|
||||
}
|
||||
|
||||
return hsic, nil
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue