Ragflow Superuser
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.
In the Docker image, docker/entrypoint.sh is the container entrypoint. You can validate this in the Dockerfile:
ENTRYPOINT ["./entrypoint.sh"]
That entrypoint starts the Web service first and, when enabled, starts the Admin service afterward:
"$PY" api/ragflow_server.py ${INIT_SUPERUSER_ARGS} &
"$PY" admin/server/admin_server.py &
So the creation attempts happen in this order:
api/ragflow_server.pyconsumesinit_superuser()fromapi/db/init_data.pyadmin/server/admin_server.pyconsumesinit_default_admin()fromadmin/server/auth.py
In other words, Docker startup first gives the Web service a chance to create the superuser via init_superuser(), then the Admin service performs its fallback check via init_default_admin().
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