Authentication and least privilege
Your app should not be able to do more to the database than it actually needs to.
Why the app user shouldn't be an admin
It's tempting to connect as the same superuser you use to run migrations, one set of credentials, one less thing to manage. The problem shows up the first time there's a bug or an injection vulnerability, an app connecting as superuser can drop tables, read other databases on the same server, and change permissions, not just misbehave inside its own data.
Warning
If your app's database credentials could run DROP DATABASE, that's not a hypothetical risk, that's the blast radius of your next bug.
Splitting roles by job
A reasonable minimum split for most production systems:
Role | Can do | Used by |
|---|
App | SELECT, INSERT, UPDATE, DELETE on app tables only | The application itself |
Migration | Everything the app role has, plus schema changes | CI/CD pipeline, not the running app |
Read only | SELECT only | Analytics, reporting tools, read replicas exposed to other teams |
Admin | Full control | A human, used rarely, ideally with extra auth like MFA |
Setting this up
Postgres
CREATE ROLE app_user LOGIN PASSWORD '...';
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
CREATE ROLE readonly_user LOGIN PASSWORD '...';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_user;
MySQL
CREATE USER 'app_user'@'%' IDENTIFIED BY '...';
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'app_user'@'%';
CREATE USER 'readonly_user'@'%' IDENTIFIED BY '...';
GRANT SELECT ON mydb.* TO 'readonly_user'@'%';
Default privileges catch you later
Granting privileges on today's tables doesn't cover tables created tomorrow. It's easy to grant access carefully at setup, then six months later add a new table that's wide open because nobody remembered to grant it explicitly.
-- Postgres: make sure future tables inherit the same grants
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
Note
Worth a recurring check, not just a one time setup: does every table actually have the permissions you think it has, or just the ones it had when it was created.
Row level security
Beyond table-level grants, some databases can restrict access to specific rows based on who's asking, not just what table they're touching.
-- Postgres: a tenant can only see their own rows
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant_id')::int);
Useful for multi-tenant systems where a bug in application logic shouldn't be able to leak one customer's data to another, the database enforces it even if the app forgets to filter.
Service accounts vs human accounts
An app's database credentials and a person's personal login should never be the same thing. Service accounts should be scoped tightly to what the app needs, human accounts should be individually identifiable, so access can be traced back to a specific person, not a shared password everyone knows.
MFA for admin access
The admin role, the one that can do real damage, is exactly the one worth protecting with more than a password. Most managed database providers support MFA on the console or CLI access, worth turning on even if it adds a small amount of friction.
Network level access control
Least privilege isn't only about what a role can query, it's also about where connections can come from at all.
Control | What it limits |
|---|
VPC / private network | The database isn't reachable from the public internet at all |
Firewall / security group rules | Only specific known IP ranges can even attempt to connect |
IP allowlisting | A manual list of exactly which sources are trusted |
A perfectly scoped role still doesn't help if the database is reachable from anywhere on the internet in the first place.
Auditing who changed what permission
Grants and revokes should leave a trail. If a role suddenly has more access than it did last month, you want to be able to answer who changed that and when, not just notice it happened.
Temporary, just in time access
Instead of a human having standing admin access all the time, some setups grant elevated access only for a limited window, requested and approved for a specific task, and automatically revoked afterward. Shrinks the window where a compromised account could do real damage.
Revoking access when someone leaves
A surprisingly common gap, a former employee's or contractor's database credentials quietly still work months after they've left. Tie credential revocation to your offboarding process directly, not to someone remembering to do it separately.
Database level vs application level authorization
Application code often has its own permission checks, "can this user see this order." Those checks are only a backstop if the database itself would also refuse an unauthorized query, relying on the app to always remember to check is a single point of failure, row level security or scoped roles are the actual enforcement layer.
Certificate based authentication
Instead of, or in addition to, a password, some setups authenticate connections using client certificates. Harder to phish or accidentally leak in a log line than a password, but adds real overhead to issue, rotate, and distribute the certificates.
Reviewing permissions on a schedule
Access accumulates over time, someone gets temporary write access for a migration and it's never revoked, a role gets a broad grant "just to get something working" and nobody circles back. A periodic review, even just quarterly, catches drift that no single change would have flagged on its own.