mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-14 02:15:57 -04:00
Add option to limit access to admin to certain IP (ranges) for self hosted users (#1975)
* Add docs for restricting admin access (#1556) * Add admin geo scaffolding (#1556) * Update nginx config (#1556) * Add admin IP allowlist setting (#1556) * Update docs (#1556) * Update docs (#1556) * Update install.sh with configure-admin-access command (#1556) * Update admin access docs (#1556)
This commit is contained in:
committed by
GitHub
parent
000d1c2f87
commit
3eaf2ac5c6
@@ -111,3 +111,11 @@ SUPPORT_EMAIL=
|
||||
# API uploads, etc.). Increase if you run into "413 Request Entity Too Large"
|
||||
# errors when syncing large vaults. Defaults to 100MB.
|
||||
MAX_UPLOAD_SIZE_MB=100
|
||||
|
||||
# Restrict access to the /admin endpoint by client IP at the reverse-proxy layer. Options:
|
||||
# - Empty = no restriction (default, preserves existing behavior).
|
||||
# - "private" = allow only loopback + RFC1918 (127/8, 10/8, 172.16/12, 192.168/16).
|
||||
# - A comma-separated list of CIDRs/IPs (e.g. "192.168.1.0/24,10.0.0.0/8").
|
||||
# Requests from non-allowlisted IPs are silently routed to the client app and will
|
||||
# result in a 404 error.
|
||||
ADMIN_IP_ALLOWLIST=
|
||||
|
||||
@@ -19,6 +19,60 @@ fi
|
||||
MAX_UPLOAD_SIZE_MB_VALUE="${MAX_UPLOAD_SIZE_MB:-100}"
|
||||
sed -i "s|__MAX_UPLOAD_SIZE__|${MAX_UPLOAD_SIZE_MB_VALUE}|g" /etc/nginx/nginx.conf
|
||||
|
||||
# Build the geo-block rules for the /admin IP allowlist from ADMIN_IP_ALLOWLIST.
|
||||
# Empty -> "default 0;" (everyone allowed). "private" -> default-deny with
|
||||
# loopback + RFC1918 carved out. Anything else is treated as a comma-separated
|
||||
# list of CIDRs/IPs (loopback is always allowed). Tokens that don't look like
|
||||
# an IPv4/IPv6 address or CIDR are skipped with a warning so a typo can't
|
||||
# crash-loop nginx; if no valid tokens remain we fail closed (deny everything
|
||||
# except loopback). When the resulting $admin_block is 1, the /admin location
|
||||
# quietly routes the request to the client app via @admin_fallback rather than
|
||||
# emitting a 403/404.
|
||||
is_valid_cidr() {
|
||||
case "$1" in
|
||||
*:*)
|
||||
printf '%s' "$1" | grep -Eq '^[0-9a-fA-F:]+(/[0-9]{1,3})?$'
|
||||
;;
|
||||
*)
|
||||
printf '%s' "$1" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}(/[0-9]{1,2})?$'
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
build_admin_ip_geo_rules() {
|
||||
local raw="${ADMIN_IP_ALLOWLIST:-}"
|
||||
case "$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')" in
|
||||
"")
|
||||
printf '%s' "default 0;"
|
||||
;;
|
||||
private)
|
||||
printf '%s' "default 1; 127.0.0.0/8 0; 10.0.0.0/8 0; 172.16.0.0/12 0; 192.168.0.0/16 0;"
|
||||
;;
|
||||
*)
|
||||
local out="default 1; 127.0.0.0/8 0;"
|
||||
local valid=0
|
||||
local IFS=','
|
||||
for cidr in $raw; do
|
||||
cidr=$(printf '%s' "$cidr" | tr -d '[:space:]')
|
||||
[ -z "$cidr" ] && continue
|
||||
if is_valid_cidr "$cidr"; then
|
||||
out="$out $cidr 0;"
|
||||
valid=$((valid + 1))
|
||||
else
|
||||
printf 'warning: ADMIN_IP_ALLOWLIST: ignoring invalid CIDR/IP %s\n' "$cidr" >&2
|
||||
fi
|
||||
done
|
||||
if [ "$valid" -eq 0 ]; then
|
||||
printf 'warning: ADMIN_IP_ALLOWLIST has no valid entries; /admin will fall through to the client app for all non-loopback traffic\n' >&2
|
||||
fi
|
||||
printf '%s' "$out"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
ADMIN_IP_GEO_RULES_VALUE=$(build_admin_ip_geo_rules)
|
||||
sed -i "s|__ADMIN_IP_GEO_RULES__|${ADMIN_IP_GEO_RULES_VALUE}|g" /etc/nginx/nginx.conf
|
||||
|
||||
# Function to check if certificate needs regeneration
|
||||
needs_cert_regeneration() {
|
||||
# If cert doesn't exist, need to generate
|
||||
|
||||
@@ -29,6 +29,14 @@ http {
|
||||
real_ip_header X-Forwarded-For;
|
||||
real_ip_recursive on;
|
||||
|
||||
# /admin IP allowlist. Rules rendered by entrypoint.sh from ADMIN_IP_ALLOWLIST:
|
||||
# empty -> "default 0;" (everyone allowed); otherwise "default 1;" plus an
|
||||
# allow rule per CIDR. When $admin_block is 1, the /admin location quietly
|
||||
# routes the request to the client app via @admin_fallback.
|
||||
geo $admin_block {
|
||||
__ADMIN_IP_GEO_RULES__
|
||||
}
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
@@ -91,6 +99,14 @@ http {
|
||||
|
||||
# Admin interface
|
||||
location /admin {
|
||||
# If the IP allowlist is set and this client isn't allowed, fall through
|
||||
# to the client app instead of revealing that /admin exists. The 418 is
|
||||
# an internal-only signal caught by the error_page handler below.
|
||||
error_page 418 = @admin_fallback;
|
||||
if ($admin_block) {
|
||||
return 418;
|
||||
}
|
||||
|
||||
proxy_pass http://admin;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -140,5 +156,21 @@ http {
|
||||
proxy_intercept_errors on;
|
||||
error_page 502 503 504 =503 /status.html;
|
||||
}
|
||||
|
||||
# Internal-only fallback for /admin when the client IP isn't allowlisted.
|
||||
# Mirrors `location /` so the response is indistinguishable from any other
|
||||
# client-app request.
|
||||
location @admin_fallback {
|
||||
proxy_pass http://client;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_redirect http:// https://;
|
||||
|
||||
proxy_intercept_errors on;
|
||||
error_page 502 503 504 =503 /status.html;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,14 @@ http {
|
||||
real_ip_header X-Forwarded-For;
|
||||
real_ip_recursive on;
|
||||
|
||||
# /admin IP allowlist. Rules rendered by entrypoint.sh from ADMIN_IP_ALLOWLIST:
|
||||
# empty -> "default 0;" (everyone allowed); otherwise "default 1;" plus an
|
||||
# allow rule per CIDR. When $admin_block is 1, the /admin location quietly
|
||||
# routes the request to the client app via @admin_fallback.
|
||||
geo $admin_block {
|
||||
__ADMIN_IP_GEO_RULES__
|
||||
}
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
@@ -82,6 +90,14 @@ http {
|
||||
|
||||
# Admin interface
|
||||
location /admin {
|
||||
# If the IP allowlist is set and this client isn't allowed, fall through
|
||||
# to the client app instead of revealing that /admin exists. The 418 is
|
||||
# an internal-only signal caught by the error_page handler below.
|
||||
error_page 418 = @admin_fallback;
|
||||
if ($admin_block) {
|
||||
return 418;
|
||||
}
|
||||
|
||||
proxy_pass http://admin;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -125,5 +141,19 @@ http {
|
||||
proxy_intercept_errors on;
|
||||
error_page 502 503 504 =503 /status.html;
|
||||
}
|
||||
|
||||
# Internal-only fallback for /admin when the client IP isn't allowlisted.
|
||||
# Mirrors `location /` so the response is indistinguishable from any other
|
||||
# client-app request.
|
||||
location @admin_fallback {
|
||||
proxy_pass http://client;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_intercept_errors on;
|
||||
error_page 502 503 504 =503 /status.html;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,14 @@ http {
|
||||
real_ip_header X-Forwarded-For;
|
||||
real_ip_recursive on;
|
||||
|
||||
# /admin IP allowlist. Rules rendered by the nginx s6 script from ADMIN_IP_ALLOWLIST:
|
||||
# empty -> "default 0;" (everyone allowed); otherwise "default 1;" plus an
|
||||
# allow rule per CIDR. When $admin_block is 1, the /admin location quietly
|
||||
# routes the request to the client app via @admin_fallback.
|
||||
geo $admin_block {
|
||||
__ADMIN_IP_GEO_RULES__
|
||||
}
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
@@ -79,6 +87,14 @@ http {
|
||||
|
||||
# Admin interface
|
||||
location /admin {
|
||||
# If the IP allowlist is set and this client isn't allowed, fall through
|
||||
# to the client app instead of revealing that /admin exists. The 418 is
|
||||
# an internal-only signal caught by the error_page handler below.
|
||||
error_page 418 = @admin_fallback;
|
||||
if ($admin_block) {
|
||||
return 418;
|
||||
}
|
||||
|
||||
proxy_pass http://admin;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -128,5 +144,24 @@ http {
|
||||
proxy_intercept_errors on;
|
||||
error_page 502 503 504 =503 /status.html;
|
||||
}
|
||||
|
||||
# Internal-only fallback for /admin when the client IP isn't allowlisted.
|
||||
# Mirrors `location /` so the response is indistinguishable from any other
|
||||
# client-app request.
|
||||
location @admin_fallback {
|
||||
proxy_pass http://client;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_hide_header Cache-Control;
|
||||
proxy_hide_header Pragma;
|
||||
proxy_hide_header Expires;
|
||||
add_header Cache-Control $upstream_http_cache_control;
|
||||
|
||||
proxy_intercept_errors on;
|
||||
error_page 502 503 504 =503 /status.html;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,14 @@ http {
|
||||
real_ip_header X-Forwarded-For;
|
||||
real_ip_recursive on;
|
||||
|
||||
# /admin IP allowlist. Rules rendered by the nginx s6 script from ADMIN_IP_ALLOWLIST:
|
||||
# empty -> "default 0;" (everyone allowed); otherwise "default 1;" plus an
|
||||
# allow rule per CIDR. When $admin_block is 1, the /admin location quietly
|
||||
# routes the request to the client app via @admin_fallback.
|
||||
geo $admin_block {
|
||||
__ADMIN_IP_GEO_RULES__
|
||||
}
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
@@ -66,6 +74,14 @@ http {
|
||||
|
||||
# Admin interface
|
||||
location /admin {
|
||||
# If the IP allowlist is set and this client isn't allowed, fall through
|
||||
# to the client app instead of revealing that /admin exists. The 418 is
|
||||
# an internal-only signal caught by the error_page handler below.
|
||||
error_page 418 = @admin_fallback;
|
||||
if ($admin_block) {
|
||||
return 418;
|
||||
}
|
||||
|
||||
proxy_pass http://admin;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -115,6 +131,25 @@ http {
|
||||
proxy_intercept_errors on;
|
||||
error_page 502 503 504 =503 /status.html;
|
||||
}
|
||||
|
||||
# Internal-only fallback for /admin when the client IP isn't allowlisted.
|
||||
# Mirrors `location /` so the response is indistinguishable from any other
|
||||
# client-app request.
|
||||
location @admin_fallback {
|
||||
proxy_pass http://client;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_hide_header Cache-Control;
|
||||
proxy_hide_header Pragma;
|
||||
proxy_hide_header Expires;
|
||||
add_header Cache-Control $upstream_http_cache_control;
|
||||
|
||||
proxy_intercept_errors on;
|
||||
error_page 502 503 504 =503 /status.html;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTPS server
|
||||
@@ -147,6 +182,14 @@ http {
|
||||
|
||||
# Admin interface
|
||||
location /admin {
|
||||
# If the IP allowlist is set and this client isn't allowed, fall through
|
||||
# to the client app instead of revealing that /admin exists. The 418 is
|
||||
# an internal-only signal caught by the error_page handler below.
|
||||
error_page 418 = @admin_fallback;
|
||||
if ($admin_block) {
|
||||
return 418;
|
||||
}
|
||||
|
||||
proxy_pass http://admin;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -196,5 +239,24 @@ http {
|
||||
proxy_intercept_errors on;
|
||||
error_page 502 503 504 =503 /status.html;
|
||||
}
|
||||
|
||||
# Internal-only fallback for /admin when the client IP isn't allowlisted.
|
||||
# Mirrors `location /` so the response is indistinguishable from any other
|
||||
# client-app request.
|
||||
location @admin_fallback {
|
||||
proxy_pass http://client;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_hide_header Cache-Control;
|
||||
proxy_hide_header Pragma;
|
||||
proxy_hide_header Expires;
|
||||
add_header Cache-Control $upstream_http_cache_control;
|
||||
|
||||
proxy_intercept_errors on;
|
||||
error_page 502 503 504 =503 /status.html;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,60 @@ else
|
||||
cp /etc/nginx/nginx-80-443.conf /etc/nginx/nginx.conf
|
||||
fi
|
||||
|
||||
# Build the geo-block rules for the /admin IP allowlist from ADMIN_IP_ALLOWLIST.
|
||||
# Empty -> "default 0;" (everyone allowed). "private" -> default-deny with
|
||||
# loopback + RFC1918 carved out. Anything else is treated as a comma-separated
|
||||
# list of CIDRs/IPs (loopback is always allowed). Tokens that don't look like
|
||||
# an IPv4/IPv6 address or CIDR are skipped with a warning so a typo can't
|
||||
# crash-loop nginx; if no valid tokens remain we fail closed (deny everything
|
||||
# except loopback). When the resulting $admin_block is 1, the /admin location
|
||||
# quietly routes the request to the client app via @admin_fallback rather than
|
||||
# emitting a 403/404.
|
||||
is_valid_cidr() {
|
||||
case "$1" in
|
||||
*:*)
|
||||
printf '%s' "$1" | grep -Eq '^[0-9a-fA-F:]+(/[0-9]{1,3})?$'
|
||||
;;
|
||||
*)
|
||||
printf '%s' "$1" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}(/[0-9]{1,2})?$'
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
build_admin_ip_geo_rules() {
|
||||
local raw="${ADMIN_IP_ALLOWLIST:-}"
|
||||
case "$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')" in
|
||||
"")
|
||||
printf '%s' "default 0;"
|
||||
;;
|
||||
private)
|
||||
printf '%s' "default 1; 127.0.0.0/8 0; 10.0.0.0/8 0; 172.16.0.0/12 0; 192.168.0.0/16 0;"
|
||||
;;
|
||||
*)
|
||||
local out="default 1; 127.0.0.0/8 0;"
|
||||
local valid=0
|
||||
local IFS=','
|
||||
for cidr in $raw; do
|
||||
cidr=$(printf '%s' "$cidr" | tr -d '[:space:]')
|
||||
[ -z "$cidr" ] && continue
|
||||
if is_valid_cidr "$cidr"; then
|
||||
out="$out $cidr 0;"
|
||||
valid=$((valid + 1))
|
||||
else
|
||||
printf 'warning: ADMIN_IP_ALLOWLIST: ignoring invalid CIDR/IP %s\n' "$cidr" >&2
|
||||
fi
|
||||
done
|
||||
if [ "$valid" -eq 0 ]; then
|
||||
printf 'warning: ADMIN_IP_ALLOWLIST has no valid entries; /admin will fall through to the client app for all non-loopback traffic\n' >&2
|
||||
fi
|
||||
printf '%s' "$out"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
ADMIN_IP_GEO_RULES_VALUE=$(build_admin_ip_geo_rules)
|
||||
sed -i "s|__ADMIN_IP_GEO_RULES__|${ADMIN_IP_GEO_RULES_VALUE}|g" /etc/nginx/nginx.conf
|
||||
|
||||
echo "Starting Nginx reverse proxy..."
|
||||
|
||||
# Set nginx error log level based on verbosity
|
||||
|
||||
63
docs/installation/docker-compose/advanced/admin-access.md
Normal file
63
docs/installation/docker-compose/advanced/admin-access.md
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
layout: default
|
||||
title: Admin access
|
||||
parent: Advanced
|
||||
grand_parent: Docker Compose
|
||||
nav_order: 5
|
||||
---
|
||||
|
||||
# Admin access
|
||||
|
||||
By default the admin panel at `/admin` is reachable from the public internet, alongside the regular client app. This is intentional and safe to leave as-is for most installations:
|
||||
|
||||
- Sign-in requires the admin password you set during installation (and optional 2FA).
|
||||
- The admin account is protected against brute force: after 10 failed sign-in attempts the account is locked for 30 minutes.
|
||||
|
||||
If you'd still rather not expose `/admin` to the open internet, for example if your AliasVault server is only meant to be reached from a home network or VPN, you can restrict it by client IP at the reverse-proxy layer using the `ADMIN_IP_ALLOWLIST` environment variable.
|
||||
|
||||
## How it works
|
||||
|
||||
When a request to `/admin` comes from an IP that is **not** on the allowlist, the reverse proxy quietly forwards it to the regular client app instead of returning a 403/404. From the outside, `/admin` looks identical to any other path on the public surface. There's no signal that an admin panel exists at that URL.
|
||||
|
||||
Requests from allowlisted IPs reach the admin panel as normal.
|
||||
|
||||
## Options
|
||||
|
||||
Set `ADMIN_IP_ALLOWLIST` in the `environment:` section of your `docker-compose.yml` to one of:
|
||||
|
||||
| Value | Effect |
|
||||
|---|---|
|
||||
| _empty_ (default) | No restriction. `/admin` is reachable from anywhere. |
|
||||
| `private` | Only loopback and RFC1918 addresses are allowed (`127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`). |
|
||||
| Comma-separated list of CIDRs/IPs | Only the listed ranges are allowed (loopback is always allowed). |
|
||||
|
||||
### Examples
|
||||
|
||||
```yaml
|
||||
# ...
|
||||
environment:
|
||||
# Only allow access from a specific home IP and a corporate /24:
|
||||
ADMIN_IP_ALLOWLIST: "203.0.113.42,198.51.100.0/24"
|
||||
# ...
|
||||
```
|
||||
|
||||
```yaml
|
||||
# ...
|
||||
environment:
|
||||
# Only allow access from machines on the local network:
|
||||
ADMIN_IP_ALLOWLIST: "private"
|
||||
# ...
|
||||
```
|
||||
|
||||
## Apply the change
|
||||
|
||||
After updating `docker-compose.yml`, the container must be recreated for the new environment value to take effect:
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Behind another reverse proxy
|
||||
|
||||
If AliasVault is itself running behind another reverse proxy (Cloudflare, Traefik, an upstream nginx, etc.), the allowlist is matched against the client IP forwarded via `X-Forwarded-For`. Make sure your upstream proxy is setting that header correctly, otherwise every request will appear to come from the proxy's own address.
|
||||
42
docs/installation/script/advanced/admin-access.md
Normal file
42
docs/installation/script/advanced/admin-access.md
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
layout: default
|
||||
title: Admin access
|
||||
parent: Advanced
|
||||
grand_parent: Install Script
|
||||
nav_order: 5
|
||||
---
|
||||
|
||||
# Admin access
|
||||
|
||||
By default the admin panel at `/admin` is reachable from the public internet, alongside the regular client app. This is intentional and safe to leave as-is for most installations:
|
||||
|
||||
- Sign-in requires the admin password you set during installation (and optional 2FA).
|
||||
- The admin account is protected against brute force: after 10 failed sign-in attempts the account is locked for 30 minutes.
|
||||
|
||||
If you'd still rather not expose `/admin` to the open internet — for example if your AliasVault server is only meant to be reached from a home network or VPN — you can restrict it by client IP at the reverse-proxy layer.
|
||||
|
||||
## How it works
|
||||
|
||||
When a request to `/admin` comes from an IP that is **not** on the allowlist, the reverse proxy quietly forwards it to the regular client app instead of returning a 403/404. From the outside, `/admin` looks identical to any other path on the public surface. There's no signal that an admin panel exists at that URL.
|
||||
|
||||
Requests from allowlisted IPs reach the admin panel as normal.
|
||||
|
||||
## Configure
|
||||
|
||||
Run the install script — it walks you through the options, validates your input, and offers to restart the containers for you:
|
||||
|
||||
```bash
|
||||
$ ./install.sh configure-admin-access
|
||||
```
|
||||
|
||||
You'll be prompted to choose one of:
|
||||
|
||||
| Option | Effect |
|
||||
|---|---|
|
||||
| No restriction (default) | `/admin` is reachable from anywhere. |
|
||||
| Private networks only | Only loopback and RFC1918 addresses are allowed (`127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`). |
|
||||
| Custom CIDRs/IPs | Only the listed ranges are allowed (loopback is always allowed). |
|
||||
|
||||
## Behind another reverse proxy
|
||||
|
||||
If AliasVault is itself running behind another reverse proxy (Cloudflare, Traefik, an upstream nginx, etc.), the allowlist is matched against the client IP forwarded via `X-Forwarded-For`. Make sure your upstream proxy is setting that header correctly, otherwise every request will appear to come from the proxy's own address.
|
||||
143
install.sh
143
install.sh
@@ -61,6 +61,7 @@ show_usage() {
|
||||
printf " configure-email Configure email domains for receiving emails\n"
|
||||
printf " configure-registration Configure new account registration (enable or disable)\n"
|
||||
printf " configure-ip-logging Configure IP address logging (enable or disable)\n"
|
||||
printf " configure-admin-access Configure /admin IP allowlist (restrict admin access by client IP)\n"
|
||||
printf " reset-admin-password Reset admin password\n"
|
||||
printf " uninstall Uninstall AliasVault\n"
|
||||
printf "\n"
|
||||
@@ -164,6 +165,10 @@ parse_args() {
|
||||
COMMAND="configure-ip-logging"
|
||||
shift
|
||||
;;
|
||||
configure-admin-access|admin-access)
|
||||
COMMAND="configure-admin-access"
|
||||
shift
|
||||
;;
|
||||
start|s)
|
||||
COMMAND="start"
|
||||
shift
|
||||
@@ -970,6 +975,9 @@ main() {
|
||||
"configure-ip-logging")
|
||||
handle_ip_logging_configuration
|
||||
;;
|
||||
"configure-admin-access")
|
||||
handle_admin_access_configuration
|
||||
;;
|
||||
"start")
|
||||
handle_start
|
||||
;;
|
||||
@@ -3297,6 +3305,135 @@ handle_ip_logging_configuration() {
|
||||
esac
|
||||
}
|
||||
|
||||
# Validate that a token looks like an IPv4/IPv6 address with optional CIDR mask.
|
||||
# IPv4 enforces each octet 0-255 and mask 0-32. IPv6 is checked loosely (hex
|
||||
# segments + colons) but with a strict 0-128 mask bound; nginx will reject any
|
||||
# malformed IPv6 at startup.
|
||||
is_valid_cidr() {
|
||||
case "$1" in
|
||||
*:*)
|
||||
printf '%s' "$1" | grep -Eq '^[0-9a-fA-F:]+(/(12[0-8]|1[01][0-9]|[1-9]?[0-9]))?$'
|
||||
;;
|
||||
*)
|
||||
printf '%s' "$1" | grep -Eq '^(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}(/(3[0-2]|[12]?[0-9]))?$'
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Function to handle /admin IP allowlist configuration
|
||||
handle_admin_access_configuration() {
|
||||
printf "${YELLOW}+++ Admin Access Configuration +++${NC}\n"
|
||||
printf "\n"
|
||||
|
||||
# Check if AliasVault is installed
|
||||
if [ ! -f "docker-compose.yml" ]; then
|
||||
printf "${RED}Error: AliasVault must be installed first.${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CURRENT_SETTING=$(grep "^ADMIN_IP_ALLOWLIST=" "$ENV_FILE" | cut -d '=' -f2-)
|
||||
|
||||
printf "${CYAN}About Admin Access:${NC}\n"
|
||||
printf "By default /admin is reachable from anywhere. You can optionally restrict it by client IP. \n"
|
||||
printf "Requests from non-allowlisted IPs are silently routed to the client app and will throw a 404.\n"
|
||||
printf "\n"
|
||||
printf "${CYAN}Current Configuration:${NC}\n"
|
||||
if [ -z "$CURRENT_SETTING" ]; then
|
||||
printf "Admin IP Allowlist: ${GREEN}No restriction${NC} (reachable from anywhere)\n"
|
||||
else
|
||||
printf "Admin IP Allowlist: ${CYAN}${CURRENT_SETTING}${NC}\n"
|
||||
fi
|
||||
printf "\n"
|
||||
printf "${CYAN}Options:${NC}\n"
|
||||
printf "1) No restriction — /admin is reachable from anywhere (default)\n"
|
||||
printf "2) Private networks only — allow loopback + RFC1918 (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)\n"
|
||||
printf "3) Custom — comma-separated CIDRs/IPs (e.g. 203.0.113.42,198.51.100.0/24)\n"
|
||||
printf "4) Cancel\n"
|
||||
printf "\n"
|
||||
|
||||
read -p "Select an option [1-4]: " allowlist_option
|
||||
|
||||
NEW_VALUE=""
|
||||
case $allowlist_option in
|
||||
1)
|
||||
NEW_VALUE=""
|
||||
;;
|
||||
2)
|
||||
NEW_VALUE="private"
|
||||
;;
|
||||
3)
|
||||
while true; do
|
||||
read -p "Enter comma-separated CIDRs/IPs: " CUSTOM_LIST
|
||||
if [ -z "$CUSTOM_LIST" ]; then
|
||||
printf "${YELLOW}> List cannot be empty. Use option 1 to remove restrictions.${NC}\n"
|
||||
continue
|
||||
fi
|
||||
|
||||
INVALID=""
|
||||
CLEANED=""
|
||||
OLD_IFS="$IFS"
|
||||
IFS=','
|
||||
for token in $CUSTOM_LIST; do
|
||||
token=$(printf '%s' "$token" | tr -d '[:space:]')
|
||||
[ -z "$token" ] && continue
|
||||
if is_valid_cidr "$token"; then
|
||||
if [ -z "$CLEANED" ]; then
|
||||
CLEANED="$token"
|
||||
else
|
||||
CLEANED="$CLEANED,$token"
|
||||
fi
|
||||
else
|
||||
if [ -z "$INVALID" ]; then
|
||||
INVALID="$token"
|
||||
else
|
||||
INVALID="$INVALID, $token"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
IFS="$OLD_IFS"
|
||||
|
||||
if [ -n "$INVALID" ]; then
|
||||
printf "${YELLOW}> Invalid entries: ${INVALID}${NC}\n"
|
||||
printf "${YELLOW}> Each entry must be an IPv4/IPv6 address with an optional /mask. Try again.${NC}\n"
|
||||
continue
|
||||
fi
|
||||
|
||||
NEW_VALUE="$CLEANED"
|
||||
break
|
||||
done
|
||||
;;
|
||||
4)
|
||||
printf "${YELLOW}Admin access configuration cancelled.${NC}\n"
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
printf "${RED}Invalid option selected.${NC}\n"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
update_env_var "ADMIN_IP_ALLOWLIST" "$NEW_VALUE"
|
||||
|
||||
printf "\n${YELLOW}Warning: Docker containers need to be restarted to apply these changes.${NC}\n"
|
||||
read -p "Restart now? (y/n): " restart_confirm
|
||||
|
||||
if [ "$restart_confirm" != "y" ] && [ "$restart_confirm" != "Y" ]; then
|
||||
printf "${YELLOW}Please restart manually to apply the changes.${NC}\n"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
handle_restart
|
||||
|
||||
printf "\n"
|
||||
if [ -z "$NEW_VALUE" ]; then
|
||||
print_success_box "Admin access restriction removed — /admin is now reachable from anywhere."
|
||||
elif [ "$NEW_VALUE" = "private" ]; then
|
||||
print_success_box "Admin access restricted to loopback + private networks."
|
||||
else
|
||||
print_success_box "Admin access restricted to: ${NEW_VALUE}"
|
||||
fi
|
||||
}
|
||||
|
||||
check_and_populate_env() {
|
||||
printf "${CYAN}ℹ Checking .env values...${NC} ${GREEN}✓${NC}\n"
|
||||
|
||||
@@ -3374,6 +3511,12 @@ check_and_populate_env() {
|
||||
update_env_var "MAX_UPLOAD_SIZE_MB" "100"
|
||||
printf " Set MAX_UPLOAD_SIZE_MB\n"
|
||||
fi
|
||||
|
||||
# ADMIN_IP_ALLOWLIST
|
||||
if ! grep -q "^ADMIN_IP_ALLOWLIST=" "$ENV_FILE" 2>/dev/null; then
|
||||
update_env_var "ADMIN_IP_ALLOWLIST" ""
|
||||
printf " Set ADMIN_IP_ALLOWLIST\n"
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
Reference in New Issue
Block a user