Skip to content

Commit f1685fa

Browse files
committed
frontend,backend: Add exec credential plugin auth support
Add support for exec-based credential plugins (Teleport tsh, aws-iam-authenticator) and client certificate authentication. Backend changes: - kubeconfig: Detect auth type from exec config - kubeconfig: Return "tsh" for Teleport, "exec" for others Frontend changes: - clusterApi: Use /version endpoint for exec/cert auth - clusterApi: Add backward-compatible options parameter - RouteSwitcher: Show auth-type specific error messages - RouteSwitcher: Wait for clusters to load before auth check
1 parent 30e1c46 commit f1685fa

File tree

4 files changed

+120
-11
lines changed

4 files changed

+120
-11
lines changed

backend/pkg/kubeconfig/kubeconfig.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net/http/httputil"
99
"net/url"
1010
"os"
11+
"path/filepath"
1112
"runtime"
1213
"strings"
1314

@@ -441,11 +442,39 @@ func (c *Context) SetupProxy() error {
441442
}
442443

443444
// AuthType returns the authentication type for the context.
445+
// Returns "oidc" for OIDC authentication, "tsh" for Teleport exec plugin,
446+
// "exec" for other exec credential plugins, "client-cert" for client certificate auth,
447+
// or empty string for other auth types (token, basic auth, etc.).
448+
//
449+
// Priority order (first match wins):
450+
// 1. OIDC (AuthProvider configured)
451+
// 2. Exec plugin (tsh is detected specifically, otherwise generic "exec")
452+
// 3. Client certificate
453+
//
454+
// This priority is intentional: exec plugins are dynamic auth mechanisms that
455+
// require backend execution, while client certificates are static TLS credentials.
456+
// When both are present, exec typically represents the primary authentication method.
444457
func (c *Context) AuthType() string {
445458
if (c.OidcConf != nil) || (c.AuthInfo != nil && c.AuthInfo.AuthProvider != nil) {
446459
return "oidc"
447460
}
448461

462+
// Check for exec credential plugin (e.g., Teleport tsh, aws-iam-authenticator)
463+
if c.AuthInfo != nil && c.AuthInfo.Exec != nil {
464+
// Check if it's Teleport's tsh command by checking the basename
465+
// Using filepath.Base to avoid false positives like "/usr/bin/mytshell"
466+
if filepath.Base(c.AuthInfo.Exec.Command) == "tsh" {
467+
return "tsh"
468+
}
469+
470+
return "exec"
471+
}
472+
473+
// Check for client certificate authentication
474+
if c.AuthInfo != nil && (c.AuthInfo.ClientCertificate != "" || len(c.AuthInfo.ClientCertificateData) > 0) {
475+
return "client-cert"
476+
}
477+
449478
return ""
450479
}
451480

frontend/src/components/App/RouteSwitcher.tsx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ interface AuthRouteProps {
143143
requiresAuth: boolean;
144144
requiresCluster: boolean;
145145
requiresToken: () => boolean;
146+
clusters: ReturnType<typeof useClustersConf>;
146147
[otherProps: string]: any;
147148
}
148149

@@ -153,15 +154,24 @@ function AuthRoute(props: AuthRouteProps) {
153154
requiresAuth = true,
154155
requiresCluster = true,
155156
computedMatch = {},
157+
clusters,
156158
...other
157159
} = props;
158160
const redirectRoute = getCluster() ? 'login' : 'chooser';
159161
useSidebarItem(sidebar, computedMatch);
160162
const cluster = useCluster();
163+
164+
// Get the cluster's auth type - for tsh/exec/client-cert, the backend handles auth
165+
const clusterConfig = cluster && clusters ? clusters[cluster] : null;
166+
const authType = clusterConfig?.auth_type || '';
167+
const isExecOrCertAuth = authType === 'tsh' || authType === 'exec' || authType === 'client-cert';
168+
161169
const query = useQuery({
162-
queryKey: ['auth', cluster],
163-
queryFn: () => testAuth(cluster!),
164-
enabled: !!cluster && requiresAuth,
170+
queryKey: ['auth', cluster, authType],
171+
queryFn: () => testAuth(cluster!, { authType }),
172+
// Run auth test for all auth types including exec/client-cert
173+
// Wait for clusters to be loaded to ensure we have the correct authType
174+
enabled: !!cluster && requiresAuth && !!clusters,
165175
retry: 0,
166176
});
167177

@@ -182,6 +192,30 @@ function AuthRoute(props: AuthRouteProps) {
182192
}
183193

184194
if (query.isError) {
195+
// For tsh/exec/client-cert auth, show a helpful error message
196+
if (isExecOrCertAuth) {
197+
let errorMessage: string;
198+
if (authType === 'tsh') {
199+
errorMessage =
200+
'Teleport authentication failed. Please run `tsh login` to refresh your credentials, then reload this page in your browser.';
201+
} else if (authType === 'exec') {
202+
errorMessage =
203+
'Exec credential plugin authentication failed. Please refresh your credentials and try again.';
204+
} else {
205+
errorMessage =
206+
'Client certificate authentication failed. Please check your certificates.';
207+
}
208+
return (
209+
<ErrorComponent
210+
error={{
211+
name: 'AuthenticationError',
212+
message: errorMessage,
213+
stack: query.error instanceof Error ? query.error.message : String(query.error),
214+
}}
215+
/>
216+
);
217+
}
218+
185219
return (
186220
<Redirect
187221
to={{

frontend/src/components/authchooser/index.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,15 @@ function AuthChooser({ children }: AuthChooserProps) {
105105
// cluster, then we check here.
106106
// With clusterAuthType == oidc,
107107
// they are presented with a choice of login or enter token.
108-
if (clusterAuthType !== 'oidc' && cluster.useToken === undefined) {
108+
// With clusterAuthType == exec, client-cert, or tsh,
109+
// RouteSwitcher handles auth - don't run testAuth here to avoid conflicts.
110+
if (
111+
clusterAuthType !== 'oidc' &&
112+
clusterAuthType !== 'exec' &&
113+
clusterAuthType !== 'client-cert' &&
114+
clusterAuthType !== 'tsh' &&
115+
cluster.useToken === undefined
116+
) {
109117
let useToken = true;
110118

111119
setTestingAuth(true);

frontend/src/lib/k8s/api/v1/clusterApi.ts

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,56 @@ import type { ClusterRequest } from './clusterRequests';
2727
import { clusterRequest, post, request } from './clusterRequests';
2828
import { JSON_HEADERS } from './constants';
2929

30+
/**
31+
* Options for testAuth function
32+
*/
33+
interface TestAuthOptions {
34+
/** Namespace to use for authorization check (default: 'default') */
35+
namespace?: string;
36+
/** Auth type ('exec', 'client-cert', 'oidc', 'tsh', etc.) */
37+
authType?: string;
38+
}
39+
3040
/**
3141
* Test authentication for the given cluster.
32-
* Will throw an error if the user is not authenticated.
42+
* Will throw an error if the user is not authenticated (401).
43+
* Returns success for 403 since that means auth succeeded but user lacks permissions.
44+
* @param cluster - The cluster to test auth for
45+
* @param options - Can be a string (legacy: treated as namespace) or TestAuthOptions object
3346
*/
34-
export async function testAuth(cluster = '', namespace = 'default') {
35-
const spec = { namespace };
47+
export async function testAuth(cluster = '', options?: string | TestAuthOptions) {
3648
const clusterName = cluster || getCluster();
3749

38-
return post('/apis/authorization.k8s.io/v1/selfsubjectrulesreviews', { spec }, false, {
39-
timeout: 5 * 1000,
40-
cluster: clusterName,
41-
});
50+
// Backward compatibility: if options is a string, treat it as namespace (legacy behavior)
51+
const resolvedOptions: TestAuthOptions =
52+
typeof options === 'string' ? { namespace: options } : options ?? {};
53+
54+
const { namespace = 'default', authType } = resolvedOptions;
55+
56+
try {
57+
// For tsh/exec auth (e.g., Teleport), use the cluster version endpoint
58+
// which is accessible to any authenticated user without RBAC restrictions
59+
if (authType === 'tsh' || authType === 'exec' || authType === 'client-cert') {
60+
return clusterRequest('/version', {
61+
timeout: 5 * 1000, // Longer timeout for exec auth plugins like tsh
62+
cluster: clusterName,
63+
});
64+
}
65+
66+
// For standard auth types (token, oidc), use selfsubjectrulesreviews
67+
const spec = { namespace };
68+
return post('/apis/authorization.k8s.io/v1/selfsubjectrulesreviews', { spec }, false, {
69+
timeout: 5 * 1000,
70+
cluster: clusterName,
71+
});
72+
} catch (err: any) {
73+
// 403 (Forbidden) means authentication worked, but user lacks permissions.
74+
if (err.status === 403) {
75+
return { authenticated: true, status: err.status };
76+
}
77+
// Re-throw 401 and other errors - those are real auth failures
78+
throw err;
79+
}
4280
}
4381

4482
/**

0 commit comments

Comments
 (0)