Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion backend/cmd/headlamp.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,14 +450,20 @@

// In-cluster
if config.UseInCluster {
if config.UseServiceAccountToken {
logger.Log(logger.LevelWarn, nil, nil, "Using service account token for in-cluster authentication could pose security risks if Headlamp is exposed externally.")

Check failure on line 454 in backend/cmd/headlamp.go

View workflow job for this annotation

GitHub Actions / build

The line is 163 characters long, which exceeds the maximum of 120 characters. (lll)
}
context, err := kubeconfig.GetInClusterContext(

Check failure on line 456 in backend/cmd/headlamp.go

View workflow job for this annotation

GitHub Actions / build

assignments should only be cuddled with other assignments (wsl)
config.InClusterContextName,
config.oidcIdpIssuerURL,
config.oidcClientID, config.oidcClientSecret,
strings.Join(config.oidcScopes, ","),
config.oidcSkipTLSVerify,
config.oidcCACert)
config.oidcCACert,
config.UseServiceAccountToken,
config.ServiceAccountTokenPath,
)
if err != nil {

Check failure on line 466 in backend/cmd/headlamp.go

View workflow job for this annotation

GitHub Actions / build

only one cuddle assignment allowed before if statement (wsl)
logger.Log(logger.LevelError, nil, err, "Failed to get in-cluster context")
}

