microsoft/TypeAgent

Public

mirrored fromhttps://github.com/microsoft/TypeAgentAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
8fcc2174628a5ffa8055a25dc727377cd23ce3fd

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

.devcontainer/scripts/setup-ssh-access.sh

365lines · modecode

1#!/usr/bin/env bash
2# Copyright (c) Microsoft Corporation.
3# Licensed under the MIT License.
4
5set -euo pipefail
6
7SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
8REPO_ROOT=$(cd -- "$SCRIPT_DIR/../.." && pwd)
9WORKSPACE_FOLDER=$(cd -- "$REPO_ROOT" && pwd)
10DEFAULT_KEY_NAME="typeagent-devcontainer"
11DEFAULT_KEY_PATH="$HOME/.ssh/$DEFAULT_KEY_NAME"
12DEFAULT_CONFIG_PATH="$HOME/.ssh/config"
13DEFAULT_LOCAL_PORT="2222"
14REMOTE_USER="codespace"
15
16usage() {
17 cat <<EOF
18Usage: $(basename "$0") [options]
19
20Set up SSH key access to the running TypeAgent devcontainer.
21
22Options:
23 --workspace-folder PATH Workspace folder to match (default: repo root)
24 --config PATH Devcontainer config file to match (optional)
25 --key-path PATH Private key path to use (default: $DEFAULT_KEY_PATH)
26 --host-alias NAME SSH host alias to write to ~/.ssh/config (default: typeagent-devcontainer)
27 --local-port PORT Local SSH port to expose in ssh config (default: $DEFAULT_LOCAL_PORT)
28 --insecure-local Disable host key verification for local-only workflows
29 --print-only Do not modify files or container state; print detected values only
30 -h, --help Show this help text
31EOF
32}
33
34log() {
35 printf '[setup-ssh-access] %s\n' "$*"
36}
37
38fail() {
39 printf '[setup-ssh-access] Error: %s\n' "$*" >&2
40 exit 1
41}
42
43require_cmd() {
44 command -v "$1" >/dev/null 2>&1 || fail "Required command not found: $1"
45}
46
47is_wsl() {
48 [[ -n "${WSL_DISTRO_NAME:-}" ]] || grep -qiE "microsoft|wsl" /proc/version 2>/dev/null
49}
50
51ensure_ssh_config_block() {
52 local config_path=$1
53 local host_alias=$2
54 local key_path_for_config=$3
55 local strict_host_key_checking=$4
56 local user_known_hosts_file=$5
57 local global_known_hosts_file=$6
58
59 local ssh_config_block
60 ssh_config_block=$(cat <<EOF
61Host $host_alias
62 HostName localhost
63 Port $LOCAL_PORT
64 User $REMOTE_USER
65 IdentityFile $key_path_for_config
66 IdentitiesOnly yes
67 PreferredAuthentications publickey
68 PubkeyAuthentication yes
69 PasswordAuthentication no
70 KbdInteractiveAuthentication no
71 StrictHostKeyChecking $strict_host_key_checking
72 UserKnownHostsFile $user_known_hosts_file
73 GlobalKnownHostsFile $global_known_hosts_file
74EOF
75)
76
77 local config_begin_marker="# BEGIN typeagent-devcontainer:$host_alias"
78 local config_end_marker="# END typeagent-devcontainer:$host_alias"
79 local legacy_marker="# typeagent-devcontainer:$host_alias"
80
81 mkdir -p "$(dirname "$config_path")"
82 touch "$config_path"
83 chmod 600 "$config_path" 2>/dev/null || true
84
85 if [[ $PRINT_ONLY -eq 1 ]]; then
86 log "Would ensure SSH config block in $config_path"
87 return 0
88 fi
89
90 local tmp_file
91 tmp_file=$(mktemp)
92 trap 'rm -f "$tmp_file"' RETURN
93
94 awk \
95 -v alias="$host_alias" \
96 -v begin_marker="$config_begin_marker" \
97 -v end_marker="$config_end_marker" \
98 -v legacy_marker="$legacy_marker" '
99 $0 == begin_marker { in_managed=1; next }
100 in_managed {
101 if ($0 == end_marker) {
102 in_managed=0
103 }
104 next
105 }
106 $0 == legacy_marker { next }
107 $0 ~ ("^Host[[:space:]]+" alias "$") { in_alias_stanza=1; next }
108 in_alias_stanza {
109 if ($0 ~ /^Host[[:space:]]+/) {
110 in_alias_stanza=0
111 print
112 }
113 next
114 }
115 { print }
116 ' "$config_path" > "$tmp_file"
117 mv "$tmp_file" "$config_path"
118 trap - RETURN
119
120 {
121 if [[ -s "$config_path" ]] && [[ "$(tail -c 1 "$config_path")" != "" ]]; then
122 printf '\n'
123 fi
124 printf '%s\n%s\n%s\n' "$config_begin_marker" "$ssh_config_block" "$config_end_marker"
125 } >> "$config_path"
126}
127
128WORKSPACE_MATCH="$WORKSPACE_FOLDER"
129CONFIG_MATCH=""
130KEY_PATH="$DEFAULT_KEY_PATH"
131HOST_ALIAS="$DEFAULT_KEY_NAME"
132LOCAL_PORT="$DEFAULT_LOCAL_PORT"
133PRINT_ONLY=0
134INSECURE_LOCAL=0
135
136while [[ $# -gt 0 ]]; do
137 case "$1" in
138 --workspace-folder)
139 [[ $# -ge 2 ]] || fail "Missing value for $1"
140 WORKSPACE_MATCH=$(cd -- "$2" && pwd)
141 shift 2
142 ;;
143 --config)
144 [[ $# -ge 2 ]] || fail "Missing value for $1"
145 CONFIG_MATCH=$(cd -- "$(dirname -- "$2")" && pwd)/$(basename -- "$2")
146 shift 2
147 ;;
148 --key-path)
149 [[ $# -ge 2 ]] || fail "Missing value for $1"
150 KEY_PATH="$2"
151 shift 2
152 ;;
153 --host-alias)
154 [[ $# -ge 2 ]] || fail "Missing value for $1"
155 HOST_ALIAS="$2"
156 shift 2
157 ;;
158 --local-port)
159 [[ $# -ge 2 ]] || fail "Missing value for $1"
160 LOCAL_PORT="$2"
161 shift 2
162 ;;
163 --insecure-local)
164 INSECURE_LOCAL=1
165 shift
166 ;;
167 --print-only)
168 PRINT_ONLY=1
169 shift
170 ;;
171 -h|--help)
172 usage
173 exit 0
174 ;;
175 *)
176 fail "Unknown argument: $1"
177 ;;
178 esac
179done
180
181require_cmd docker
182require_cmd jq
183require_cmd ssh-keygen
184require_cmd ssh
185
186[[ "$HOST_ALIAS" =~ ^[a-zA-Z0-9._-]+$ ]] || fail "Invalid host alias: $HOST_ALIAS"
187[[ "$LOCAL_PORT" =~ ^[0-9]+$ ]] || fail "Invalid local port: $LOCAL_PORT"
188
189mkdir -p "$HOME/.ssh"
190chmod 700 "$HOME/.ssh"
191
192STRICT_HOST_KEY_CHECKING="accept-new"
193USER_KNOWN_HOSTS_FILE="$HOME/.ssh/known_hosts"
194GLOBAL_KNOWN_HOSTS_FILE="/etc/ssh/ssh_known_hosts"
195if [[ $INSECURE_LOCAL -eq 1 ]]; then
196 STRICT_HOST_KEY_CHECKING="no"
197 USER_KNOWN_HOSTS_FILE="/dev/null"
198 GLOBAL_KNOWN_HOSTS_FILE="/dev/null"
199fi
200
201find_container() {
202 local workspace=$1
203 local config=$2
204 local container_ids=()
205 mapfile -t container_ids < <(docker ps -q)
206 [[ ${#container_ids[@]} -gt 0 ]] || return 0
207
208 docker inspect "${container_ids[@]}" | jq -r --arg workspace "$workspace" --arg config "$config" '
209 .[]
210 | select(.Config.Labels["devcontainer.local_folder"] == $workspace)
211 | select(($config == "") or (.Config.Labels["devcontainer.config_file"] == $config))
212 | .Name
213 | ltrimstr("/")
214 ' | head -n 1
215}
216
217CONTAINER_NAME=$(find_container "$WORKSPACE_MATCH" "$CONFIG_MATCH")
218[[ -n "$CONTAINER_NAME" ]] || fail "No running devcontainer found for $WORKSPACE_MATCH${CONFIG_MATCH:+ using $CONFIG_MATCH}"
219
220log "Using container: $CONTAINER_NAME"
221log "Workspace match: $WORKSPACE_MATCH"
222if [[ -n "$CONFIG_MATCH" ]]; then
223 log "Config match: $CONFIG_MATCH"
224fi
225
226if [[ ! -f "$KEY_PATH" ]]; then
227 if [[ $PRINT_ONLY -eq 1 ]]; then
228 log "Would create SSH key: $KEY_PATH"
229 else
230 log "Creating SSH key: $KEY_PATH"
231 ssh-keygen -t ed25519 -f "$KEY_PATH" -N '' -C "$HOST_ALIAS"
232 fi
233else
234 log "Using existing SSH key: $KEY_PATH"
235fi
236
237PUB_KEY_PATH="$KEY_PATH.pub"
238if [[ ! -f "$PUB_KEY_PATH" ]]; then
239 if [[ $PRINT_ONLY -eq 1 ]]; then
240 log "Would create public key: $PUB_KEY_PATH"
241 PUB_KEY=""
242 else
243 fail "Public key not found: $PUB_KEY_PATH"
244 fi
245else
246 PUB_KEY=$(cat "$PUB_KEY_PATH")
247fi
248
249if [[ $PRINT_ONLY -eq 1 ]]; then
250 log "Would install public key into container user $REMOTE_USER"
251else
252 log "Installing public key into container authorized_keys if needed"
253 docker exec -u "$REMOTE_USER" "$CONTAINER_NAME" sh -lc '
254 set -eu
255 umask 077
256 mkdir -p "$HOME/.ssh"
257 touch "$HOME/.ssh/authorized_keys"
258 chmod 700 "$HOME/.ssh"
259 chmod 600 "$HOME/.ssh/authorized_keys"
260 '
261
262 docker exec -i -u "$REMOTE_USER" "$CONTAINER_NAME" sh -lc '
263 set -eu
264 key=$(cat)
265 auth="$HOME/.ssh/authorized_keys"
266 if ! grep -Fqx "$key" "$auth"; then
267 printf "%s\n" "$key" >> "$auth"
268 echo "added"
269 else
270 echo "present"
271 fi
272 ' <<< "$PUB_KEY" >/tmp/typeagent-devcontainer-ssh-key-status.$$ || fail "Failed to install public key into container"
273
274 KEY_STATUS=$(cat /tmp/typeagent-devcontainer-ssh-key-status.$$)
275 rm -f /tmp/typeagent-devcontainer-ssh-key-status.$$
276 log "Container key status: $KEY_STATUS"
277fi
278
279if [[ $PRINT_ONLY -eq 1 ]]; then
280 log "Would harden container sshd to key-only authentication"
281else
282 log "Hardening container sshd configuration to key-only authentication"
283 docker exec -u root "$CONTAINER_NAME" sh -lc '
284 set -eu
285 install -d -m 755 /etc/ssh/sshd_config.d
286 cat > /etc/ssh/sshd_config.d/99-typeagent-key-only.conf <<"EOF"
287PubkeyAuthentication yes
288PasswordAuthentication no
289KbdInteractiveAuthentication no
290ChallengeResponseAuthentication no
291UsePAM yes
292EOF
293
294 if pgrep sshd >/dev/null 2>&1; then
295 pkill -HUP sshd || true
296 elif command -v service >/dev/null 2>&1; then
297 service ssh restart || service sshd restart || true
298 fi
299 ' || fail "Failed to harden sshd in container"
300 log "Container sshd hardening applied"
301fi
302
303CONFIG_PATH="$DEFAULT_CONFIG_PATH"
304log "Writing SSH config block for $HOST_ALIAS"
305ensure_ssh_config_block \
306 "$CONFIG_PATH" \
307 "$HOST_ALIAS" \
308 "$KEY_PATH" \
309 "$STRICT_HOST_KEY_CHECKING" \
310 "$USER_KNOWN_HOSTS_FILE" \
311 "$GLOBAL_KNOWN_HOSTS_FILE"
312
313if is_wsl; then
314 if ! command -v cmd.exe >/dev/null 2>&1 || ! command -v wslpath >/dev/null 2>&1; then
315 log "Warning: WSL detected but cmd.exe / wslpath unavailable; skipping Windows SSH sync"
316 else
317 WINDOWS_USERPROFILE_WIN=$(cmd.exe /C "echo %USERPROFILE%" 2>/dev/null | tr -d '\r' || true)
318 if [[ -z "$WINDOWS_USERPROFILE_WIN" ]] || [[ "$WINDOWS_USERPROFILE_WIN" == "%USERPROFILE%" ]]; then
319 log "Warning: could not resolve Windows %USERPROFILE%; skipping Windows SSH sync"
320 else
321 WINDOWS_USERPROFILE_WSL=$(wslpath -u "$WINDOWS_USERPROFILE_WIN" 2>/dev/null || true)
322 if [[ -z "$WINDOWS_USERPROFILE_WSL" ]]; then
323 log "Warning: wslpath could not translate %USERPROFILE% ($WINDOWS_USERPROFILE_WIN); skipping Windows SSH sync"
324 else
325 WINDOWS_SSH_DIR_WSL="$WINDOWS_USERPROFILE_WSL/.ssh"
326 WINDOWS_KEY_BASENAME=$(basename "$KEY_PATH")
327 WINDOWS_KEY_PATH_WSL="$WINDOWS_SSH_DIR_WSL/$WINDOWS_KEY_BASENAME"
328 WINDOWS_PUB_KEY_PATH_WSL="$WINDOWS_KEY_PATH_WSL.pub"
329 WINDOWS_CONFIG_PATH_WSL="$WINDOWS_SSH_DIR_WSL/config"
330 WINDOWS_KEY_PATH_CONFIG=$(wslpath -m "$WINDOWS_KEY_PATH_WSL" 2>/dev/null || true)
331 WINDOWS_KNOWN_HOSTS_CONFIG=$(wslpath -m "$WINDOWS_SSH_DIR_WSL/known_hosts" 2>/dev/null || true)
332 [[ -n "$WINDOWS_KEY_PATH_CONFIG" ]] || fail "Failed to convert WSL key path to Windows path"
333 [[ -n "$WINDOWS_KNOWN_HOSTS_CONFIG" ]] || fail "Failed to convert WSL known_hosts path to Windows path"
334
335 if [[ $PRINT_ONLY -eq 1 ]]; then
336 log "Would copy keypair to Windows SSH dir: $WINDOWS_SSH_DIR_WSL"
337 else
338 mkdir -p "$WINDOWS_SSH_DIR_WSL"
339 cp -f "$KEY_PATH" "$WINDOWS_KEY_PATH_WSL"
340 cp -f "$PUB_KEY_PATH" "$WINDOWS_PUB_KEY_PATH_WSL"
341 log "Copied keypair to Windows SSH dir: $WINDOWS_SSH_DIR_WSL"
342 fi
343
344 log "Writing Windows SSH config block for $HOST_ALIAS"
345 ensure_ssh_config_block \
346 "$WINDOWS_CONFIG_PATH_WSL" \
347 "$HOST_ALIAS" \
348 "$WINDOWS_KEY_PATH_CONFIG" \
349 "$STRICT_HOST_KEY_CHECKING" \
350 "$WINDOWS_KNOWN_HOSTS_CONFIG" \
351 "none"
352 fi
353 fi
354 fi
355fi
356
357printf '\nSSH setup complete.\n\n'
358printf 'Container: %s\n' "$CONTAINER_NAME"
359printf 'Host alias: %s\n' "$HOST_ALIAS"
360printf 'Key: %s\n' "$KEY_PATH"
361printf 'Local port: %s\n\n' "$LOCAL_PORT"
362printf 'Connect with:\n'
363printf ' ssh %s\n\n' "$HOST_ALIAS"
364printf 'Or without SSH config:\n'
365printf ' ssh -i %s -p %s %s@localhost\n' "$KEY_PATH" "$LOCAL_PORT" "$REMOTE_USER"
366