ERPNext security

ERPNext Security Hardening Checklist

A practical ERPNext security checklist for self-hosted Frappe deployments: patching, roles and permission levels, 2FA and password policy, API key hygiene, safe whitelisted methods, server hardening, encrypted backups, file uploads, audit trails and Server Script risk.

9 min read

The short answer

ERPNext security gets less attention than it deserves because the framework ships with sensible defaults and a mature permission system, so teams assume the job is done. It is not. A self-hosted ERPNext site is a web application, a Python runtime, a MariaDB database, several Redis instances and a Linux server, and the weakest of those decides how safe your ledgers, payroll and customer data are. This checklist covers what I check, in the order I check it.

  • Patch Frappe, ERPNext and custom apps on a schedule, and watch the Frappe security advisories.
  • Use User Permissions and permission levels, not just role permissions, and keep System Manager to a named few.
  • Turn on 2FA, password policy, lockout and sensible session expiry in System Settings.
  • Whitelisting exposes a function; it never authorises the caller. Check permissions in every whitelisted method.
  • Leave Server Scripts disabled unless needed, and move stable logic into a versioned custom app.
  • Keep MariaDB and Redis off the internet, serve only HTTPS, and run the bench as a non-root user.
  • Encrypt and test backups, and store the encryption key away from them.

ERPNext security gets less attention than it deserves because the framework ships with sensible defaults and a mature permission system, so teams assume the job is done. It is not. A self-hosted ERPNext site is a web application, a Python runtime, a MariaDB database, several Redis instances and a Linux server, and the weakest of those decides how safe your ledgers, payroll and customer data are. This checklist covers what I check, in the order I check it.

The hardening checklist at a glance

AreaCheckWhy
PatchingFrappe, ERPNext and custom apps on a supported, current release; security advisories watchedMost real compromises use known, already-fixed bugs
RolesRoles by job function; System Manager limited to a named fewSystem Manager is effectively root inside the application
Data scopeUser Permissions and permission levels applied to sensitive DocTypes and fieldsRole permissions alone grant every record of a DocType
LoginTwo-factor auth, password policy, lockout and session expiry set in System SettingsCredential stuffing and phishing are the cheapest attacks
API accessKeys on dedicated integration users with minimal roles; rotated and revokedAn API key acts with the full rights of its user
Custom codeEvery whitelisted method checks permissions; guest methods reviewed and rate-limitedWhitelisting only exposes a function; it does not authorise the caller
Server ScriptsLeft disabled unless needed; changes reviewedAnyone who can edit them can run server-side code
ServerOnly 80, 443 and SSH open; Redis and MariaDB on localhost; TLS; fail2banExposed Redis or MariaDB bypasses every application control
BackupsEncrypted, off-server, restore-tested; keys from site_config.json stored separatelyWithout the keys, encrypted backups and stored passwords cannot be recovered
FilesAllowed extensions and size limits set; private by default; guest uploads offUpload features are a classic route to malware and data exposure
AuditTrack Changes on high-value DocTypes; Access and Activity logs reviewedYou cannot investigate what you did not record

1. Patch the framework, the apps and the bench

Frappe and ERPNext move quickly, and security fixes land in point releases. The Frappe team publishes advisories on the Frappe GitHub security page; someone on your team should be watching it. Stay on a supported major version, apply minor updates on a schedule rather than "when something breaks", and remember that custom and third-party apps need the same discipline. Rehearse every update on a staging copy first, as described in the ERPNext development guide, so patching is routine rather than a risk.

2. Roles, User Permissions and permission levels

Frappe's permission model has three layers, and most deployments only use the first. The users and permissions documentation covers each in detail.

  • Role permissions, managed in the Role Permissions Manager, decide which DocTypes a role can read, write, create, submit, cancel, amend, export or print. Grant Export and Print deliberately; they are the easiest way to take data out in bulk.
  • User Permissions restrict which records a user sees, based on a Link field value: a company, a branch, a warehouse, a territory. Without them, a role that can read Sales Invoice can read every Sales Invoice.
  • Permission levels group fields so that, for example, salary or margin fields sit at a higher level that only some roles can read or write. Use them for the handful of fields that are sensitive on otherwise ordinary forms.
  • "If the user is owner" limits a rule to documents the user created, which is useful for self-service forms.

Then deal with the powerful accounts. Administrator should not be anyone's daily login, and its password should be long, unique and stored in a password manager. Membership of System Manager should be a short, named list reviewed every quarter, because it accumulates quietly: one consultant during go-live, one manager who needed a report, one integration that "just needed access". The Restrict IP field on the User record can pin high-privilege accounts to office or VPN addresses.

3. Login: 2FA, password policy and lockout

