How RAGFlow Handles Passwords: Transport Encryption + One-Way Hashing

When reviewing RAGFlow’s authentication flow, one detail is especially important: passwords are not stored using reversible encryption. Instead, the system combines encrypted transport with one-way hashing at persistence time.

This post walks through that design and explains why it is practical for real-world deployments.

Security Model at a Glance

RAGFlow applies layered protection across the password lifecycle:

  1. Client-side transport protection: the password is encrypted with an RSA public key before being sent.
  2. Server-side validation and decoding: payloads are decrypted and validated in reset flows.
  3. Storage protection: the final password representation in the database is a hash generated by Werkzeug (generate_password_hash), which is one-way.

This means transport confidentiality and storage safety are handled as separate concerns, which is a sound security architecture.

Password Flow in Practice

1) CLI Reset Path

In the CLI path, the new password is base64-encoded and then hashed:

Reference: commands.py:39-44

encode_password = base64.b64encode(new_password.encode('utf-8')).decode('utf-8')
password_hash = generate_password_hash(encode_password)

The key point is the hash step: once generated, the stored value cannot be reversed to plaintext.

2) Web Reset Path

In the web reset flow, the incoming encrypted payload is decrypted and base64-decoded for validation (for example, to ensure both password entries match):

Reference: user_app.py:1026-1046

new_pwd_base64 = decrypt(new_pwd)
new_pwd_string = base64.b64decode(new_pwd_base64).decode('utf-8')
# ...
UserService.update_user_password(user.id, new_pwd_base64)

Even though the API layer temporarily handles a base64 form during flow processing, persistence still follows the hashing strategy through the user service path.

3) Client-Side Encryption

The client encrypts the base64-encoded password using an RSA public key before transmission:

Reference: ragflow_client.py:39-44

def encrypt(input_string):
    pub = "-----BEGIN PUBLIC KEY-----\n..."
    pub_key = RSA.importKey(pub)
    cipher = Cipher_pkcs1_v1_5.new(pub_key)
    cipher_text = cipher.encrypt(base64.b64encode(input_string.encode("utf-8")))
    return base64.b64encode(cipher_text).decode("utf-8")

This protects secrets in transit, especially when interacting with APIs from automation tools or admin clients.

Why This Design Works

  • Strong breach posture: if the database is leaked, attackers get password hashes rather than plaintext.
  • Clear separation of concerns: RSA handles transport confidentiality; hashing handles storage security.
  • Operationally practical: hashing is a standard, battle-tested pattern compatible with normal login verification workflows.

Operational Notes

  • One-way hashing is aligned with common password security best practices.
  • Base64 is an encoding format, not encryption; the security guarantees come from RSA (transport) and hashing (storage).
  • The default admin password admin should always be changed in production.

References