Deep Dive: RAGFlow’s Admin & Superuser Initialization System
Deep Dive: RAGFlow’s Admin & Superuser Initialization System
RAGFlow is an open-source RAG (Retrieval-Augmented Generation) engine. One of its less-documented but critical subsystems is how it bootstraps administrator accounts at startup. This post dissects the dual-service initialization architecture, the authentication pipeline, and the security implications you should be aware of before deploying to production.
Table of Contents
- Architecture Overview
- How the Default Superuser Is Created
- Docker Environment Defaults
- Manual Initialization with
--init-superuser - Enabling the Admin Server
init_superuservsinit_default_admin- Authentication Flow
- The First Registered User Is NOT a Superuser
- Customizing the Default Superuser
- Security Recommendations
Architecture Overview
RAGFlow’s codebase contains two independent Python services that share the same MySQL database:
| Service | Directory | Purpose | Default Port |
|---|---|---|---|
| Web Service | api/ |
Handles user documents, conversations, and the RAG pipeline | 9380 |
| Admin Service | admin/ |
System monitoring, user management, and operational CLI | 9381 |
Both services independently attempt to ensure an administrator account exists at startup, but they use different functions with different behaviors.
How the Default Superuser Is Created
When the Web service starts, it imports init_superuser from api/db/init_data.py:
# api/ragflow_server.py
from api.db.init_data import init_web_data, init_superuser
The init_superuser() function reads its configuration from environment variables, falling back to hardcoded defaults:
DEFAULT_SUPERUSER_NICKNAME = os.getenv("DEFAULT_SUPERUSER_NICKNAME", "admin")
DEFAULT_SUPERUSER_EMAIL = os.getenv("DEFAULT_SUPERUSER_EMAIL", "admin@ragflow.io")
DEFAULT_SUPERUSER_PASSWORD = os.getenv("DEFAULT_SUPERUSER_PASSWORD", "admin")
The password is Base64-encoded before being persisted to MySQL:
user_info = {
"id": uuid.uuid1().hex,
"password": encode_to_base64(password),
"nickname": nickname,
"is_superuser": True,
"email": email,
"creator": "system",
"status": "1",
}
The record is then saved via UserService.save() into the user table, along with associated tenant, user_tenant, and tenant_llm records.
The Admin service has its own parallel logic. When admin_server.py starts, it calls init_default_admin():
def init_default_admin():
users = UserService.query(is_superuser=True)
if not users:
default_admin = {
"id": uuid.uuid1().hex,
"password": encode_to_base64("admin"),
"nickname": "admin",
"is_superuser": True,
"email": "admin@ragflow.io",
"creator": "system",
"status": "1",
}
if not UserService.save(**default_admin):
raise AdminException("Can't init admin.", 500)
add_tenant_for_admin(default_admin, UserTenantRole.OWNER)
Both functions include existence checks to prevent duplicate creation. The key properties of the default account:
| Property | Value |
|---|---|
admin@ragflow.io (overridable via DEFAULT_SUPERUSER_EMAIL) |
|
| Password | admin (overridable via DEFAULT_SUPERUSER_PASSWORD) |
| Auto-created | Yes, at system startup |
| Storage | MySQL user table |
| Encoding | Base64 |
| Role | Superuser (is_superuser=True) |
Docker Environment Defaults
In a Docker deployment, if no .env file or environment variables are provided, the system uses the hardcoded fallback values and creates a fully functional admin account out of the box.
This “works-without-configuration” design means a fresh docker run produces:
- Email:
admin@ragflow.io - Password:
admin - Nickname:
admin
The Docker entrypoint.sh also supports a --init-superuser flag that gets forwarded to the Python process:
--init-superuser)
INIT_SUPERUSER_ARGS="--init-superuser"
"$PY" api/ragflow_server.py ${INIT_SUPERUSER_ARGS} &
For production, always override the default password via environment variables.
Manual Initialization with --init-superuser
Instead of relying on automatic startup behavior, you can explicitly trigger superuser creation:
python api/ragflow_server.py --init-superuser
The argument is parsed via argparse:
parser.add_argument(
"--init-superuser", default=False, help="init superuser", action="store_true"
)
When the flag is detected:
if args.init_superuser:
init_superuser()
This triggers the same init_superuser() function, which:
- Checks existence - queries by email to see if the user already exists
- Creates the user - builds the user record using environment variables or defaults
- Persists to MySQL - saves user, tenant, and LLM configuration records
- Validates LLM connectivity - tests that configured chat and embedding models respond
Manual vs Automatic Initialization
| Aspect | Automatic | Manual (--init-superuser) |
|---|---|---|
| Trigger | System startup | Explicit CLI argument |
| Entry point | init_web_data() call |
Direct init_superuser() call |
| Flexibility | Uses defaults | Respects environment variables |
| Repeat behavior | Checked every startup | Only when explicitly invoked |
Docker Usage
# Direct execution
python api/ragflow_server.py --init-superuser
# Via Docker entrypoint
docker run --entrypoint="/ragflow/docker/entrypoint.sh" ragflow --init-superuser
Enabling the Admin Server
By default, only the Web service runs. The Admin service is activated with --enable-adminserver:
# In docker-compose.yml
command:
- --enable-adminserver
The entrypoint script processes this flag:
ENABLE_ADMIN_SERVER=0 # Default: disabled
--enable-adminserver)
ENABLE_ADMIN_SERVER=1
When enabled, an additional process is spawned:
if [[ "${ENABLE_ADMIN_SERVER}" -eq 1 ]]; then
echo "Starting admin_server..."
while true; do
"$PY" admin/server/admin_server.py &
wait;
sleep 1;
done &
fi
You also need to expose the Admin port:
ports:
- ${SVR_HTTP_PORT}:9380
- ${ADMIN_SVR_HTTP_PORT}:9381
What the Admin Service Provides
- Real-time monitoring of the RAGFlow server, Task Executor processes, and dependency services (MySQL, Elasticsearch, Redis, MinIO)
- User management - create, modify, delete users and their associated knowledge bases and agents
- Service management - list and inspect system service status
- CLI interface - the
ragflow-clicommand-line tool for administrative operations
Kubernetes / Helm Support
args:
- "--enable-adminserver"
init_superuser vs init_default_admin
These two functions serve different services and differ in several important ways:
| Aspect | init_superuser (Web) |
init_default_admin (Admin) |
|---|---|---|
| Location | api/db/init_data.py |
admin/server/auth.py |
| Config source | Environment variables | Hardcoded values |
| Password | DEFAULT_SUPERUSER_PASSWORD env var (default: "admin") |
Always "admin" |
| Existence check | Checks by email only | Checks superuser existence AND active status |
| LLM validation | Tests chat and embedding model connectivity | None |
| Tenant setup | Creates user + tenant + user_tenant + tenant_llm | Creates user + tenant via add_tenant_for_admin() |
init_superuser - Full User Bootstrapping
def init_superuser(
nickname=DEFAULT_SUPERUSER_NICKNAME,
email=DEFAULT_SUPERUSER_EMAIL,
password=DEFAULT_SUPERUSER_PASSWORD,
role=UserTenantRole.OWNER
):
This function is designed for the Web service context. It:
- Supports full customization via environment variables
- Sets up the complete tenant hierarchy (user, tenant, LLM bindings)
- Validates that configured LLM models actually respond
- Is invoked via
--init-superuseror duringinit_web_data()
init_default_admin - Conservative Fallback
def init_default_admin():
users = UserService.query(is_superuser=True)
if not users:
# Create with hardcoded defaults
elif not any([u.is_active == ActiveEnum.ACTIVE.value for u in users]):
raise AdminException("No active admin. Please update 'is_active' in db manually.", 500)
This function is the Admin service’s safety net. It:
- Uses hardcoded credentials (not configurable via env vars)
- Performs stricter validation - checks not just existence but also active status
- Raises an exception if all superusers are inactive, forcing manual database intervention
- Only runs when the Admin service starts
This dual-function design follows the separation of concerns principle:
init_superuseris user-configurable for the Web serviceinit_default_adminis a system-level guarantee for the Admin service
Authentication Flow
Both services use JWT-based token authentication, but with different framework integrations.
Admin Service - Flask-Login request_loader
The Admin service registers a request_loader with Flask-Login:
def setup_auth(login_manager):
@login_manager.request_loader
def load_user(web_request):
jwt = Serializer(secret_key=settings.SECRET_KEY)
authorization = web_request.headers.get("Authorization")
The authentication pipeline:
- Extract the
Authorizationheader from the HTTP request - Decode the JWT token using the application secret key
- Validate format - the token must be at least 32 characters (UUID format)
- Query the database for a user matching the
access_token - Return the user object or
None
This integrates with the check_admin_auth decorator for permission enforcement:
def check_admin_auth(func):
@wraps(func)
def wrapper(*args, **kwargs):
user = UserService.filter_by_id(current_user.id)
if not user:
raise UserNotFoundError(current_user.email)
if not user.is_superuser:
raise AdminException("Not admin", 403)
if user.is_active == ActiveEnum.INACTIVE.value:
raise AdminException(f"User {current_user.email} inactive", 403)
return func(*args, **kwargs)
return wrapper
Web Service - Custom LocalProxy Pattern
The Web service uses a custom _load_user() function with Werkzeug’s LocalProxy:
def _load_user():
jwt = Serializer(secret_key=settings.SECRET_KEY)
authorization = request.headers.get("Authorization")
# ... JWT validation logic ...
# Falls back to API token authentication if JWT fails
current_user = LocalProxy(_load_user)
The Web service adds a fallback: if JWT authentication fails, it attempts API token-based authentication by splitting the Authorization header and looking up the token in the APIToken table.
The First Registered User Is NOT a Superuser
A common misconception: the first user to register through the Web UI does not get superuser privileges. The init_web_data() function has its automatic superuser creation commented out:
# if not UserService.get_all().count():
# init_superuser()
The registration endpoint explicitly sets is_superuser to False:
"is_superuser": False,
Superuser accounts can only be created through:
python api/ragflow_server.py --init-superuser- The Admin service’s
init_default_admin()at startup
| User Type | Creation Method | is_superuser |
Scope |
|---|---|---|---|
| Regular user | Web registration | False |
Own data only |
| Superuser | --init-superuser or Admin service |
True |
Full system administration |
The system functions normally without a superuser - users can still create knowledge bases and chat - but system-level management operations are unavailable.
Customizing the Default Superuser
Method 1: Docker .env File
DEFAULT_SUPERUSER_EMAIL=custom@example.com
DEFAULT_SUPERUSER_NICKNAME=myadmin
DEFAULT_SUPERUSER_PASSWORD=a_strong_password_here
Method 2: Docker Compose Environment
services:
ragflow:
environment:
- DEFAULT_SUPERUSER_EMAIL=custom@example.com
- DEFAULT_SUPERUSER_NICKNAME=myadmin
- DEFAULT_SUPERUSER_PASSWORD=a_strong_password_here
Method 3: Inline Docker Run
docker run -e DEFAULT_SUPERUSER_EMAIL=custom@example.com \
-e DEFAULT_SUPERUSER_NICKNAME=myadmin \
-e DEFAULT_SUPERUSER_PASSWORD=a_strong_password_here \
infiniflow/ragflow:latest
Caveat: The Admin service’s
init_default_admin()ignores these environment variables and always uses hardcodedadmin@ragflow.io. If you need a fully custom admin email, the Admin service source code must be modified.
Security Recommendations
- Always override the default password in production via
DEFAULT_SUPERUSER_PASSWORD - Change the password immediately after first login - the default
adminis publicly known - Enable the Admin service (
--enable-adminserver) for production monitoring and management - Restrict port 9381 - the Admin API should not be publicly accessible
- Be aware of Base64 encoding - passwords are Base64-encoded, not hashed. This is a reversible encoding, not a security measure. Treat database access as equivalent to plaintext password access