Skip to content

Commit b9e2ca2

Browse files
mjudeikis-botOpenClaw Botmjudeikis
authored
feat: SSH user mapping modes for server-type edges (#92)
* feat: SSH user mapping modes for server-type edges (inherited/provided/identity) Add spec.server.sshUserMapping field with three modes: - inherited (default): username+key from agent-reported Edge.Status.SSHCredentials - provided: username+key from admin-configured spec.server.sshCredentialsRef Secret - identity: username from caller kcp/OIDC identity; key from sshCredentialsRef ## API changes - New type SSHUserMappingMode (string) with constants: - SSHUserMappingInherited = "inherited" - SSHUserMappingProvided = "provided" - SSHUserMappingIdentity = "identity" - ServerEdgeSpec.SSHUserMapping field (default: inherited) - ServerEdgeSpec.SSHCredentialsRef field (*corev1.SecretReference) - Regenerated CRDs via make codegen ## Hub changes (pkg/virtual/builder/edges_proxy_builder.go) - resolveCallerIdentity(): kcp TokenReview to extract caller username (best-effort) - edgesSSHHandler: accepts callerIdentity, passes to fetchSSHCredentials - fetchSSHCredentials: branches on SSHUserMapping mode - readStatusSSHCreds(): extracted helper for inherited/fallback path - readSSHCredsFromSecret(): new helper reads username+key from a Secret (usernameOverride non-empty overrides the Secret's username field) ## Test infrastructure changes - TestSSHServer: added AddUser()/AddAnyUserKey() for configurable auth, ConnectedUsers() for test assertions, USER env var in spawned commands - ServerProcess: added SSHUser/SSHPassword fields; SSHServer is now public and can be pre-configured before Start() - New framework helpers: GenerateTestSSHKeypair, IndentLines, ResolveCallerIdentity, context helpers for key/identity ## New e2e tests - SSHUserMappingInherited: agent reports testuser, verified via echo $USER - SSHUserMappingProvided: hub reads username+key from sshCredentialsRef Secret - SSHUserMappingIdentity: hub uses TokenReview caller identity as SSH username Closes #53 Co-authored-by: Mangirdas Judeikis <mangirdas@judeikis.lt> * fix: add --ssh-user/--ssh-password/--ssh-private-key flags to 'kedge agent join' CLI The e2e tests invoke the agent via the 'kedge agent join' subcommand, not the standalone kedge-agent binary. The SSH credential flags (--ssh-user, --ssh-password, --ssh-private-key) were added to cmd/kedge-agent/main.go but were not propagated to pkg/cli/cmd/agent.go, causing TestSSHUserMappingInherited to fail with 'Error: unknown flag: --ssh-user'. Co-authored-by: Mangirdas Judeikis <mangirdas@judeikis.lt> --------- Co-authored-by: OpenClaw Bot <bot@openclaw.ai> Co-authored-by: Mangirdas Judeikis <mangirdas@judeikis.lt>
1 parent d4ea891 commit b9e2ca2

13 files changed

Lines changed: 934 additions & 47 deletions

File tree

apis/kedge/v1alpha1/types_edge.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,18 @@ const (
3131
EdgeTypeServer EdgeType = "server"
3232
)
3333

34+
// SSHUserMappingMode controls SSH username selection for server-type edges.
35+
type SSHUserMappingMode string
36+
37+
const (
38+
// SSHUserMappingInherited uses the credentials reported by the agent at registration.
39+
SSHUserMappingInherited SSHUserMappingMode = "inherited"
40+
// SSHUserMappingProvided uses admin-configured credentials from spec.server.sshCredentialsRef.
41+
SSHUserMappingProvided SSHUserMappingMode = "provided"
42+
// SSHUserMappingIdentity uses the caller's kcp/OIDC username as the SSH username.
43+
SSHUserMappingIdentity SSHUserMappingMode = "identity"
44+
)
45+
3446
// EdgePhase describes the lifecycle phase of an Edge.
3547
type EdgePhase string
3648

@@ -118,6 +130,21 @@ type ServerEdgeSpec struct {
118130
// When set, the hub serves this key to authenticated CLI clients via the /ssh subresource.
119131
// +optional
120132
SSHKeySecretRef *corev1.SecretReference `json:"sshKeySecretRef,omitempty"`
133+
134+
// SSHUserMapping controls how the SSH username is determined for callers.
135+
// inherited: use credentials reported by the agent at registration (default).
136+
// provided: use credentials from spec.server.sshCredentialsRef Secret.
137+
// identity: use the caller's kcp/OIDC username; key from sshCredentialsRef.
138+
// +kubebuilder:validation:Enum=inherited;provided;identity
139+
// +kubebuilder:default=inherited
140+
// +optional
141+
SSHUserMapping SSHUserMappingMode `json:"sshUserMapping,omitempty"`
142+
143+
// SSHCredentialsRef references a Secret with admin-configured SSH credentials.
144+
// Used when sshUserMapping=provided, or as the key source for identity mode.
145+
// The Secret must contain: "username" (string) and one of "privateKey" or "password".
146+
// +optional
147+
SSHCredentialsRef *corev1.SecretReference `json:"sshCredentialsRef,omitempty"`
121148
}
122149

123150
// EdgeStatus defines the observed state of an Edge.

apis/kedge/v1alpha1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crds/kedge.faros.sh_edges.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,22 @@ spec:
7676
Server holds configuration specific to SSH server-mode edges.
7777
Only valid when type=server.
7878
properties:
79+
sshCredentialsRef:
80+
description: |-
81+
SSHCredentialsRef references a Secret with admin-configured SSH credentials.
82+
Used when sshUserMapping=provided, or as the key source for identity mode.
83+
The Secret must contain: "username" (string) and one of "privateKey" or "password".
84+
properties:
85+
name:
86+
description: name is unique within a namespace to reference
87+
a secret resource.
88+
type: string
89+
namespace:
90+
description: namespace defines the space within which the
91+
secret name must be unique.
92+
type: string
93+
type: object
94+
x-kubernetes-map-type: atomic
7995
sshKeySecretRef:
8096
description: |-
8197
SSHKeySecretRef references a Secret containing the SSH private key (key: id_rsa).
@@ -96,6 +112,18 @@ spec:
96112
description: 'SSHPort is the port sshd listens on inside the remote
97113
host (default: 22).'
98114
type: integer
115+
sshUserMapping:
116+
default: inherited
117+
description: |-
118+
SSHUserMapping controls how the SSH username is determined for callers.
119+
inherited: use credentials reported by the agent at registration (default).
120+
provided: use credentials from spec.server.sshCredentialsRef Secret.
121+
identity: use the caller's kcp/OIDC username; key from sshCredentialsRef.
122+
enum:
123+
- inherited
124+
- provided
125+
- identity
126+
type: string
99127
type: object
100128
type:
101129
description: Type discriminates between Kubernetes cluster and SSH

config/kcp/apiexport-kedge.faros.sh.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ spec:
6565
resources:
6666
- group: kedge.faros.sh
6767
name: edges
68-
schema: v260303-6b34333.edges.kedge.faros.sh
68+
schema: v260304-d4ea891.edges.kedge.faros.sh
6969
storage:
7070
crd: {}
7171
- group: kedge.faros.sh

config/kcp/apiresourceschema-edges.kedge.faros.sh.yaml

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
apiVersion: apis.kcp.io/v1alpha1
22
kind: APIResourceSchema
33
metadata:
4-
name: v260303-6b34333.edges.kedge.faros.sh
4+
name: v260304-d4ea891.edges.kedge.faros.sh
55
spec:
66
group: kedge.faros.sh
77
names:
@@ -72,6 +72,22 @@ spec:
7272
Server holds configuration specific to SSH server-mode edges.
7373
Only valid when type=server.
7474
properties:
75+
sshCredentialsRef:
76+
description: |-
77+
SSHCredentialsRef references a Secret with admin-configured SSH credentials.
78+
Used when sshUserMapping=provided, or as the key source for identity mode.
79+
The Secret must contain: "username" (string) and one of "privateKey" or "password".
80+
properties:
81+
name:
82+
description: name is unique within a namespace to reference
83+
a secret resource.
84+
type: string
85+
namespace:
86+
description: namespace defines the space within which the secret
87+
name must be unique.
88+
type: string
89+
type: object
90+
x-kubernetes-map-type: atomic
7591
sshKeySecretRef:
7692
description: |-
7793
SSHKeySecretRef references a Secret containing the SSH private key (key: id_rsa).
@@ -92,6 +108,18 @@ spec:
92108
description: 'SSHPort is the port sshd listens on inside the remote
93109
host (default: 22).'
94110
type: integer
111+
sshUserMapping:
112+
default: inherited
113+
description: |-
114+
SSHUserMapping controls how the SSH username is determined for callers.
115+
inherited: use credentials reported by the agent at registration (default).
116+
provided: use credentials from spec.server.sshCredentialsRef Secret.
117+
identity: use the caller's kcp/OIDC username; key from sshCredentialsRef.
118+
enum:
119+
- inherited
120+
- provided
121+
- identity
122+
type: string
95123
type: object
96124
type:
97125
description: Type discriminates between Kubernetes cluster and SSH server

pkg/cli/cmd/agent.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ func newAgentJoinCommand() *cobra.Command {
7676
`Edge type: "kubernetes" (Kubernetes cluster) or "server" (bare-metal/systemd host with SSH access)`)
7777
cmd.Flags().StringVar(&opts.Cluster, "cluster", "",
7878
"kcp logical cluster name (e.g. '1tww43gelbj45g0k'); required when using static token auth without a cluster-scoped hub kubeconfig")
79+
cmd.Flags().StringVar(&opts.SSHUser, "ssh-user", "", "SSH username for server-type edges (default: current user)")
80+
cmd.Flags().StringVar(&opts.SSHPassword, "ssh-password", "", "SSH password for password-based authentication (prefer --ssh-private-key for security)")
81+
cmd.Flags().StringVar(&opts.SSHPrivateKeyPath, "ssh-private-key", "", "Path to SSH private key file for key-based authentication")
7982

8083
return cmd
8184
}

pkg/hub/bootstrap/crds/kedge.faros.sh_edges.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,22 @@ spec:
7676
Server holds configuration specific to SSH server-mode edges.
7777
Only valid when type=server.
7878
properties:
79+
sshCredentialsRef:
80+
description: |-
81+
SSHCredentialsRef references a Secret with admin-configured SSH credentials.
82+
Used when sshUserMapping=provided, or as the key source for identity mode.
83+
The Secret must contain: "username" (string) and one of "privateKey" or "password".
84+
properties:
85+
name:
86+
description: name is unique within a namespace to reference
87+
a secret resource.
88+
type: string
89+
namespace:
90+
description: namespace defines the space within which the
91+
secret name must be unique.
92+
type: string
93+
type: object
94+
x-kubernetes-map-type: atomic
7995
sshKeySecretRef:
8096
description: |-
8197
SSHKeySecretRef references a Secret containing the SSH private key (key: id_rsa).
@@ -96,6 +112,18 @@ spec:
96112
description: 'SSHPort is the port sshd listens on inside the remote
97113
host (default: 22).'
98114
type: integer
115+
sshUserMapping:
116+
default: inherited
117+
description: |-
118+
SSHUserMapping controls how the SSH username is determined for callers.
119+
inherited: use credentials reported by the agent at registration (default).
120+
provided: use credentials from spec.server.sshCredentialsRef Secret.
121+
identity: use the caller's kcp/OIDC username; key from sshCredentialsRef.
122+
enum:
123+
- inherited
124+
- provided
125+
- identity
126+
type: string
99127
type: object
100128
type:
101129
description: Type discriminates between Kubernetes cluster and SSH

0 commit comments

Comments
 (0)