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)
|
- 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)
|
- 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)
|
- 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)
|
## 0.17.1 (2022-12-05)
|
||||||
|
|
||||||
|
|
|
@ -276,6 +276,11 @@ unix_socket_permission: "0770"
|
||||||
# issuer: "https://your-oidc.issuer.com/path"
|
# issuer: "https://your-oidc.issuer.com/path"
|
||||||
# client_id: "your-oidc-client-id"
|
# client_id: "your-oidc-client-id"
|
||||||
# client_secret: "your-oidc-client-secret"
|
# 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
|
# 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".
|
# 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"
|
"io/fs"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -26,6 +27,8 @@ const (
|
||||||
TextLogFormat = "text"
|
TextLogFormat = "text"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var errOidcMutuallyExclusive = errors.New("oidc_client_secret and oidc_client_secret_path are mutually exclusive")
|
||||||
|
|
||||||
// Config contains the initial Headscale configuration.
|
// Config contains the initial Headscale configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ServerURL string
|
ServerURL string
|
||||||
|
@ -528,6 +531,19 @@ func GetHeadscaleConfig() (*Config, error) {
|
||||||
Msgf("'ip_prefixes' not configured, falling back to default: %v", prefixes)
|
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{
|
return &Config{
|
||||||
ServerURL: viper.GetString("server_url"),
|
ServerURL: viper.GetString("server_url"),
|
||||||
Addr: viper.GetString("listen_addr"),
|
Addr: viper.GetString("listen_addr"),
|
||||||
|
@ -580,7 +596,7 @@ func GetHeadscaleConfig() (*Config, error) {
|
||||||
),
|
),
|
||||||
Issuer: viper.GetString("oidc.issuer"),
|
Issuer: viper.GetString("oidc.issuer"),
|
||||||
ClientID: viper.GetString("oidc.client_id"),
|
ClientID: viper.GetString("oidc.client_id"),
|
||||||
ClientSecret: viper.GetString("oidc.client_secret"),
|
ClientSecret: oidcClientSecret,
|
||||||
Scope: viper.GetStringSlice("oidc.scope"),
|
Scope: viper.GetStringSlice("oidc.scope"),
|
||||||
ExtraParams: viper.GetStringMapString("oidc.extra_params"),
|
ExtraParams: viper.GetStringMapString("oidc.extra_params"),
|
||||||
AllowedDomains: viper.GetStringSlice("oidc.allowed_domains"),
|
AllowedDomains: viper.GetStringSlice("oidc.allowed_domains"),
|
||||||
|
|
|
@ -20,6 +20,10 @@ oidc:
|
||||||
# Specified/generated by your OIDC provider
|
# Specified/generated by your OIDC provider
|
||||||
client_id: "your-oidc-client-id"
|
client_id: "your-oidc-client-id"
|
||||||
client_secret: "your-oidc-client-secret"
|
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
|
# 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".
|
# 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{
|
oidcMap := map[string]string{
|
||||||
"HEADSCALE_OIDC_ISSUER": oidcConfig.Issuer,
|
"HEADSCALE_OIDC_ISSUER": oidcConfig.Issuer,
|
||||||
"HEADSCALE_OIDC_CLIENT_ID": oidcConfig.ClientID,
|
"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),
|
"HEADSCALE_OIDC_STRIP_EMAIL_DOMAIN": fmt.Sprintf("%t", oidcConfig.StripEmaildomain),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,6 +70,7 @@ func TestOIDCAuthenticationPingAll(t *testing.T) {
|
||||||
hsic.WithTestName("oidcauthping"),
|
hsic.WithTestName("oidcauthping"),
|
||||||
hsic.WithConfigEnv(oidcMap),
|
hsic.WithConfigEnv(oidcMap),
|
||||||
hsic.WithHostnameAsServerURL(),
|
hsic.WithHostnameAsServerURL(),
|
||||||
|
hsic.WithFileInContainer("/tmp/hs_client_oidc_secret", []byte(oidcConfig.ClientSecret)),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to create headscale environment: %s", err)
|
t.Errorf("failed to create headscale environment: %s", err)
|
||||||
|
|
|
@ -36,6 +36,11 @@ const (
|
||||||
|
|
||||||
var errHeadscaleStatusCodeNotOk = errors.New("headscale status code not ok")
|
var errHeadscaleStatusCodeNotOk = errors.New("headscale status code not ok")
|
||||||
|
|
||||||
|
type fileInContainer struct {
|
||||||
|
path string
|
||||||
|
contents []byte
|
||||||
|
}
|
||||||
|
|
||||||
type HeadscaleInContainer struct {
|
type HeadscaleInContainer struct {
|
||||||
hostname string
|
hostname string
|
||||||
|
|
||||||
|
@ -49,6 +54,7 @@ type HeadscaleInContainer struct {
|
||||||
env map[string]string
|
env map[string]string
|
||||||
tlsCert []byte
|
tlsCert []byte
|
||||||
tlsKey []byte
|
tlsKey []byte
|
||||||
|
filesInContainer []fileInContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
type Option = func(c *HeadscaleInContainer)
|
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(
|
func New(
|
||||||
pool *dockertest.Pool,
|
pool *dockertest.Pool,
|
||||||
network *dockertest.Network,
|
network *dockertest.Network,
|
||||||
|
@ -130,6 +146,7 @@ func New(
|
||||||
network: network,
|
network: network,
|
||||||
|
|
||||||
env: DefaultConfigEnv(),
|
env: DefaultConfigEnv(),
|
||||||
|
filesInContainer: []fileInContainer{},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range opts {
|
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
|
return hsic, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue