-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathentrypoint.sh
More file actions
308 lines (283 loc) · 11.7 KB
/
entrypoint.sh
File metadata and controls
308 lines (283 loc) · 11.7 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
#!/bin/sh
# ── Signal handling ──────────────────────────────────────────────────────────
# When docker stop sends SIGTERM, clean up background processes and stop sshd.
TAIL_PID=""
SYNC_PID=""
SSHD_PID=""
cleanup() {
printf '{"version":"1.1","host":"%s","timestamp":%s,"level":6,"short_message":"shutting down","_mode":"system","_event":"shutdown"}\n' \
"${HOSTNAME:-goBastion}" "$(date +%s)"
[ -n "$TAIL_PID" ] && kill "$TAIL_PID" 2>/dev/null
# Kill sync process group to catch orphaned children
if [ -n "$SYNC_PID" ]; then
kill -- -"$SYNC_PID" 2>/dev/null || kill "$SYNC_PID" 2>/dev/null
fi
[ -n "$SSHD_PID" ] && kill "$SSHD_PID" 2>/dev/null
wait "$SSHD_PID" 2>/dev/null
exit 0
}
trap cleanup TERM INT
# ── Config ───────────────────────────────────────────────────────────────────
# Write DB config to a file so ForceCommand sessions can read it.
# sshd strips Docker env vars from child processes; SetEnv doesn't support
# values with spaces (DB_DSN), so we use a config file instead.
# goBastion reads /run/gobastion/db.conf as fallback when env vars are absent.
mkdir -p /run/gobastion
{
[ -n "$DB_DRIVER" ] && printf 'DB_DRIVER=%s\n' "$DB_DRIVER"
[ -n "$DB_DSN" ] && printf 'DB_DSN=%s\n' "$DB_DSN"
[ -n "$EGRESS_ENC_KEY" ] && printf 'EGRESS_ENC_KEY=%s\n' "$EGRESS_ENC_KEY"
} > /run/gobastion/db.conf
chmod 600 /run/gobastion/db.conf
# ── Log tail ─────────────────────────────────────────────────────────────────
# Tail GELF app logs immediately so docker logs captures startup events
# (DB connect, migrations, no-admin warnings) before sshd even starts.
touch /goBastion.log
tail -f /goBastion.log &
TAIL_PID=$!
# ── First install ────────────────────────────────────────────────────────────
# Auto startup: syncs DB state to OS if present.
# Exits 1 if no admin exists (no TTY) — waits until one is created via --firstInstall.
until /app/goBastion; do
printf '{"version":"1.1","host":"%s","timestamp":%s,"level":4,"short_message":"Waiting for first admin. Run: docker exec -it %s /app/goBastion --firstInstall","_mode":"system","_event":"first_install_required"}\n' \
"${HOSTNAME:-goBastion}" "$(date +%s)" "${HOSTNAME:-goBastion}"
sleep 5
done
# ── Periodic sync ────────────────────────────────────────────────────────────
# Enforce DB as source of truth every 5 minutes.
# Logs drift (rogue users, key changes) and corrects it automatically.
(while true; do
sleep 300
/app/goBastion --sync
done) &
SYNC_PID=$!
# ── sshd ─────────────────────────────────────────────────────────────────────
printf '{"version":"1.1","host":"%s","timestamp":%s,"level":6,"short_message":"starting sshd","_mode":"system","_event":"sshd_start"}\n' \
"${HOSTNAME:-goBastion}" "$(date +%s)"
# -e sends sshd logs to stderr instead of syslog; 2>&1 forwards them to docker logs.
/usr/sbin/sshd -D -e 2>&1 \
| awk '
function esc(s, t){ t=s; gsub(/\\/,"\\\\",t); gsub(/"/,"\\\"",t); gsub(/\r/,"",t); return t }
function emit(msg, host, lvl, ev, from, port, user, to, fp, now, out, ts, Y,M,D,h,m,s,d,z,era,doe,yoe,doy,mp,t){
now = systime()
if (now == _last_ts) { _seq++ } else { _last_ts = now; _seq = 0 }
ts = now + _seq/10000
# Compute ISO 8601 time from unix timestamp (no strftime in busybox awk)
s = now % 60; t = int(now / 60)
m = t % 60; t = int(t / 60)
h = t % 24; d = int(t / 24)
z = d + 719468; era = int(z / 146097)
doe = z - era * 146097
yoe = int((doe - int(doe/1460) + int(doe/36524) - int(doe/146096)) / 365)
Y = yoe + era * 400
doy = doe - (365*yoe + int(yoe/4) - int(yoe/100))
mp = int((5*doy + 2) / 153)
D = doy - int((153*mp + 2) / 5) + 1
M = (mp < 10) ? mp + 3 : mp - 9
Y = (M <= 2) ? Y + 1 : Y
out = "{"
out = out "\"version\":\"1.1\""
out = out ",\"host\":\"" esc(host) "\""
out = out ",\"short_message\":\"" esc(msg) "\""
out = out ",\"timestamp\":" ts
out = out ",\"level\":" lvl
out = out ",\"msg\":\"" esc(msg) "\""
out = out sprintf(",\"time\":\"%04d-%02d-%02dT%02d:%02d:%02dZ\"", Y, M, D, h, m, s)
out = out ",\"_mode\":\"ssh\""
if (ev != "") out = out ",\"_event\":\"" esc(ev) "\""
if (from!= "") out = out ",\"_from\":\"" esc(from) "\""
if (to != "") out = out ",\"_to\":\"" esc(to) "\""
if (port!= "") out = out ",\"_port\":" port
if (user!= "") out = out ",\"_user\":\"" esc(user) "\""
if (fp != "") out = out ",\"_fingerprint\":\"" esc(fp) "\""
out = out "}"
print out
fflush()
}
function extract_fp(s, tmp){
if (match(s, /SHA256:[A-Za-z0-9+\/=]+/)) {
return substr(s, RSTART, RLENGTH)
}
return ""
}
function port_clean(p, x){
# "57852:11:" -> "57852"
split(p, x, ":")
return x[1]
}
BEGIN {
host = (ENVIRON["HOSTNAME"] != "" ? ENVIRON["HOSTNAME"] : "goBastion")
}
function split_msgs(chunk, arr, ns) {
# Insert newline before known sshd message starters when mid-line.
while (match(chunk, /[^ \t\r\n][ \t]+(Connection from |Starting session:|User child is on pid |Accepted publickey for |Postponed publickey for |Failed password for |Failed publickey for |Invalid user |Connection closed by |Received disconnect from |Disconnected from |Timeout before authentication for connection from |srclimit_penalise: )/)) {
chunk = substr(chunk, 1, RSTART-1) "\n" substr(chunk, RSTART+1)
}
ns = split(chunk, arr, "\n")
return ns
}
function trim(s) { gsub(/^[ \t]+|[ \t]+$/, "", s); return s }
{
raw = $0
gsub(/\r$/, "", raw)
# Split multi-message sshd chunks into separate lines.
n_chunks = split_msgs(raw, chunks)
for (ci = 1; ci <= n_chunks; ci++) {
msg = trim(chunks[ci])
if (msg == "") continue
# defaults
lvl=6; ev=""
from=""; port=""; user=""; to=""; fp=""
# tokenize (space-separated, POSIX-compatible)
n = split(msg, f, /[[:space:]]+/)
# --- Listen ---
if (msg ~ /^Server listening on /) {
ev="listen"
}
# --- Connection ---
# "Connection from <ip> port <p> on <to> port <p2> [rdomain ...]"
# f[1]=Connection f[2]=from f[3]=IP f[4]=port f[5]=PORT f[6]=on f[7]=TO
else if (msg ~ /^Connection from /) {
if (n >= 7) {
from = f[3]
port = port_clean(f[5])
to = f[7]
ev="connect"
}
}
# --- Auth attempts / success ---
# "Postponed publickey for <user> from <ip> port <p> ..."
# f[1]=Postponed f[2]=publickey f[3]=for f[4]=USER f[5]=from f[6]=IP f[7]=port f[8]=PORT
else if (msg ~ /^Postponed publickey for /) {
if (n >= 8) {
user=f[4]; from=f[6]; port=port_clean(f[8])
ev="auth_attempt"
}
}
# "Accepted publickey for <user> from <ip> port <p> ..."
else if (msg ~ /^Accepted publickey for /) {
if (n >= 8) {
user=f[4]; from=f[6]; port=port_clean(f[8])
ev="auth_success"
fp=extract_fp(msg)
}
}
else if (msg ~ /^Accepted key /) {
fp=extract_fp(msg)
ev="key_seen"
}
# --- Session start ---
else if (msg ~ /^Starting session:/) {
for (i=1; i<=n; i++) {
if (f[i] == "for" && i+1 <= n) user = f[i+1]
if (f[i] == "from" && i+1 <= n) from = f[i+1]
if (f[i] == "port" && i+1 <= n) { port = port_clean(f[i+1]); break }
}
ev="session_start"
}
# --- Failures / abuse ---
# "Failed publickey for root from 1.2.3.4 port 12345 ..."
# "Failed publickey for invalid user bob from 1.2.3.4 port 12345 ..."
else if (msg ~ /^Failed (password|publickey) for /) {
if (f[4] == "invalid" && f[5] == "user") {
user = f[6]
} else {
user = f[4]
}
for (i=1; i<=n; i++) {
if (f[i] == "from" && i+1 <= n) from = f[i+1]
if (f[i] == "port" && i+1 <= n) { port = port_clean(f[i+1]); break }
}
ev="auth_failed"
lvl=4
fp=extract_fp(msg)
}
# "Invalid user <user> from <ip> port <p>"
else if (msg ~ /^Invalid user /) {
user=f[3]
for (i=1; i<=n; i++) {
if (f[i] == "from" && i+1 <= n) from = f[i+1]
if (f[i] == "port" && i+1 <= n) { port = port_clean(f[i+1]); break }
}
ev="invalid_user"
lvl=4
}
# "Connection closed by invalid user <user> <ip> port <p> [preauth]"
# f[1]=Connection f[2]=closed f[3]=by f[4]=invalid f[5]=user f[6]=USER f[7]=IP f[8]=port f[9]=PORT
else if (msg ~ /^Connection closed by invalid user /) {
if (n >= 8) {
user=f[6]; from=f[7]; port=port_clean(f[9])
ev="closed"
lvl=4
}
}
# "Connection closed by authenticating user <user> <ip> port <p> [preauth]"
# f[1]=Connection f[2]=closed f[3]=by f[4]=authenticating f[5]=user f[6]=USER f[7]=IP f[8]=port f[9]=PORT
else if (msg ~ /^Connection closed by authenticating user /) {
if (n >= 8) {
user=f[6]; from=f[7]; port=port_clean(f[9])
ev="closed"
lvl=4
}
}
# "Connection closed by <ip> port <p> [preauth]"
else if (msg ~ /^Connection closed by /) {
for (i=1; i<=n; i++) {
if (f[i] == "by" && i+1 <= n) from = f[i+1]
if (f[i] == "port" && i+1 <= n) { port = port_clean(f[i+1]); break }
}
ev="closed"
lvl=4
}
# "Timeout before authentication for connection from <ip> to <ip>, pid = ..."
else if (msg ~ /^Timeout before authentication for connection from /) {
for (i=1; i<=n; i++) {
if (f[i] == "from" && i+1 <= n) from = f[i+1]
if (f[i] == "to" && i+1 <= n) to = f[i+1]
}
ev="timeout"
lvl=4
}
# "srclimit_penalise: ipv4: new <ip>/<mask> deferred penalty ..."
else if (msg ~ /^srclimit_penalise:/) {
for (i=1; i<=n; i++) {
if (f[i] == "new" && i+1 <= n) {
split(f[i+1], z, "/")
from = z[1]
break
}
}
ev="rate_limit"
lvl=4
}
# --- Disconnects ---
# "Received disconnect from <ip> port <p>:11: ..."
# This is always paired with a "Disconnected from [user|invalid user] X" line — we
# emit only this one to avoid duplicates. It carries IP+port; the username was already
# logged in the auth_success / auth_failed event above.
else if (msg ~ /^Received disconnect from /) {
for (i=1; i<=n; i++) {
if (f[i] == "from" && i+1 <= n) from = f[i+1]
if (f[i] == "port" && i+1 <= n) { port = port_clean(f[i+1]); break }
}
ev="disconnect"
}
# "Disconnected from authenticating user <user> <ip> port <p> [preauth]"
# Emitted when auth fails before a session is established (no Received disconnect pair).
# f[1]=Disconnected f[2]=from f[3]=authenticating f[4]=user f[5]=USER f[6]=IP f[7]=port f[8]=PORT
else if (msg ~ /^Disconnected from authenticating user /) {
if (n >= 8) {
user=f[5]; from=f[6]; port=port_clean(f[8])
ev="disconnect"
lvl=4
}
}
# "Disconnected from user <user> <ip> port <p>" — always paired with Received disconnect; skip.
# "Disconnected from invalid user <user> <ip> port <p>" — same; skip.
else if (msg ~ /^Disconnected from (user |invalid user )/) {
continue
}
emit(msg, host, lvl, ev, from, port, user, to, fp)
}
}
'