Episode 15: offboarding waits out the Vaultwarden admin rate limit, fail2ban exception only on the provider host
Measured clean run: Pocket ID disable alone left 12 of 14 ways open, the script (0.9 s, 9 of 9 steps) left 1 of 14.
This commit is contained in:
parent
12542ecb19
commit
54ab43dac7
3 changed files with 16 additions and 8 deletions
|
|
@ -587,11 +587,12 @@ before the eth0 lines:
|
||||||
**fail2ban locked out people logging in.** The `caddy-probes` jail counts
|
**fail2ban locked out people logging in.** The `caddy-probes` jail counts
|
||||||
401/403/404. Pocket ID's login page asks `/api/users/me` (401 without a session)
|
401/403/404. Pocket ID's login page asks `/api/users/me` (401 without a session)
|
||||||
and the logo (404) on every load. Ten page loads in a minute and the employee is
|
and the logo (404) on every load. Ten page loads in a minute and the employee is
|
||||||
banned for an hour. Narrow exception in `filter.d/caddy-probes.conf`:
|
banned for an hour. Narrow exception in `filter.d/caddy-probes.conf`, only for
|
||||||
|
the provider's own host, so the same paths elsewhere still count:
|
||||||
|
|
||||||
```
|
```
|
||||||
ignoreregex = "uri":"/api/users/me"
|
ignoreregex = "host":"id\.[^"]*","uri":"/api/users/me"
|
||||||
"uri":"/api/application-images/
|
"host":"id\.[^"]*","uri":"/api/application-images/
|
||||||
```
|
```
|
||||||
|
|
||||||
**Pocket ID's `/setup` belongs to whoever opens it first.** A scanner fetched
|
**Pocket ID's `/setup` belongs to whoever opens it first.** A scanner fetched
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ Measured on the demo box, with a test employee who had the access a real one has
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Forgejo login prohibited (Forgejo was the SSO provider) | **10 of 13**, unchanged for 45 minutes |
|
| Forgejo login prohibited (Forgejo was the SSO provider) | **10 of 13**, unchanged for 45 minutes |
|
||||||
| Pocket ID account disabled, nothing else | **12 of 14** |
|
| Pocket ID account disabled, nothing else | **12 of 14** |
|
||||||
| `./offboard.py <user>` (about one second) | **1 of 14**: the vault copy on their laptop |
|
| `./offboard.py <user>` (0.9 s, all 9 steps ok) | **1 of 14**: the vault copy on their laptop |
|
||||||
|
|
||||||
What stayed open with only the SSO switched off: the wiki session (valid for
|
What stayed open with only the SSO switched off: the wiki session (valid for
|
||||||
three months) and its API key, the vault sync (Forgejo kept handing out fresh
|
three months) and its API key, the vault sync (Forgejo kept handing out fresh
|
||||||
|
|
@ -65,7 +65,8 @@ give every key an expiry, and renew them on purpose.
|
||||||
without a JSON content type is not a 403, it is a 404, and it looks exactly
|
without a JSON content type is not a 403, it is a 404, and it looks exactly
|
||||||
like "user not found".
|
like "user not found".
|
||||||
- **Vaultwarden rate-limits the admin login** (a handful per five minutes). Two
|
- **Vaultwarden rate-limits the admin login** (a handful per five minutes). Two
|
||||||
test runs back to back and the third one gets 429. The script says so.
|
test runs back to back and the third one gets 429. The script now waits 60 s
|
||||||
|
and retries, up to six times, and prints each wait.
|
||||||
- **An old n8n API key cannot delete users.** Keys created before scopes
|
- **An old n8n API key cannot delete users.** Keys created before scopes
|
||||||
existed, or with the wrong ones, get 403. Create one with `user:delete`.
|
existed, or with the wrong ones, get 403. Create one with `user:delete`.
|
||||||
- **Forgejo tokens:** an admin can list a user's tokens (`Sudo:` header) but
|
- **Forgejo tokens:** an admin can list a user's tokens (`Sudo:` header) but
|
||||||
|
|
|
||||||
|
|
@ -125,9 +125,15 @@ def outline(email):
|
||||||
def vaultwarden(email):
|
def vaultwarden(email):
|
||||||
jar = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
|
jar = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
|
||||||
base = f"http://{ip('vaultwarden')}"
|
base = f"http://{ip('vaultwarden')}"
|
||||||
|
# The admin login is rate-limited (a few tries per 5 minutes). On 429 wait and retry.
|
||||||
|
for attempt in range(6):
|
||||||
s, _ = call("POST", f"{base}/admin", {"token": CFG["VAULTWARDEN_ADMIN_TOKEN"]}, form=True, opener=jar)
|
s, _ = call("POST", f"{base}/admin", {"token": CFG["VAULTWARDEN_ADMIN_TOKEN"]}, form=True, opener=jar)
|
||||||
if s != 200: # 429: the admin login allows only a few tries per 5 minutes
|
if s != 429:
|
||||||
return step("vaultwarden", f"admin login refused (HTTP {s}), run again in 5 minutes", False)
|
break
|
||||||
|
step("vaultwarden", f"admin login rate-limited (429), waiting 60 s, try {attempt + 2} of 6", True)
|
||||||
|
time.sleep(60)
|
||||||
|
if s != 200:
|
||||||
|
return step("vaultwarden", f"admin login refused (HTTP {s})", False)
|
||||||
s, users = call("GET", f"{base}/admin/users", opener=jar)
|
s, users = call("GET", f"{base}/admin/users", opener=jar)
|
||||||
hits = [u for u in (users if s == 200 else []) if u["email"] == email]
|
hits = [u for u in (users if s == 200 else []) if u["email"] == email]
|
||||||
if not hits:
|
if not hits:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue