feat: Kubernetes native integration — Helm chart + K8s/SPIFFE config
Helm chart (charts/bascule/): Deployment with shell sidecar container (shared jumphost model) Service (LoadBalancer/NodePort/ClusterIP) ConfigMap with auto-generated config.toml RBAC (Role + RoleBinding for pods/exec) NetworkPolicy (restrict shell egress, allow DNS + K8s API) ServiceAccount with create flag Configurable shell image (k8s-ops, net-ops, dev, minimal) Helm lint passes clean K8s backend config (bascule-core): [k8s] section: enabled, namespace, pod_name, shell_container, shell Auto-detection via POD_NAME/POD_NAMESPACE env vars (downward API) Backend priority: K8s > proxy > container > local PTY K8s exec implementation deferred to --features k8s (kube crate) SPIFFE/SPIRE auth config: [auth.spiffe] section: trust_domain, trust_bundle_path, workload_api_socket JWT-SVID token-as-password authentication pattern Implementation deferred to bascule-auth-spiffe crate Zero substrate dependencies. Default build unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
043b9b9bdc
commit
9dc5cb9eee
11 changed files with 368 additions and 0 deletions
12
charts/bascule/Chart.yaml
Normal file
12
charts/bascule/Chart.yaml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
apiVersion: v2
|
||||||
|
name: bascule
|
||||||
|
description: Identity-aware SSH proxy for Kubernetes
|
||||||
|
version: 0.1.0
|
||||||
|
appVersion: "0.1.0"
|
||||||
|
type: application
|
||||||
|
keywords:
|
||||||
|
- ssh
|
||||||
|
- proxy
|
||||||
|
- identity
|
||||||
|
- jumphost
|
||||||
|
- bastion
|
||||||
12
charts/bascule/templates/NOTES.txt
Normal file
12
charts/bascule/templates/NOTES.txt
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
Bascule SSH proxy deployed!
|
||||||
|
|
||||||
|
Connect:
|
||||||
|
{{- if eq .Values.service.type "LoadBalancer" }}
|
||||||
|
ssh -p {{ .Values.service.port }} $(kubectl get svc {{ include "bascule.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
|
||||||
|
{{- else }}
|
||||||
|
kubectl port-forward svc/{{ include "bascule.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }}
|
||||||
|
ssh -p {{ .Values.service.port }} localhost
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
Shell: {{ .Values.shell.image.repository }}:{{ .Values.shell.image.tag }}
|
||||||
|
Auth: {{ .Values.auth.mode }}
|
||||||
36
charts/bascule/templates/_helpers.tpl
Normal file
36
charts/bascule/templates/_helpers.tpl
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
{{- define "bascule.name" -}}
|
||||||
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "bascule.fullname" -}}
|
||||||
|
{{- if .Values.fullnameOverride }}
|
||||||
|
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||||
|
{{- if contains $name .Release.Name }}
|
||||||
|
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "bascule.labels" -}}
|
||||||
|
helm.sh/chart: {{ include "bascule.name" . }}
|
||||||
|
{{ include "bascule.selectorLabels" . }}
|
||||||
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "bascule.selectorLabels" -}}
|
||||||
|
app.kubernetes.io/name: {{ include "bascule.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "bascule.serviceAccountName" -}}
|
||||||
|
{{- if .Values.serviceAccount.create }}
|
||||||
|
{{- default (include "bascule.fullname" .) .Values.serviceAccount.name }}
|
||||||
|
{{- else }}
|
||||||
|
{{- default "default" .Values.serviceAccount.name }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
22
charts/bascule/templates/configmap.yaml
Normal file
22
charts/bascule/templates/configmap.yaml
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: {{ include "bascule.fullname" . }}-config
|
||||||
|
data:
|
||||||
|
config.toml: |
|
||||||
|
listen_addr = "0.0.0.0:2222"
|
||||||
|
max_sessions = {{ .Values.maxSessions }}
|
||||||
|
{{- if .Values.hostKey.persistence }}
|
||||||
|
host_key_path = "/var/lib/bascule/host_key"
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
[auth]
|
||||||
|
mode = "{{ .Values.auth.mode }}"
|
||||||
|
{{- if eq .Values.auth.mode "authorized-keys" }}
|
||||||
|
authorized_keys_path = "{{ .Values.auth.authorizedKeysPath }}"
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
[k8s]
|
||||||
|
enabled = true
|
||||||
|
shell_container = "shell"
|
||||||
|
shell = "/bin/bash"
|
||||||
102
charts/bascule/templates/deployment.yaml
Normal file
102
charts/bascule/templates/deployment.yaml
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "bascule.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "bascule.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
replicas: {{ .Values.replicaCount }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "bascule.selectorLabels" . | nindent 6 }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
{{- include "bascule.selectorLabels" . | nindent 8 }}
|
||||||
|
spec:
|
||||||
|
serviceAccountName: {{ include "bascule.serviceAccountName" . }}
|
||||||
|
containers:
|
||||||
|
- name: bascule
|
||||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
ports:
|
||||||
|
- name: ssh
|
||||||
|
containerPort: 2222
|
||||||
|
protocol: TCP
|
||||||
|
env:
|
||||||
|
- name: BASCULE_CONFIG
|
||||||
|
value: /etc/bascule/config.toml
|
||||||
|
- name: POD_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.name
|
||||||
|
- name: POD_NAMESPACE
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.namespace
|
||||||
|
- name: BASCULE_SHELL_CONTAINER
|
||||||
|
value: shell
|
||||||
|
{{- range .Values.extraEnv }}
|
||||||
|
- name: {{ .name }}
|
||||||
|
value: {{ .value | quote }}
|
||||||
|
{{- end }}
|
||||||
|
volumeMounts:
|
||||||
|
- name: config
|
||||||
|
mountPath: /etc/bascule
|
||||||
|
readOnly: true
|
||||||
|
{{- if .Values.hostKey.persistence }}
|
||||||
|
- name: hostkey
|
||||||
|
mountPath: /var/lib/bascule
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.auth.authorizedKeysSecret }}
|
||||||
|
- name: authorized-keys
|
||||||
|
mountPath: /etc/bascule/keys
|
||||||
|
readOnly: true
|
||||||
|
{{- end }}
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: ssh
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 30
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: ssh
|
||||||
|
initialDelaySeconds: 3
|
||||||
|
periodSeconds: 10
|
||||||
|
{{- if .Values.shell.enabled }}
|
||||||
|
- name: shell
|
||||||
|
image: "{{ .Values.shell.image.repository }}:{{ .Values.shell.image.tag }}"
|
||||||
|
imagePullPolicy: {{ .Values.shell.image.pullPolicy }}
|
||||||
|
command: ["sleep", "infinity"]
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.shell.resources | nindent 12 }}
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: ["ALL"]
|
||||||
|
add: ["SETUID", "SETGID"]
|
||||||
|
{{- end }}
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
configMap:
|
||||||
|
name: {{ include "bascule.fullname" . }}-config
|
||||||
|
{{- if .Values.hostKey.persistence }}
|
||||||
|
- name: hostkey
|
||||||
|
emptyDir: {}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.auth.authorizedKeysSecret }}
|
||||||
|
- name: authorized-keys
|
||||||
|
secret:
|
||||||
|
secretName: {{ .Values.auth.authorizedKeysSecret }}
|
||||||
|
defaultMode: 0600
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.nodeSelector }}
|
||||||
|
nodeSelector:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
30
charts/bascule/templates/networkpolicy.yaml
Normal file
30
charts/bascule/templates/networkpolicy.yaml
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{{- if .Values.networkPolicy.enabled }}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: {{ include "bascule.fullname" . }}
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "bascule.selectorLabels" . | nindent 6 }}
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
- Egress
|
||||||
|
ingress:
|
||||||
|
- ports:
|
||||||
|
- port: 2222
|
||||||
|
protocol: TCP
|
||||||
|
egress:
|
||||||
|
- ports:
|
||||||
|
- port: 53
|
||||||
|
protocol: UDP
|
||||||
|
- port: 53
|
||||||
|
protocol: TCP
|
||||||
|
{{- if .Values.networkPolicy.allowKubeApi }}
|
||||||
|
- ports:
|
||||||
|
- port: 443
|
||||||
|
protocol: TCP
|
||||||
|
- port: 6443
|
||||||
|
protocol: TCP
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
25
charts/bascule/templates/rbac.yaml
Normal file
25
charts/bascule/templates/rbac.yaml
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{{- if .Values.rbac.create }}
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: {{ include "bascule.fullname" . }}
|
||||||
|
rules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["pods/exec"]
|
||||||
|
verbs: ["create"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["pods"]
|
||||||
|
verbs: ["get", "list"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: {{ include "bascule.fullname" . }}
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: {{ include "bascule.serviceAccountName" . }}
|
||||||
|
roleRef:
|
||||||
|
kind: Role
|
||||||
|
name: {{ include "bascule.fullname" . }}
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
{{- end }}
|
||||||
15
charts/bascule/templates/service.yaml
Normal file
15
charts/bascule/templates/service.yaml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "bascule.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "bascule.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.service.type }}
|
||||||
|
ports:
|
||||||
|
- port: {{ .Values.service.port }}
|
||||||
|
targetPort: ssh
|
||||||
|
protocol: TCP
|
||||||
|
name: ssh
|
||||||
|
selector:
|
||||||
|
{{- include "bascule.selectorLabels" . | nindent 4 }}
|
||||||
8
charts/bascule/templates/serviceaccount.yaml
Normal file
8
charts/bascule/templates/serviceaccount.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{{- if .Values.serviceAccount.create }}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: {{ include "bascule.serviceAccountName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "bascule.labels" . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
65
charts/bascule/values.yaml
Normal file
65
charts/bascule/values.yaml
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
replicaCount: 1
|
||||||
|
|
||||||
|
image:
|
||||||
|
repository: ghcr.io/guildhouse/bascule-server
|
||||||
|
tag: "latest"
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
|
shell:
|
||||||
|
enabled: true
|
||||||
|
image:
|
||||||
|
repository: ghcr.io/guildhouse/bascule-shell
|
||||||
|
tag: "k8s-ops"
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
cpu: "1"
|
||||||
|
memory: 512Mi
|
||||||
|
|
||||||
|
service:
|
||||||
|
type: LoadBalancer
|
||||||
|
port: 2222
|
||||||
|
|
||||||
|
auth:
|
||||||
|
mode: authorized-keys
|
||||||
|
authorizedKeysPath: /etc/bascule/keys
|
||||||
|
authorizedKeysSecret: ""
|
||||||
|
|
||||||
|
maxSessions: 100
|
||||||
|
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 256Mi
|
||||||
|
|
||||||
|
hostKey:
|
||||||
|
persistence: true
|
||||||
|
size: 1Mi
|
||||||
|
|
||||||
|
serviceAccount:
|
||||||
|
create: true
|
||||||
|
name: ""
|
||||||
|
|
||||||
|
rbac:
|
||||||
|
create: true
|
||||||
|
|
||||||
|
networkPolicy:
|
||||||
|
enabled: true
|
||||||
|
allowKubeApi: true
|
||||||
|
denyAllOtherEgress: true
|
||||||
|
|
||||||
|
podDisruptionBudget:
|
||||||
|
enabled: false
|
||||||
|
minAvailable: 1
|
||||||
|
|
||||||
|
extraEnv: []
|
||||||
|
|
||||||
|
tolerations: []
|
||||||
|
affinity: {}
|
||||||
|
nodeSelector: {}
|
||||||
|
|
@ -40,6 +40,10 @@ pub struct BasculeConfig {
|
||||||
/// Priority: proxy > container > local PTY.
|
/// Priority: proxy > container > local PTY.
|
||||||
pub container: Option<ContainerConfig>,
|
pub container: Option<ContainerConfig>,
|
||||||
|
|
||||||
|
/// K8s backend configuration.
|
||||||
|
/// When running in-cluster, exec into a shell sidecar instead of local PTY.
|
||||||
|
pub k8s: Option<K8sConfig>,
|
||||||
|
|
||||||
/// Telemetry (OTel tracing).
|
/// Telemetry (OTel tracing).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub telemetry: TelemetryConfig,
|
pub telemetry: TelemetryConfig,
|
||||||
|
|
@ -78,6 +82,9 @@ pub struct AuthConfig {
|
||||||
|
|
||||||
/// AI agent authentication via Microsoft Entra Agent ID.
|
/// AI agent authentication via Microsoft Entra Agent ID.
|
||||||
pub agent_id: Option<AgentIdConfig>,
|
pub agent_id: Option<AgentIdConfig>,
|
||||||
|
|
||||||
|
/// SPIFFE/SPIRE workload identity authentication.
|
||||||
|
pub spiffe: Option<SpiffeConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AuthConfig {
|
impl Default for AuthConfig {
|
||||||
|
|
@ -86,6 +93,7 @@ impl Default for AuthConfig {
|
||||||
mode: default_auth_mode(),
|
mode: default_auth_mode(),
|
||||||
authorized_keys_path: None,
|
authorized_keys_path: None,
|
||||||
agent_id: None,
|
agent_id: None,
|
||||||
|
spiffe: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -114,6 +122,7 @@ impl Default for BasculeConfig {
|
||||||
max_sessions: 0,
|
max_sessions: 0,
|
||||||
proxy: None,
|
proxy: None,
|
||||||
container: None,
|
container: None,
|
||||||
|
k8s: None,
|
||||||
telemetry: TelemetryConfig::default(),
|
telemetry: TelemetryConfig::default(),
|
||||||
metrics: MetricsConfig::default(),
|
metrics: MetricsConfig::default(),
|
||||||
}
|
}
|
||||||
|
|
@ -189,6 +198,38 @@ pub struct MountConfig {
|
||||||
pub readonly: bool,
|
pub readonly: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// SPIFFE/SPIRE authentication configuration.
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct SpiffeConfig {
|
||||||
|
/// SPIFFE trust domain.
|
||||||
|
pub trust_domain: String,
|
||||||
|
/// Path to trust bundle PEM file.
|
||||||
|
pub trust_bundle_path: Option<String>,
|
||||||
|
/// SPIRE Workload API socket path.
|
||||||
|
pub workload_api_socket: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// K8s backend configuration.
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct K8sConfig {
|
||||||
|
/// Enable K8s backend.
|
||||||
|
#[serde(default)]
|
||||||
|
pub enabled: bool,
|
||||||
|
/// Namespace (auto-detected from downward API if not set).
|
||||||
|
pub namespace: Option<String>,
|
||||||
|
/// Pod name (auto-detected from downward API if not set).
|
||||||
|
pub pod_name: Option<String>,
|
||||||
|
/// Shell container name in the Pod.
|
||||||
|
#[serde(default = "default_shell_container")]
|
||||||
|
pub shell_container: String,
|
||||||
|
/// Shell command inside the container.
|
||||||
|
#[serde(default = "default_k8s_shell")]
|
||||||
|
pub shell: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_shell_container() -> String { "shell".to_string() }
|
||||||
|
fn default_k8s_shell() -> String { "/bin/bash".to_string() }
|
||||||
|
|
||||||
/// Telemetry configuration (OTel + metrics).
|
/// Telemetry configuration (OTel + metrics).
|
||||||
#[derive(Debug, Deserialize, Clone, Default)]
|
#[derive(Debug, Deserialize, Clone, Default)]
|
||||||
pub struct TelemetryConfig {
|
pub struct TelemetryConfig {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue