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:
Leendert de Borst
2026-04-29 10:37:40 +02:00
committed by GitHub
parent 000d1c2f87
commit 3eaf2ac5c6
10 changed files with 523 additions and 0 deletions

View File

@@ -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=

View File

@@ -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

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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

View 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.

View 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.

View File

@@ -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 "$@"