-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathalias
More file actions
423 lines (391 loc) · 17.3 KB
/
Copy pathalias
File metadata and controls
423 lines (391 loc) · 17.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
[toplevel]
whoami =
!f() {
echo "Profile: ${AWS_PROFILE:-default}"
aws sts get-caller-identity --output=yaml
}; f
id = sts get-caller-identity --query "Account" --output text
# ============================================================================
# op-credentials — 1Password -> AWS credential_process bridge
# ============================================================================
#
# Usage:
# aws op-credentials <base-item> [--mfa <mfa-item>] [--vault <vault>]
#
# Emits credential_process JSON:
# {Version:1, AccessKeyId, SecretAccessKey[, SessionToken, Expiration],
# ProviderType} ("op" for static keys, "opmfa" for MFA sessions)
#
# Wire into ~/.aws/config:
# [profile foo]
# credential_process = aws op-credentials "Foo AWS CLI" --mfa "Foo AWS"
#
# NOTE: Python's configparser (which parses this alias file) strips any `#`
# preceded by whitespace as an inline comment. Comments inside the alias body
# are therefore blank lines at parse time — they exist for source readers only.
# Same reason case/esac and ` ; ` between commands are avoided in the body.
# ============================================================================
op-credentials =
!f() {
# --- arg parsing ---
# Unknown --flags are silently tolerated so callers can be updated to
# pass future flags without breaking older versions of this alias.
local base_item="" mfa_item="" vault="Private"
while [ $# -gt 0 ]; do
if [ "$1" = "--mfa" ]; then
mfa_item="$2"
shift
elif [ "$1" = "--vault" ]; then
vault="$2"
shift
elif [ "${1#--}" != "$1" ]; then
:
elif [ -z "$base_item" ]; then
base_item="$1"
fi
shift
done
if [ -z "$base_item" ]; then
echo "usage: aws op-credentials <base-item> [--mfa <mfa-item>] [--vault <vault>]" >&2
return 2
fi
# --- platform detection ---
# Captured once here and read via dynamic scope by the helpers below
# so we avoid forking /bin/uname on every stat or date call. POSIX
# doesn't give us arrays or global-scope alternatives that are portable
# across dash/ash/bash/zsh, so dynamic scope is the right tool.
local uname_s
uname_s=$(uname)
# --- cache directory selection ---
# credential_process is called by every AWS SDK client that hits a
# config-file profile — IDE extensions, LSPs, Terraform, kubectl token
# plugins — potentially many in parallel. Without caching, each caller
# spawns its own op + STS round-trip. With a shared ephemeral cache the
# hot path is a single `cat`.
#
# Probe order targets the fastest ephemeral per-user location available:
# XDG_RUNTIME_DIR Linux systemd-logind tmpfs, already per-user
# /dev/shm universal Linux tmpfs, world-writable so UID-scoped
# TMPDIR macOS launchd per-user /var/folders path
# /tmp universal fallback — disk, but always present
#
# The 0700 on the dir and 0600 on each file are defense-in-depth for
# the /dev/shm case where the parent is world-writable.
#
# TODO: --cache-dir <path> flag to pin the cache to ~/.aws/cli/cache
# so MFA sessions survive container rebuilds without re-authing.
# --persist-cache as a shorthand for that common case.
local cache_base="$XDG_RUNTIME_DIR"
if [ -z "$cache_base" ] || [ ! -w "$cache_base" ]; then
if [ -d /dev/shm ] && [ -w /dev/shm ]; then
cache_base=/dev/shm
elif [ -n "$TMPDIR" ] && [ -w "$TMPDIR" ]; then
cache_base="$TMPDIR"
else
cache_base=/tmp
fi
fi
local cache_dir="$cache_base/op-credentials-$(id -u)"
mkdir -p "$cache_dir" || return $?
chmod 700 "$cache_dir" 2>/dev/null || true
# --- cache key derivation ---
# OP item names are arbitrary strings; slugify to a valid filename.
# Static and MFA caches are keyed separately so they can coexist and
# age out independently — switching --mfa on/off doesn't bust the other.
local base_slug mfa_slug cache_name
base_slug=$(printf '%s' "$base_item" | tr -c 'A-Za-z0-9._-' '_')
cache_name="$base_slug"
if [ -n "$mfa_item" ]; then
mfa_slug=$(printf '%s' "$mfa_item" | tr -c 'A-Za-z0-9._-' '_')
cache_name="${cache_name}.mfa-${mfa_slug}"
fi
local cache_file="$cache_dir/${cache_name}.json"
local lock_file="$cache_dir/${cache_name}.lock"
# --- cross-platform helpers ---
# All three helpers are POSIX-only (no arrays, no [[ ]], no bashisms)
# and read $uname_s from the enclosing scope rather than calling uname
# themselves. `local` is the one non-POSIX construct used — it's
# supported by dash, ash, bash, and zsh, making it safe enough.
_parse_exp() {
if [ "$uname_s" = "Darwin" ]; then
local t="${1%+*}"
t="${t%Z}"
date -u -jf '%Y-%m-%dT%H:%M:%S' "$t" +%s 2>/dev/null
else
date -d "$1" +%s 2>/dev/null
fi
}
_file_mtime() {
if [ "$uname_s" = "Darwin" ]; then
stat -f %m "$1" 2>/dev/null
else
stat -c %Y "$1" 2>/dev/null
fi
}
# Freshness rules differ by credential type:
# MFA sessions — STS sets an explicit Expiration; we subtract a
# 5-min buffer so callers never receive an about-to-
# expire token mid-request.
# Static keys — no Expiration field, so we fall back to file mtime
# with a 10h TTL (covers a workday; key rotations land
# within that window on the next cold-cache call).
# Cache contents are the raw credential_process JSON so the hot path
# is a single `cat` with no jq reshaping.
#
# TODO: --ttl <seconds> to override the 10h static TTL.
# --no-cache to force a refresh on a single call (e.g. after
# an emergency key rotation).
_emit_if_fresh() {
[ -f "$cache_file" ] || return 1
local exp now epoch mtime age
now=$(date +%s)
exp=$(jq -r '.Expiration // empty' "$cache_file" 2>/dev/null)
if [ -n "$exp" ]; then
epoch=$(_parse_exp "$exp")
[ -n "$epoch" ] || return 1
[ $((epoch - now)) -gt 300 ] || return 1
else
mtime=$(_file_mtime "$cache_file")
[ -n "$mtime" ] || return 1
age=$((now - mtime))
[ "$age" -lt 36000 ] || return 1
fi
cat "$cache_file"
}
# --- hot path: cache hit ---
# On a warm cache this is the only I/O — one `cat`. Returning here
# avoids the subshell + flock overhead entirely.
_emit_if_fresh && return 0
# --- slow path: fetch and cache (subshell) ---
# Runs inside a subshell so the flock file descriptor is automatically
# released when the block exits without needing explicit cleanup.
#
# The double-checked locking pattern (check outside lock -> acquire lock
# -> check again inside) ensures that with N concurrent cold-cache callers
# only one actually talks to 1Password + STS; the others block on flock,
# then read the freshly-written cache. One OTP consumed, not N.
#
# flock is util-linux — present on all Linux distros, absent on stock
# macOS. Without it we proceed unlocked; concurrent callers may each do
# their own op/STS round-trip, which wastes work but doesn't cause
# recursion or correctness problems.
#
# TODO: mkdir-based POSIX lock as a fallback to close the macOS race.
(
if command -v flock >/dev/null 2>&1; then
exec 9>"$lock_file"
flock -x 9
fi
_emit_if_fresh && exit 0
# --- fetch static credentials from 1Password ---
local base_json ak sk
base_json=$(op --vault="$vault" item get "$base_item" --format=json \
--fields='label=AccessKeyId,label=SecretAccessKey') || exit $?
ak=$(printf '%s' "$base_json" | jq -r '.[] | select(.label=="AccessKeyId") | .value')
sk=$(printf '%s' "$base_json" | jq -r '.[] | select(.label=="SecretAccessKey") | .value')
if [ -z "$ak" ] || [ -z "$sk" ] || [ "$ak" = "null" ] || [ "$sk" = "null" ]; then
echo "op-credentials: could not extract AccessKeyId/SecretAccessKey from '$base_item'" >&2
exit 1
fi
local creds_json
if [ -z "$mfa_item" ]; then
# --- static-only path ---
# No MFA needed; the IAM key is the credential. ProviderType "op"
# lets callers distinguish a raw key from an STS session token.
creds_json=$(jq -n --arg ak "$ak" --arg sk "$sk" \
'{Version:1, AccessKeyId:$ak, SecretAccessKey:$sk, ProviderType:"op"}')
else
# --- MFA path: OP key -> IAM serial -> STS session ---
# AWS_PROFILE and AWS_CONFIG_FILE are unset for the inner `aws`
# calls so boto3 resolves credentials via EnvProvider (the static
# key we just fetched from OP) rather than re-entering
# credential_process. Without this, each concurrent SDK caller
# would recurse back into this alias, fanning out to ~5-6 `aws`
# Python processes per caller — enough to OOM a 12 GB devcontainer
# under normal IDE + LSP + Terraform + kubectl load.
local mfa_serial otp sts_json
mfa_serial=$(env -u AWS_PROFILE -u AWS_CONFIG_FILE \
AWS_ACCESS_KEY_ID="$ak" AWS_SECRET_ACCESS_KEY="$sk" \
aws iam list-mfa-devices \
--query 'MFADevices[0].SerialNumber' \
--output text) || exit $?
if [ -z "$mfa_serial" ] || [ "$mfa_serial" = "None" ]; then
echo "op-credentials: no MFA device found for IAM user tied to '$base_item'" >&2
exit 1
fi
otp=$(op item get "$mfa_item" --otp --vault="$vault") || exit $?
sts_json=$(env -u AWS_PROFILE -u AWS_CONFIG_FILE \
AWS_ACCESS_KEY_ID="$ak" AWS_SECRET_ACCESS_KEY="$sk" \
aws sts get-session-token \
--serial-number "$mfa_serial" \
--token-code "$otp") || exit $?
creds_json=$(printf '%s' "$sts_json" | jq '.Credentials
| {Version:1, AccessKeyId, SecretAccessKey, SessionToken, Expiration, ProviderType:"opmfa"}')
fi
# --- atomic cache write ---
# Write to a temp file and mv into place so any concurrent reader
# sees either the previous complete file or the new complete file —
# never a partially-written one. chmod 600 before mv as a final
# defense-in-depth step in case the parent dir is shared.
local tmp_file
tmp_file=$(mktemp "${cache_file}.XXXXXX") || exit $?
chmod 600 "$tmp_file" 2>/dev/null || true
if ! echo "$creds_json" > "$tmp_file"; then
rm -f "$tmp_file"
exit 1
fi
mv "$tmp_file" "$cache_file" || exit $?
cat "$cache_file"
)
}; f
jit =
!f() {
duploctl jit web $@
}; f
console =
!f() {
local ACCOUNT_ID="$(aws id)"
local url="https://${ACCOUNT_ID}.signin.aws.amazon.com/console"
open "$url"
}; f
temp-session =
!f() {
local creds="$(aws sts temp)"
cat <<EOF
export AWS_ACCESS_KEY_ID="$(echo $creds | jq -r '.Credentials.AccessKeyId')"
export AWS_SECRET_ACCESS_KEY="$(echo $creds | jq -r '.Credentials.SecretAccessKey')"
export AWS_SESSION_TOKEN="$(echo $creds | jq -r '.Credentials.SessionToken')"
EOF
}; f
gcl-session =
!f() {
local creds="$(aws sts temp)"
cat <<EOF > .gitlab-ci-local-variables.yml
AWS_ACCESS_KEY_ID: "$(echo $creds | jq -r '.Credentials.AccessKeyId')"
AWS_SECRET_ACCESS_KEY: "$(echo $creds | jq -r '.Credentials.SecretAccessKey')"
AWS_SESSION_TOKEN: "$(echo $creds | jq -r '.Credentials.SessionToken')"
AWS_ACCOUNT_ID: "$(aws id)"
AWS_DEFAULT_REGION: $AWS_DEFAULT_REGION
EOF
}; f
act-session =
!f() {
local creds="$(aws sts temp)"
local act_args=()
act_args+=(--env AWS_ACCESS_KEY_ID="$(echo $creds | jq -r '.Credentials.AccessKeyId')")
act_args+=(--env AWS_SECRET_ACCESS_KEY="$(echo $creds | jq -r '.Credentials.SecretAccessKey')")
act_args+=(--env AWS_SESSION_TOKEN="$(echo $creds | jq -r '.Credentials.SessionToken')")
act_args+=(--env AWS_ACCOUNT_ID="$(aws id)")
act_args+=(--env AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION")
act_args+=(--env AWS_REGION="$AWS_DEFAULT_REGION")
echo "${act_args[*]}"
}; f
[command sts]
temp =
!f() {
aws sts assume-role \
--role-arn arn:aws:iam::$(aws id):role/duplomaster \
--role-session-name "AdminSession"
}; f
[command ssm]
select =
!f() {
local data=$(aws ssm describe-instance-information \
--filters "Key=PingStatus,Values=Online" \
--query "InstanceInformationList[*].[InstanceId, IPAddress, ComputerName, PlatformName]" \
--output text)
local selected=$(printf 'INSTANCE_ID\tIP\tNAME\tPLATFORM\n%s' "$data" | column -t | fzf --tac --header="Select a managed instance")
set -- $selected
echo "$1"
}; f
proxy =
!f() {
# Arguments
USER=$1
HOSTNAME=$2
PORT=$3
# Prepare temporary SSH key
ONE_TIME_KEY_FILE_NAME="$(mktemp /tmp/${HOSTNAME}.${USER}.XXXXXX)"
yes | ssh-keygen -t rsa -b 4096 -f ${ONE_TIME_KEY_FILE_NAME} -N ''
ssh-add -t 60 ${ONE_TIME_KEY_FILE_NAME}
# Send the SSH key to the target instance
AZ="$(aws ec2 describe-instances \
--instance-ids ${HOSTNAME} \
--output=text \
--query 'Reservations[0].Instances[0].Placement.AvailabilityZone')"
aws ec2-instance-connect send-ssh-public-key \
--instance-id ${HOSTNAME} \
--availability-zone $AZ \
--instance-os-user "${USER}" \
--ssh-public-key "file://${ONE_TIME_KEY_FILE_NAME}.pub"
# Start the SSM session
aws ssm start-session \
--target ${HOSTNAME} \
--document-name AWS-StartSSHSession \
--parameters "portNumber=${PORT}" \
--region $AWS_DEFAULT_REGION
}; f
[command ec2]
select =
!f() {
local selected=$(aws ec2 describe-instances \
--query "Reservations[*].Instances[*].{name: Tags[?Key=='Name'] | [0].Value, instance_id: InstanceId, ip_address: PrivateIpAddress, state: State.Name}" \
--output table | fzf --header="Select an EC2 instance" --tac)
set -- $(echo "$selected" | tr '|' ' ')
echo "$1"
}; f
exec =
!f() {
local user="${1:-ec2-user}"
local ec2id=`aws ssm select`
ssh "$user@$ec2id"
}; f
[command eks]
add-config =
!f() {
local cluster=`aws eks list-clusters --query "clusters[*]" | jq -r ".[]" | fzf`
aws eks update-kubeconfig --name "$cluster"
}; f
[command iam]
mymfa = list-mfa-devices --query 'MFADevices[0].SerialNumber' --output text
[command ecr]
login =
!f() {
local REGISTRY ACCOUNT_ID
ACCOUNT_ID="$(aws id)"
REGISTRY="${ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com"
aws ecr get-login-password | docker login --username AWS --password-stdin $REGISTRY
}; f
[command rds]
select =
!f() {
local selected=$(aws rds describe-db-clusters \
--output table \
--query "DBClusters[*].{name: TagList[?Key=='Name'] | [0].Value, id: DBClusterIdentifier, tenant: TagList[?Key=='TENANT_NAME'] | [0].Value}" | fzf --header="Select an RDS cluster" --tac)
set -- $(echo "$selected" | tr '|' ' ')
echo "$1"
}; f
port-forward =
!f() {
local target_instance target_cluster endpoint
target_instance=$(aws ssm select)
target_cluster=$(aws rds select)
ports=${1:-5432:5432}
local local_port=${ports%%:*}
local remote_port=${ports##*:}
rds_endpoint=$(aws rds describe-db-cluster-endpoints \
--output text \
--db-cluster-identifier "${target_cluster}" \
--query "DBClusterEndpoints[?EndpointType=='WRITER'] | [0].Endpoint")
echo """
ec2 instance: ${target_instance}
rds cluster: ${target_cluster}
rds endpoint: ${rds_endpoint}
local port: ${local_port}
remote port: ${remote_port}
"""
aws ssm start-session \
--target "${target_instance}" \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters host="${rds_endpoint}",portNumber="${remote_port}",localPortNumber="${local_port}"
}; f