cPanel MySQL Privileges: Give Users Only What They Need
Most of us set up a MySQL database in cPanel once, ticked "ALL PRIVILEGES" on the user, and never thought about it again. That's fine for a single WordPress install talking to its own database. It stops being fine the moment you add a reporting script, a backup job, a third-party integration, or a developer who only needs to read a couple of tables — because now that same all-powerful user is sitting in a config file or a cron script somewhere, and if it leaks, whoever has it can drop your tables just as easily as query them.
What "Privileges" Actually Control
In MySQL/MariaDB, every database user is granted a specific set of permissions on a specific database (or table). cPanel's MySQL Database Wizard hides most of this behind one checkbox — ALL PRIVILEGES — but under the hood there are more than a dozen individual grants, and most apps only ever touch four or five of them.
| Privilege | What it allows | Typical use case |
|---|---|---|
SELECT | Read rows | Reporting tools, read replicas, BI dashboards |
INSERT | Add rows | Form submissions, order creation |
UPDATE | Modify existing rows | Editing content, updating order status |
DELETE | Remove rows | Trashing posts, cleaning old sessions |
CREATE / ALTER / DROP | Change table structure or delete tables | App installers, migrations — almost never needed after setup |
LOCK TABLES, SHOW VIEW | Needed for consistent exports | mysqldump-based backup jobs |
INDEX | Add or remove indexes | Performance tuning scripts |
A normal WordPress or WooCommerce install genuinely needs most of these, since plugins create tables on activation. A backup script, a cron job, or an outside integration usually needs a fraction of that list — and giving it the rest is pure risk with no upside.
Symptom 1: "Command Denied to User" Errors
You've locked a user down (or someone else did) and now something breaks with an error like:
ERROR 1142 (42000): INSERT command denied to user 'myaccount_app'@'localhost' for table 'wp_options'
Cause: the user is missing exactly the privilege named in the error. This is actually the system working correctly — it's telling you precisely what to add.
Fix: grant the specific missing privilege rather than jumping straight to ALL PRIVILEGES:
GRANT INSERT ON myaccount_wp.* TO 'myaccount_app'@'localhost';
FLUSH PRIVILEGES;
If you're not on SSH, do the same thing from cPanel: Databases → MySQL Databases, scroll to Current Databases, find the user/database pair under Privileged Users, click the little pencil/edit icon, and tick just the boxes you need.
Symptom 2: One Leaked Credential, Full Database Wipe
Cause: the opposite problem — a user meant for one narrow job (a Grafana dashboard, a Zapier integration, a read-only API) was created with ALL PRIVILEGES because that's the default checkbox, and its password ended up in a script, a `.env` file, a GitHub repo, or a support ticket. Anyone with it can now DROP TABLE your entire site.
Fix: audit what you've already granted, then trim it down.
Step 1: See what a user can currently do
In phpMyAdmin (cPanel → Databases → phpMyAdmin), click User accounts in the top nav, then Edit privileges next to the user in question. Or run this directly if you have SQL access:
SHOW GRANTS FOR 'myaccount_reports'@'localhost';
You'll get back the full list, something like GRANT ALL PRIVILEGES ON `myaccount_wp`.* TO 'myaccount_reports'@'localhost'. If a user that only runs SELECT queries has that, it's over-provisioned.
Step 2: Revoke what it doesn't need
REVOKE INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX ON myaccount_wp.* FROM 'myaccount_reports'@'localhost';
FLUSH PRIVILEGES;
From cPanel's UI instead: on the same edit-privileges screen, untick everything except SELECT, then save. No SSH required.
Step 3: Build the right user for the job, not the default one
A few patterns cover almost every real case:
- Read-only reporting/dashboard user:
SELECTonly. It can never modify or delete data, even if the credential leaks. - Backup user (for a script running its own
mysqldump):SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER. This is enough for a consistent dump without giving it write access. - Plugin or integration user:
SELECT, INSERT, UPDATE, DELETEon the specific tables it touches — skipCREATE/DROP/ALTERunless it's an installer that manages its own schema. - Developer with occasional debugging access: a separate personal user (not the app's production credential) so you can revoke or rotate it individually when they roll off the project.
To scope a grant to a single table instead of the whole database, name it explicitly:
GRANT SELECT ON myaccount_wp.wp_posts TO 'myaccount_reports'@'localhost';
Why This Keeps Happening
It's not carelessness — it's the tooling nudging you the wrong way. cPanel's MySQL Database Wizard defaults to ALL PRIVILEGES because that's the fastest path to a working WordPress install, and most guides (including some of ours) tell you to tick it so setup isn't the thing that breaks. Nobody goes back afterward to ask whether a script added six months later actually needs full access. Privileges, once granted, just sit there until someone audits them — which is almost never.
Prevention
- One user per purpose. Don't reuse your WordPress app's database user for a backup cron job or a BI tool. Separate users mean you can revoke one without breaking the others.
- Default to the minimum, add privileges when something breaks. It's easier and safer to grant one missing permission after a clear "command denied" error than to discover an over-privileged user after an incident.
- Audit quarterly. Run
SHOW GRANTS FOR 'user'@'localhost';for every database user in cPanel's MySQL Databases list and confirm each one still matches what it's actually used for. - Rotate and remove. When a developer, plugin, or integration is no longer in use, delete its database user instead of leaving an unused credential with standing access.
- Never hand out your account's root-equivalent phpMyAdmin session for a task that only needs one table — create a scoped user instead.
None of this requires root or WHM access on shared hosting — cPanel's own MySQL Databases page has an edit-privileges checkbox list for exactly this. On a VPS where you manage MySQL/MariaDB directly, the GRANT/REVOKE statements above work the same way from the command line.
Frequently asked questions
Does WordPress itself need ALL PRIVILEGES?
Yes, practically speaking. WordPress core and most plugins create and alter tables on install/update, so its main database user needs CREATE, ALTER, and DROP alongside the usual SELECT/INSERT/UPDATE/DELETE. Reserve the trimmed-down, least-privilege users for secondary tools (backups, reporting, integrations) that talk to the same database.
I revoked a privilege and now my site is broken. How do I undo it?
Re-grant exactly what the error names, for example GRANT INSERT ON dbname.* TO 'user'@'localhost'; followed by FLUSH PRIVILEGES;. If you're not sure what changed, cPanel's MySQL Databases page shows the current checkbox state for that user and lets you re-tick ALL PRIVILEGES to restore the original access while you investigate.
Can I limit a user to specific tables instead of the whole database?
Yes. Instead of granting on dbname.*, grant on dbname.tablename, e.g. GRANT SELECT ON myaccount_wp.wp_posts TO 'user'@'localhost';. This isn't available as a checkbox in cPanel's UI, so it needs phpMyAdmin's SQL tab or SSH access to MySQL.
Does FLUSH PRIVILEGES matter if I make changes through cPanel or phpMyAdmin's UI?
No, it's only needed when you run GRANT or REVOKE directly via SQL, which sometimes doesn't reload the in-memory privilege cache immediately. cPanel's MySQL Databases page and phpMyAdmin's User accounts screen apply changes immediately through their own process.
Is a read-only database user actually safer if someone gets the password?
Meaningfully so. With SELECT-only access, a leaked credential lets someone read your data, which is still bad, but they cannot modify content, drop tables, or plant malicious rows. It turns a potential full-site wipe into a data-exposure incident, which is easier to contain and recover from.