-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaction.yml
More file actions
244 lines (224 loc) · 9.02 KB
/
Copy pathaction.yml
File metadata and controls
244 lines (224 loc) · 9.02 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
name: 'khm check-fingerprint'
description: >-
Verify an SSH host identity against an explicit fingerprint. Prove you are talking to the
right machine before deploying.
author: casablanque-code
branding:
icon: shield
color: green
inputs:
host:
description: 'Hostname or IP to check.'
required: true
port:
description: 'SSH port.'
required: false
default: '22'
fingerprint:
description: >-
Expected SHA256 host key fingerprint, with or without the
"SHA256:" prefix — paste it exactly as your secret manager or
provisioning tool prints it.
required: true
khm-version:
description: >-
khm release tag to install (e.g. 'v0.5.1'), pinning both the
binary and the checksum it's verified against. Leave empty to
always install whatever the "latest" GitHub release currently
is — simplest, but the exact binary a given commit runs against
can then change between one workflow run and the next. Pin this
for reproducible CI.
required: false
default: ''
outputs:
result:
description: "'match', 'mismatch', or 'error'."
value: ${{ steps.check.outputs.result }}
observed-fingerprint:
description: 'The fingerprint khm actually observed. Empty if result is error.'
value: ${{ steps.check.outputs.observed-fingerprint }}
algorithm:
description: 'Key algorithm khm observed (e.g. ssh-ed25519). Empty if result is error.'
value: ${{ steps.check.outputs.algorithm }}
runs:
using: composite
steps:
- name: Validate inputs
shell: bash
env:
HOST: ${{ inputs.host }}
FINGERPRINT: ${{ inputs.fingerprint }}
PORT: ${{ inputs.port }}
run: |
set -euo pipefail
[ -n "$HOST" ] || { echo "::error::host is required"; exit 1; }
[ -n "$FINGERPRINT" ] || { echo "::error::fingerprint is required"; exit 1; }
case "$PORT" in
''|*[!0-9]*) echo "::error::port must be a number, got '$PORT'"; exit 1 ;;
esac
command -v jq >/dev/null 2>&1 || {
echo "::error::jq is required (not found on PATH) — install it or use a runner image that includes it"
exit 1
}
command -v sha256sum >/dev/null 2>&1 || {
echo "::error::sha256sum is required (not found on PATH)"
exit 1
}
- name: Resolve khm version
id: resolve-version
shell: bash
env:
KHM_VERSION: ${{ inputs.khm-version }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if [ -n "$KHM_VERSION" ]; then
echo "version=${KHM_VERSION}" >> "$GITHUB_OUTPUT"
else
echo "::warning::khm-version not set — installing whatever 'latest' currently is. Pin khm-version for reproducible runs."
latest="$(curl -fsSL -H "Authorization: Bearer ${GH_TOKEN}" \
https://api.github.com/repos/casablanque-code/khm/releases/latest | jq -r .tag_name)"
[ -n "$latest" ] && [ "$latest" != "null" ] || {
echo "::error::could not resolve latest khm release"
exit 1
}
echo "version=${latest}" >> "$GITHUB_OUTPUT"
fi
- name: Install khm
shell: bash
env:
VERSION: ${{ steps.resolve-version.outputs.version }}
run: |
set -euo pipefail
arch="$(uname -m)"
if [ "$arch" != "x86_64" ]; then
echo "::error::khm releases only publish a linux-amd64 binary; runner arch is '$arch'"
exit 1
fi
base="https://github.com/casablanque-code/khm/releases/download/${VERSION}"
curl -fsSL "${base}/khm-linux-amd64" -o /tmp/khm
curl -fsSL "${base}/khm-linux-amd64.sha256" -o /tmp/khm-linux-amd64.sha256
# The checksum file records the filename "khm-linux-amd64" (it was
# generated as `sha256sum khm-linux-amd64 > khm-linux-amd64.sha256`
# in the release workflow) — sha256sum -c matches against that
# literal name, so verify from a directory where the downloaded
# binary is named to match, rather than rewriting the checksum file.
mv /tmp/khm /tmp/khm-linux-amd64
(cd /tmp && sha256sum -c khm-linux-amd64.sha256) || {
echo "::error::checksum verification failed for khm ${VERSION} — downloaded binary does not match the published sha256"
exit 1
}
chmod +x /tmp/khm-linux-amd64
sudo mv /tmp/khm-linux-amd64 /usr/local/bin/khm
khm --version
- name: Run check-fingerprint
id: check
shell: bash
env:
HOST: ${{ inputs.host }}
PORT: ${{ inputs.port }}
FINGERPRINT: ${{ inputs.fingerprint }}
run: |
# GitHub always launches composite bash steps with `-e` already
# active at the invocation level (`bash --noprofile --norc -e
# -o pipefail {0}`), regardless of what this script itself
# `set`s -- `set -uo pipefail` does not turn a pre-existing `-e`
# back off. Left as-is, `output="$(khm ...)"` failing (mismatch
# exit 1, error exit 2) kills the script on that line, before
# result/observed/algorithm are ever computed or written to
# GITHUB_OUTPUT -- exactly what happened in the first real run:
# outputs.result came back empty in every branch, match
# included, because that line's failure was silently fatal.
# `set +e` around just the risky call, then back to strict mode
# once exit_code is safely captured, fixes it.
set +e
output="$(khm check-fingerprint "${HOST}:${PORT}" --expect "$FINGERPRINT" --json)"
exit_code=$?
set -euo pipefail
# exit_code is authoritative for pass/fail (see the final step) —
# khm's own 0/1/2 contract, not re-derived from the JSON. The JSON
# is only parsed here for the human-readable outputs/summary.
result="$(jq -r '.result' <<<"$output" 2>/dev/null || echo error)"
observed="$(jq -r '.observed_fingerprint // empty' <<<"$output" 2>/dev/null || true)"
algorithm="$(jq -r '.algorithm // empty' <<<"$output" 2>/dev/null || true)"
error_reason="$(jq -r '.error // empty' <<<"$output" 2>/dev/null || true)"
expected="$(jq -r '.expected_fingerprint // empty' <<<"$output" 2>/dev/null || true)"
{
echo "result=${result}"
echo "observed-fingerprint=${observed}"
echo "algorithm=${algorithm}"
} >> "$GITHUB_OUTPUT"
{
echo "KHM_RESULT=${result}"
echo "KHM_ERROR_REASON=${error_reason}"
echo "KHM_EXPECTED=${expected}"
echo "KHM_OBSERVED=${observed}"
echo "KHM_ALGORITHM=${algorithm}"
} >> "$GITHUB_ENV"
exit "$exit_code"
continue-on-error: true
- name: Write summary
if: always() && steps.check.outcome != 'skipped'
shell: bash
env:
HOST: ${{ inputs.host }}
PORT: ${{ inputs.port }}
run: |
set -euo pipefail
{
echo "## SSH Host Verification"
echo
echo "**Host:** ${HOST}"
echo "**Port:** ${PORT}"
[ -n "${KHM_ALGORITHM:-}" ] && echo "**Algorithm:** ${KHM_ALGORITHM}"
echo
} >> "$GITHUB_STEP_SUMMARY"
case "${KHM_RESULT:-error}" in
match)
{
echo "| | |"
echo "|---|---|"
echo "| Expected | \`${KHM_EXPECTED:-}\` |"
echo "| Observed | \`${KHM_OBSERVED:-}\` |"
echo "| Result | ✅ MATCH |"
echo
echo "> Before deploying, prove you're talking to the right machine."
} >> "$GITHUB_STEP_SUMMARY"
;;
mismatch)
{
echo "❌ **HOST IDENTITY CHANGED**"
echo
echo "| | |"
echo "|---|---|"
echo "| Expected | \`${KHM_EXPECTED:-}\` |"
echo "| Observed | \`${KHM_OBSERVED:-}\` |"
echo "| Result | ❌ MISMATCH |"
echo
echo "Deployment should not continue."
} >> "$GITHUB_STEP_SUMMARY"
;;
*)
{
echo "⚠️ **COULD NOT VERIFY**"
echo
echo "| | |"
echo "|---|---|"
echo "| Expected | \`${KHM_EXPECTED:-}\` |"
echo "| Reason | \`${KHM_ERROR_REASON:-unknown}\` |"
echo
echo "khm could not obtain a live fingerprint from this host (timeout, connection"
echo "refused, DNS failure, or the host isn't speaking SSH). This is an"
echo "infrastructure failure, not a confirmed identity mismatch."
} >> "$GITHUB_STEP_SUMMARY"
;;
esac
- name: Fail on non-match
if: always() && steps.check.outcome != 'skipped' && steps.check.outputs.result != 'match'
shell: bash
env:
RESULT: ${{ steps.check.outputs.result }}
run: |
set -euo pipefail
echo "::error::khm check-fingerprint did not match (result: ${RESULT:-error}) — see the job summary for details"
exit 1