Expand Down
42 changes: 22 additions & 20 deletions backend/cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,26 +80,28 @@ func main() {
// buildHeadlampCFG maps the parsed config into the struct the backend uses.
func buildHeadlampCFG(conf *config.Config, kubeConfigStore kubeconfig.ContextStore) *headlampconfig.HeadlampCFG {
return &headlampconfig.HeadlampCFG{
UseInCluster: conf.InCluster,
InClusterContextName: conf.InClusterContextName,
KubeConfigPath: conf.KubeConfigPath,
SkippedKubeContexts: conf.SkippedKubeContexts,
ListenAddr: conf.ListenAddr,
CacheEnabled: conf.CacheEnabled,
Port: conf.Port,
DevMode: conf.DevMode,
StaticDir: conf.StaticDir,
Insecure: conf.InsecureSsl,
PluginDir: conf.PluginsDir,
UserPluginDir: conf.UserPluginsDir,
EnableHelm: conf.EnableHelm,
EnableDynamicClusters: conf.EnableDynamicClusters,
WatchPluginsChanges: conf.WatchPluginsChanges,
KubeConfigStore: kubeConfigStore,
BaseURL: conf.BaseURL,
ProxyURLs: strings.Split(conf.ProxyURLs, ","),
TLSCertPath: conf.TLSCertPath,
TLSKeyPath: conf.TLSKeyPath,
UseInCluster: conf.InCluster,
InClusterContextName: conf.InClusterContextName,
KubeConfigPath: conf.KubeConfigPath,
SkippedKubeContexts: conf.SkippedKubeContexts,
ListenAddr: conf.ListenAddr,
CacheEnabled: conf.CacheEnabled,
Port: conf.Port,
DevMode: conf.DevMode,
StaticDir: conf.StaticDir,
Insecure: conf.InsecureSsl,
PluginDir: conf.PluginsDir,
UserPluginDir: conf.UserPluginsDir,
EnableHelm: conf.EnableHelm,
EnableDynamicClusters: conf.EnableDynamicClusters,
WatchPluginsChanges: conf.WatchPluginsChanges,
KubeConfigStore: kubeConfigStore,
BaseURL: conf.BaseURL,
ProxyURLs: strings.Split(conf.ProxyURLs, ","),
TLSCertPath: conf.TLSCertPath,
TLSKeyPath: conf.TLSKeyPath,
UseServiceAccountToken: conf.UseServiceAccountToken,
ServiceAccountTokenPath: conf.ServiceAccountTokenPath,
}
}

Expand Down
18 changes: 12 additions & 6 deletions backend/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ const (
)

const (
DefaultMeUsernamePath = "preferred_username,upn,username,name"
DefaultMeEmailPath = "email"
DefaultMeGroupsPath = "groups,realm_access.roles"
DefaultMeUserInfoURL = ""
DefaultMeUsernamePath = "preferred_username,upn,username,name"
DefaultMeEmailPath = "email"
DefaultMeGroupsPath = "groups,realm_access.roles"
DefaultMeUserInfoURL = ""
DefaultServiceAccountTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" // #nosec G101
)

type Config struct {
Expand Down Expand Up @@ -70,6 +71,8 @@ type Config struct {
MeGroupsPath string `koanf:"me-groups-path"`
MeUserInfoURL string `koanf:"me-user-info-url"`
OidcUsePKCE bool `koanf:"oidc-use-pkce"`
UseServiceAccountToken bool `koanf:"use-service-account-token"`
ServiceAccountTokenPath string `koanf:"service-account-token-path"`
// telemetry configs
ServiceName string `koanf:"service-name"`
ServiceVersion *string `koanf:"service-version"`
Expand All @@ -87,9 +90,10 @@ type Config struct {

func (c *Config) Validate() error {
if !c.InCluster && (c.OidcClientID != "" || c.OidcClientSecret != "" || c.OidcIdpIssuerURL != "" ||
c.OidcValidatorClientID != "" || c.OidcValidatorIdpIssuerURL != "") {
c.OidcValidatorClientID != "" || c.OidcValidatorIdpIssuerURL != "" || c.UseServiceAccountToken) {
return errors.New(`oidc-client-id, oidc-client-secret, oidc-idp-issuer-url, oidc-validator-client-id,
oidc-validator-idp-issuer-url, flags are only meant to be used in inCluster mode`)
oidc-validator-idp-issuer-url, use-service-account-token
flags are only meant to be used in inCluster mode`)
}

// OIDC TLS verification warning.
Expand Down Expand Up @@ -434,6 +438,8 @@ func addGeneralFlags(f *flag.FlagSet) {
f.Uint("port", defaultPort, "Port to listen from")
f.String("proxy-urls", "", "Allow proxy requests to specified URLs")
f.Bool("enable-helm", false, "Enable Helm operations")
f.Bool("use-service-account-token", false, "Use the service account token for in-cluster authentication")
f.String("service-account-token-path", DefaultServiceAccountTokenPath, "Path to the service account token")
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--service-account-token-path is given a non-empty default value. This makes it hard to distinguish "flag not set" from "set", and it also prevents mode validation similar to the OIDC flags (because the value is always non-empty). Consider defaulting this flag to an empty string and applying the default path only when --use-service-account-token is true (the kubeconfig code already has this fallback).

Suggested change
f.String("service-account-token-path", DefaultServiceAccountTokenPath, "Path to the service account token")
f.String("service-account-token-path", "", "Path to the service account token")

Copilot uses AI. Check for mistakes.
}

func addOIDCFlags(f *flag.FlagSet) {
Expand Down
8 changes: 8 additions & 0 deletions backend/pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@
}
}

func TestParseFlags(t *testing.T) {

Check failure on line 205 in backend/pkg/config/config_test.go

View workflow job for this annotation

GitHub Actions / build

Function 'TestParseFlags' is too long (66 > 60) (funlen)
tests := []struct {
name string
args []string
Expand Down Expand Up @@ -250,6 +250,14 @@
assert.Equal(t, "warn", conf.LogLevel)
},
},
{
name: "use_service_account_token_flag",
args: []string{"go run ./cmd", "--use-service-account-token", "--service-account-token-path=/custom/token/path"},
verify: func(t *testing.T, conf *config.Config) {
assert.Equal(t, true, conf.UseServiceAccountToken)
assert.Equal(t, "/custom/token/path", conf.ServiceAccountTokenPath)
},
Comment on lines +253 to +259
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test enables --use-service-account-token without also setting --in-cluster. config.Parse() runs Validate() and should return an error for this combination, so this test will fail. Add --in-cluster to the args (or update the validation expectations if the intent is to support it out of cluster).

Copilot uses AI. Check for mistakes.
},
}

for _, tt := range tests {
Expand Down
48 changes: 25 additions & 23 deletions backend/pkg/headlampconfig/headlampConfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,29 @@ import (
)

type HeadlampCFG struct {
UseInCluster bool
InClusterContextName string
ListenAddr string
CacheEnabled bool
DevMode bool
Insecure bool
EnableHelm bool
EnableDynamicClusters bool
WatchPluginsChanges bool
Port uint
KubeConfigPath string
SkippedKubeContexts string
StaticDir string
PluginDir string
UserPluginDir string
StaticPluginDir string
KubeConfigStore kubeconfig.ContextStore
Telemetry *telemetry.Telemetry
Metrics *telemetry.Metrics
BaseURL string
ProxyURLs []string
TLSCertPath string
TLSKeyPath string
UseInCluster bool
InClusterContextName string
ListenAddr string
CacheEnabled bool
DevMode bool
Insecure bool
EnableHelm bool
EnableDynamicClusters bool
WatchPluginsChanges bool
Port uint
KubeConfigPath string
SkippedKubeContexts string
StaticDir string
PluginDir string
UserPluginDir string
StaticPluginDir string
KubeConfigStore kubeconfig.ContextStore
Telemetry *telemetry.Telemetry
Metrics *telemetry.Metrics
BaseURL string
ProxyURLs []string
TLSCertPath string
TLSKeyPath string
UseServiceAccountToken bool
ServiceAccountTokenPath string
}
8 changes: 8 additions & 0 deletions backend/pkg/kubeconfig/kubeconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,8 @@
oidcScopes string,
oidcSkipTLSVerify bool,
oidcCACert string,
useServiceAccountToken bool,
serviceAccountTokenPath string,
) (*Context, error) {
clusterConfig, err := rest.InClusterConfig()
if err != nil {
Expand All @@ -1032,6 +1034,12 @@
contextName = makeDNSFriendly(contextName)

inClusterAuthInfo := &api.AuthInfo{}
if useServiceAccountToken {

Check failure on line 1037 in backend/pkg/kubeconfig/kubeconfig.go

View workflow job for this annotation

GitHub Actions / build

if statements should only be cuddled with assignments used in the if statement itself (wsl)
if serviceAccountTokenPath == "" {
serviceAccountTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"

Check failure on line 1039 in backend/pkg/kubeconfig/kubeconfig.go

View workflow job for this annotation

GitHub Actions / build

G101: Potential hardcoded credentials (gosec)
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default token path is hard-coded here. Since you already have clusterConfig from rest.InClusterConfig(), consider using clusterConfig.BearerTokenFile as the default when serviceAccountTokenPath is empty. That keeps the behavior aligned with client-go defaults if they ever change and avoids duplicating the path constant.

Suggested change
serviceAccountTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"
if clusterConfig.BearerTokenFile != "" {
serviceAccountTokenPath = clusterConfig.BearerTokenFile
} else {
serviceAccountTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"
}

Copilot uses AI. Check for mistakes.
}
inClusterAuthInfo.TokenFile = serviceAccountTokenPath

Check failure on line 1041 in backend/pkg/kubeconfig/kubeconfig.go

View workflow job for this annotation

GitHub Actions / build

assignments should only be cuddled with other assignments (wsl)
}

var oidcConf *OidcConfig

Expand Down
6 changes: 6 additions & 0 deletions charts/headlamp/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,12 @@ spec:
{{- with .Values.config.pluginsDir}}
- "-plugins-dir={{ . }}"
{{- end }}
{{- if .Values.config.useServiceAccountToken }}
- "-use-service-account-token"
{{- end }}
{{- if and .Values.config.useServiceAccountToken .Values.config.serviceAccountTokenPath }}
Comment on lines +248 to +251
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Helm template adds -use-service-account-token (and the token path flag) even if .Values.config.inCluster is false. That will cause the backend to fail config validation/startup because --use-service-account-token is only valid in in-cluster mode. Gate these args behind and .Values.config.inCluster .Values.config.useServiceAccountToken (and similarly for the path).

Suggested change
{{- if .Values.config.useServiceAccountToken }}
- "-use-service-account-token"
{{- end }}
{{- if and .Values.config.useServiceAccountToken .Values.config.serviceAccountTokenPath }}
{{- if and .Values.config.inCluster .Values.config.useServiceAccountToken }}
- "-use-service-account-token"
{{- end }}
{{- if and .Values.config.inCluster .Values.config.useServiceAccountToken .Values.config.serviceAccountTokenPath }}

Copilot uses AI. Check for mistakes.
- "-service-account-token-path={{ .Values.config.serviceAccountTokenPath }}"
{{- end }}
{{- if not $oidc.externalSecret.enabled}}
# Check if externalSecret is disabled
{{- if or (ne $oidc.clientID "") (ne $clientID "") }}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
---
# Source: headlamp/templates/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: headlamp
labels:
helm.sh/chart: headlamp-0.39.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
app.kubernetes.io/version: "0.39.0"
app.kubernetes.io/managed-by: Helm
---
# Source: headlamp/templates/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: oidc
type: Opaque
data:
---
# Source: headlamp/templates/clusterrolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: headlamp-admin
labels:
helm.sh/chart: headlamp-0.39.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
app.kubernetes.io/version: "0.39.0"
app.kubernetes.io/managed-by: Helm
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: headlamp
namespace: default
---
# Source: headlamp/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: headlamp
labels:
helm.sh/chart: headlamp-0.39.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
app.kubernetes.io/version: "0.39.0"
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP

ports:
- port: 80
targetPort: http
protocol: TCP
name: http
selector:
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
---
# Source: headlamp/templates/deployment.yaml
# This block of code is used to extract the values from the env.
# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml.

apiVersion: apps/v1
kind: Deployment
metadata:
name: headlamp
labels:
helm.sh/chart: headlamp-0.39.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
app.kubernetes.io/version: "0.39.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
template:
metadata:
labels:
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
spec:
serviceAccountName: headlamp
securityContext:
{}
containers:
- name: headlamp
securityContext:
privileged: false
runAsGroup: 101
runAsNonRoot: true
runAsUser: 100
image: "ghcr.io/headlamp-k8s/headlamp:v0.39.0"
imagePullPolicy: IfNotPresent

env:
args:
- "-in-cluster"
- "-plugins-dir=/headlamp/plugins"
- "-use-service-account-token"
- "-service-account-token-path=/custom/token"
# Check if externalSecret is disabled
ports:
- name: http
containerPort: 4466
protocol: TCP
livenessProbe:
httpGet:
path: "/"
port: http
readinessProbe:
httpGet:
path: "/"
port: http
resources:
{}
Loading
Loading