Skip to content

Commit 50e4d0f

Browse files
authored
Merge pull request #3741 from mnutt/recover-sandcats
Add recover-sandcats.sh script to regenerate client cert
2 parents 29bb982 + 2c8ac76 commit 50e4d0f

1 file changed

Lines changed: 260 additions & 0 deletions

File tree

recover-sandcats.sh

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
#!/bin/bash
2+
#
3+
# This script recovers access to a sandcats.io subdomain by generating
4+
# new keys and using the email-based recovery flow.
5+
#
6+
# Usage: ./recover-sandcats.sh [sandstorm-dir]
7+
#
8+
# If sandstorm-dir is not provided, defaults to /opt/sandstorm
9+
10+
set -euo pipefail
11+
12+
SANDSTORM_DIR="${1:-/opt/sandstorm}"
13+
SANDCATS_BASE_DOMAIN="${SANDCATS_BASE_DOMAIN:-sandcats.io}"
14+
SANDCATS_API_BASE="${SANDCATS_API_BASE:-https://sandcats.io}"
15+
CURL_USER_AGENT="${CURL_USER_AGENT:-sandstorm-recover-script}"
16+
17+
error() {
18+
if [ $# != 0 ]; then
19+
echo -en '\e[0;31m' >&2
20+
echo "$@" | (fold -s || cat) >&2
21+
echo -en '\e[0m' >&2
22+
fi
23+
}
24+
25+
fail() {
26+
local error_code="$1"
27+
shift
28+
echo "*** RECOVERY FAILED ***" >&2
29+
echo ""
30+
error "$@"
31+
echo "" >&2
32+
exit 1
33+
}
34+
35+
prompt() {
36+
local VALUE
37+
echo -en '\e[1m' >&3
38+
echo -n "$1 [$2]" >&3
39+
echo -en '\e[0m ' >&3
40+
read -u 3 VALUE
41+
if [ -z "$VALUE" ]; then
42+
VALUE=$2
43+
fi
44+
echo "$VALUE"
45+
}
46+
47+
prompt-yesno() {
48+
while true; do
49+
local VALUE=$(prompt "$@")
50+
case $VALUE in
51+
y | Y | yes | YES | Yes )
52+
return 0
53+
;;
54+
n | N | no | NO | No )
55+
return 1
56+
;;
57+
esac
58+
echo "*** Please answer \"yes\" or \"no\"."
59+
done
60+
}
61+
62+
dotdotdot_curl() {
63+
echo -n '...' >&2
64+
curl "$@"
65+
echo -ne '\r' >&2
66+
}
67+
68+
sandcats_generate_keys() {
69+
local SANDCATS_DIR="$SANDSTORM_DIR/var/sandcats"
70+
71+
if [ -f "$SANDCATS_DIR/id_rsa.private_combined" ]; then
72+
echo "Existing keys found in $SANDCATS_DIR"
73+
if ! prompt-yesno "Generate new keys? (required for recovery)" "yes"; then
74+
fail "E_NEED_NEW_KEYS" "Recovery requires generating new keys."
75+
fi
76+
# Back up existing keys
77+
local BACKUP_DIR="$SANDCATS_DIR/backup.$(date +%Y%m%d-%H%M%S)"
78+
mkdir -p "$BACKUP_DIR"
79+
mv "$SANDCATS_DIR/id_rsa" "$BACKUP_DIR/" 2>/dev/null || true
80+
mv "$SANDCATS_DIR/id_rsa.pub" "$BACKUP_DIR/" 2>/dev/null || true
81+
mv "$SANDCATS_DIR/id_rsa.private_combined" "$BACKUP_DIR/" 2>/dev/null || true
82+
echo "Backed up existing keys to $BACKUP_DIR"
83+
fi
84+
85+
echo -n 'Generating new keys...'
86+
87+
mkdir -p -m 0700 "$SANDCATS_DIR"
88+
chmod 0700 "$SANDCATS_DIR"
89+
90+
openssl \
91+
req \
92+
-new \
93+
-newkey rsa:4096 \
94+
-days 3650 \
95+
-nodes \
96+
-x509 \
97+
-subj "/C=AU/ST=Some-State/O=Internet Widgits Pty Ltd" \
98+
-keyout "$SANDCATS_DIR/id_rsa" \
99+
-out "$SANDCATS_DIR/id_rsa.pub" \
100+
2>/dev/null
101+
102+
cat "$SANDCATS_DIR/id_rsa" "$SANDCATS_DIR/id_rsa.pub" > "$SANDCATS_DIR/id_rsa.private_combined"
103+
104+
chmod 0640 "$SANDCATS_DIR/id_rsa" "$SANDCATS_DIR/id_rsa.pub" "$SANDCATS_DIR/id_rsa.private_combined"
105+
106+
echo " done."
107+
}
108+
109+
sandcats_recover_domain() {
110+
local SANDCATS_DIR="$SANDSTORM_DIR/var/sandcats"
111+
112+
DESIRED_SANDCATS_NAME=$(prompt "What Sandcats subdomain do you want to recover?" "")
113+
114+
if [ -z "$DESIRED_SANDCATS_NAME" ]; then
115+
fail "E_NO_SUBDOMAIN" "You must provide a subdomain to recover."
116+
fi
117+
118+
# Strip the base domain if provided
119+
if [[ $DESIRED_SANDCATS_NAME =~ [.] ]]; then
120+
echo ""
121+
echo "You entered: $DESIRED_SANDCATS_NAME"
122+
echo ""
123+
echo "Please enter just the subdomain part, without any dots."
124+
echo "For example, if your domain is 'myserver.sandcats.io', just enter 'myserver'."
125+
echo ""
126+
sandcats_recover_domain
127+
return
128+
fi
129+
130+
echo "OK. We will send a recovery token to the email address on file."
131+
if ! prompt-yesno "OK to continue?" "yes"; then
132+
echo "Aborted."
133+
exit 0
134+
fi
135+
136+
# Send recovery token request
137+
local LOG_PATH="$SANDCATS_DIR/sendrecoverytoken-log"
138+
mkdir -p "$SANDCATS_DIR"
139+
touch "$LOG_PATH"
140+
chmod 0640 "$LOG_PATH"
141+
142+
echo "Requesting recovery token..."
143+
HTTP_STATUS=$(
144+
dotdotdot_curl \
145+
--silent \
146+
--max-time 20 \
147+
-A "$CURL_USER_AGENT" \
148+
-X POST \
149+
--data-urlencode "rawHostname=$DESIRED_SANDCATS_NAME" \
150+
--output "$LOG_PATH" \
151+
-w '%{http_code}' \
152+
-H 'X-Sand: cats' \
153+
-H "Accept: text/plain" \
154+
"${SANDCATS_API_BASE}/sendrecoverytoken")
155+
156+
if [ "200" != "$HTTP_STATUS" ]; then
157+
error "$(cat "$LOG_PATH")"
158+
fail "E_SEND_TOKEN_FAILED" "Failed to request recovery token."
159+
fi
160+
161+
cat "$LOG_PATH"
162+
echo ''
163+
164+
TOKEN=$(prompt "Please enter the token that we sent to you by email." '')
165+
166+
if [ -z "$TOKEN" ]; then
167+
fail "E_EMPTY_TOKEN" "Empty tokens are not valid."
168+
fi
169+
170+
# Submit recovery token
171+
LOG_PATH="$SANDCATS_DIR/recover-log"
172+
touch "$LOG_PATH"
173+
chmod 0640 "$LOG_PATH"
174+
175+
echo "Submitting recovery token..."
176+
HTTP_STATUS=$(
177+
dotdotdot_curl \
178+
--silent \
179+
--max-time 20 \
180+
-A "$CURL_USER_AGENT" \
181+
-X POST \
182+
--data-urlencode "rawHostname=$DESIRED_SANDCATS_NAME" \
183+
--data-urlencode "recoveryToken=$TOKEN" \
184+
--output "$LOG_PATH" \
185+
-w '%{http_code}' \
186+
-H 'X-Sand: cats' \
187+
-H "Accept: text/plain" \
188+
--cert "$SANDCATS_DIR/id_rsa.private_combined" \
189+
"${SANDCATS_API_BASE}/recover")
190+
191+
if [ "200" != "$HTTP_STATUS" ]; then
192+
error "$(cat "$LOG_PATH")"
193+
fail "E_RECOVER_FAILED" "Failed to recover domain."
194+
fi
195+
196+
cat "$LOG_PATH"
197+
echo ''
198+
199+
# Update DNS
200+
LOG_PATH="$SANDCATS_DIR/update-log"
201+
touch "$LOG_PATH"
202+
chmod 0640 "$LOG_PATH"
203+
204+
echo "Updating DNS..."
205+
HTTP_STATUS=$(
206+
dotdotdot_curl \
207+
--silent \
208+
--max-time 20 \
209+
-A "$CURL_USER_AGENT" \
210+
-X POST \
211+
--data-urlencode "rawHostname=$DESIRED_SANDCATS_NAME" \
212+
--output "$LOG_PATH" \
213+
-w '%{http_code}' \
214+
-H 'X-Sand: cats' \
215+
-H "Accept: text/plain" \
216+
--cert "$SANDCATS_DIR/id_rsa.private_combined" \
217+
"${SANDCATS_API_BASE}/update")
218+
219+
if [ "200" != "$HTTP_STATUS" ]; then
220+
error "$(cat "$LOG_PATH")"
221+
fail "E_UPDATE_FAILED" "Failed to update DNS."
222+
fi
223+
224+
cat "$LOG_PATH"
225+
echo ''
226+
227+
echo ""
228+
echo "Congratulations! You have recovered ${DESIRED_SANDCATS_NAME}.${SANDCATS_BASE_DOMAIN}."
229+
echo "Your credentials are in $SANDCATS_DIR; consider making a backup."
230+
echo ""
231+
echo "You may need to restart Sandstorm and/or renew your TLS certificate:"
232+
echo " sudo sandstorm restart"
233+
echo " sudo sandstorm renew-certificate"
234+
}
235+
236+
main() {
237+
if [ ! -t 1 ]; then
238+
fail "E_NO_TTY" "This script is interactive. Please run it on a terminal."
239+
fi
240+
241+
# Set up FD 3 for interactive input
242+
exec 3<&1
243+
244+
if [ ! -d "$SANDSTORM_DIR" ]; then
245+
fail "E_NO_DIR" "Sandstorm directory not found: $SANDSTORM_DIR"
246+
fi
247+
248+
echo "Sandcats.io Domain Recovery"
249+
echo "============================"
250+
echo ""
251+
echo "This script will help you recover access to a sandcats.io subdomain"
252+
echo "that you previously registered. You will need access to the email"
253+
echo "address you used when you registered the domain."
254+
echo ""
255+
256+
sandcats_generate_keys
257+
sandcats_recover_domain
258+
}
259+
260+
main "$@"

0 commit comments

Comments
 (0)