System Settings holds the login controls, and they are worth ten minutes of anyone's time:

  • Enable Two Factor Auth and choose the method; an OTP app is stronger than SMS or email. Be careful with the bypass options for restricted-IP users, which trade security for convenience.
  • Enable Password Policy with a sensible Minimum Password Score, and use Force User to Reset Password if you want periodic rotation.
  • Allow Consecutive Login Attempts and Allow Login After Fail together give you an account lockout. Set them, rather than relying on nginx alone.
  • Session Expiry controls the idle timeout. ERP sessions on shared warehouse or front-desk machines should not last for days.
  • Logout All Sessions on Password Reset should be on, so a reset actually evicts an attacker.

4. API keys and integration users

Frappe's REST API authenticates with an Authorization: token api_key:api_secret header, using keys generated from a User record. The secret is shown once. The important point is that the key carries every right its user has, so the user matters more than the key.

  • Create a dedicated user per integration with a purpose-built role that grants only the DocTypes and actions it needs. Never generate keys on an Administrator or System Manager account.
  • Keep secrets in the integration's secret store, not in scripts, spreadsheets, browser extensions or chat history.
  • Regenerate keys when a vendor or staff member with access leaves, and disable the user entirely when the integration is retired.
  • Turn on Log API Requests in System Settings where your version supports it, so you can see what each integration actually calls.

5. Whitelisted methods and guest access in custom apps

This is where I find the most serious issues in custom apps. The @frappe.whitelist() decorator makes a Python function callable over HTTP at /api/method/.... It checks that the caller is logged in. It does not check whether that user may read or change the records the function touches. Adding allow_guest=True removes even the login requirement, so the function is callable by anyone on the internet.

Two framework details make this worse in practice. frappe.get_all ignores permissions by design, whereas frappe.get_list applies them, so a whitelisted function that uses get_all returns data regardless of the caller's role. And ignore_permissions=True on an insert or save inside a whitelisted method turns the endpoint into a privilege bypass. A properly checked method looks like this:

import frappe
from frappe import _
from frappe.rate_limiter import rate_limit


@frappe.whitelist(methods=["POST"])
def approve_vendor_payout(name: str, remarks: str | None = None):
    # 1. Role gate: raises PermissionError if the caller lacks the role.
    frappe.only_for("Payout Approver")

    # 2. Record-level check: role permissions, User Permissions and
    #    sharing are all evaluated for this specific document.
    doc = frappe.get_doc("Vendor Payout", name)
    doc.check_permission("write")

    # 3. Business rule checks on server-side state, never on client input.
    if doc.docstatus != 0 or doc.status != "Pending Approval":
        frappe.throw(_("Only pending payouts can be approved."))

    doc.status = "Approved"
    doc.approval_remarks = remarks
    doc.save()  # no ignore_permissions
    return {"name": doc.name, "status": doc.status}


@frappe.whitelist(allow_guest=True, methods=["POST"])
@rate_limit(limit=5, seconds=60 * 60)
def submit_enquiry(email: str, message: str):
    # Guest endpoints do one narrow thing, validate everything,
    # and return nothing about existing records.
    frappe.utils.validate_email_address(email, throw=True)
    frappe.get_doc({
        "doctype": "Website Enquiry",
        "email": email,
        "message": message[:2000],
    }).insert(ignore_permissions=True)  # deliberate, reviewed, single DocType
    return {"ok": True}
  • Restrict HTTP methods with methods=["POST"] on anything that changes state.
  • Grep every custom app for allow_guest=True, ignore_permissions=True and frappe.get_all inside whitelisted functions, and justify each one in code review.
  • Use frappe.has_permission(..., throw=True) or doc.check_permission() rather than hand-rolled role checks.
  • Rate-limit guest endpoints with Frappe's rate_limit decorator.

6. Server Scripts and other code in the database

Server Scripts let users write Python in the browser that runs on the server. Frappe runs them in a restricted environment, but they still execute with access to your data, and anyone with rights to edit them can change business logic without a deploy, review or git history. From version 15, the Server Script documentation states they are disabled by default and enabled with bench set-config -g server_script_enabled true. Leave them off unless you have a reason, and when you do enable them, limit who can create or edit Server Script and Client Script records and move stable logic into a versioned custom app.

7. Server hardening: the bench, nginx, Redis and MariaDB

When I (Sheikh Mujtaba) harden a Frappe server, I start from the same place as on any web host I have cleaned up after a compromise: assume the application will eventually have a bug, and make sure that bug cannot reach the rest of the machine. My work on backdoor removal, hosting hardening and obfuscated PHP malware analysis taught me that attackers look for writable directories that are also served, credentials sitting in config files, and internal services listening on public interfaces. ERPNext servers have all three to think about.

  • Run the bench as a dedicated non-root user. Use bench setup sudoers for the narrow sudo rights it needs rather than broad root access.
  • Use production mode (bench setup production) so nginx and supervisor or systemd manage processes, not bench start.
  • Serve only HTTPS. bench setup lets-encrypt <site> issues and installs a certificate; redirect HTTP to HTTPS.
  • Close every port except web and SSH. bench setup firewall blocks all ports except 80, 443 and your SSH port. MariaDB (3306) and the bench Redis instances (by default 11000 and 13000) must listen on localhost or a private network only. An exposed Redis instance can be used to tamper with queues and cache; an exposed MariaDB is the whole ERP.
  • Add brute-force protection with bench setup fail2ban, and use key-only SSH.
  • Keep secrets readable only by the bench user. site_config.json holds the database password, the encryption_key and, if backups are encrypted, the backup_encryption_key; it should never be world-readable or committed to git.
  • Never serve executable content from file directories. Public files are served directly by nginx; confirm nothing under them can be executed and watch for unexpected file types.

8. Backups and the encryption key

Frappe encrypts stored passwords (such as Password fields and integration credentials) with the encryption_key in the site's site_config.json. If you restore a database without the matching key, those values cannot be decrypted. Backup encryption, switched on with Encrypt Backups in System Settings, uses a separate backup_encryption_key that Frappe generates and stores in the same site_config.json; a System Manager can also retrieve it from the Download Backups page.

  1. Back up with files (bench --site <site> backup --with-files) and copy backups off the server.
  2. Store both encryption_key and backup_encryption_key separately from the backups, in a password manager or secret store. A backup that sits next to its key is not encrypted in any meaningful sense.
  3. Perform a restore drill on a clean server. Untested backups are a hope, not a control.
  4. Limit who can download backups from the desk; that page is effectively a full data export.

9. File uploads

  • Set Allowed File Extensions and Max File Size in System Settings to what the business actually uses.
  • Keep Allow Guests to Upload Files off unless a web form genuinely needs it.
  • Make attachments private by default; public files are reachable by anyone with the URL.
  • Scan uploaded files on the server if your users exchange documents with outside parties.

10. Audit trails and monitoring

Enable Track Changes on DocTypes that hold money, prices, bank details or approvals, so every edit produces a Version record showing who changed what. Review the Activity Log and Access Log for unusual logins, exports and report access. Ship nginx and application logs off the server, because an attacker with shell access will edit local logs first. Alerts on new System Manager assignments, new API keys and bulk exports catch most of the activity that matters.

Key takeaways

  • Patch Frappe, ERPNext and custom apps on a schedule, and watch the Frappe security advisories.
  • Use User Permissions and permission levels, not just role permissions, and keep System Manager to a named few.
  • Turn on 2FA, password policy, lockout and sensible session expiry in System Settings.
  • Whitelisting exposes a function; it never authorises the caller. Check permissions in every whitelisted method.
  • Leave Server Scripts disabled unless needed, and move stable logic into a versioned custom app.
  • Keep MariaDB and Redis off the internet, serve only HTTPS, and run the bench as a non-root user.
  • Encrypt and test backups, and store the encryption key away from them.

Frequently asked questions

Is ERPNext secure out of the box?

The framework has a solid permission model and reasonable defaults, but a self-hosted deployment is only as secure as its configuration and server. Two-factor authentication, password policy, User Permissions, firewalling of MariaDB and Redis, TLS and encrypted off-server backups all need to be set up deliberately.

What is the risk of frappe.whitelist(allow_guest=True)?

It makes the function callable over HTTP by anyone, without logging in. Combined with frappe.get_all or ignore_permissions=True, it can expose or modify data with no authentication at all. Guest methods should do one narrow thing, validate all input, return nothing about existing records, and be rate-limited.

Does @frappe.whitelist() check permissions?

Without allow_guest it requires a logged-in user, but it does not check whether that user may access the records the function reads or changes. Add frappe.only_for for role gates and doc.check_permission or frappe.has_permission with throw=True for record-level checks.

What happens if I lose the encryption_key in site_config.json?

Stored passwords and credentials in the database cannot be decrypted after a restore. Encrypted backups depend on a separate backup_encryption_key in the same file, so lose site_config.json and you can lose both. Keep copies of both keys in a password manager or secret store, separate from the backups themselves.

Should I enable Server Scripts in ERPNext?

Only if you need them. They are disabled by default from version 15 because anyone who can edit them can run server-side code. If you enable them, restrict who can edit them and move stable logic into a custom app under version control.

Working on something like this?

I build agentic AI systems, ERPNext automation and the security controls that keep them safe in production. Tell me what the workflow is and I will tell you honestly whether AI belongs in it.