Chapter 11 · Users, Roles, Authentication, Authorization, and Least Privilege
Accounts, Host Matching, Authentication Plugins, Password Policy, and Account Lifecycle
Build a precise MySQL account mental model: user-plus-host identity, caching_sha2_password authentication, password-policy detection, secure connection evidence, and safe account lifecycle operations.
Learning outcomes
A ServiceHub deployment is about to expose MySQL to a second application host. The team has a dangerous assumption: “the username identifies the user.” MySQL does not work that way. The account identity used for authentication is a user plus host match, and after that identity is accepted, authorization begins as a separate stage.
Explain MySQL account identity as the combination of user name and host match, and distinguish authentication from authorization.
Inspect the actual authentication plugin and account properties instead of assuming legacy mysql_native_password behavior.
Create, test, rotate, expire, lock, unlock, and drop a disposable account safely.
Detect whether the validate_password component is installed and interpret password-policy behavior conditionally.
Verify connection identity and TLS/session evidence before deciding that an authentication test proves what you think it proves.
Authentication is stage 1; privileges are stage 2
MySQL access control is easiest to reason about as two gates. Stage 1 decides whether a connection may become a session: the server finds the most appropriate account row for the supplied user name and client host, verifies the authentication method and credentials, and rejects locked accounts. Stage 2 checks each statement against the privileges of the authenticated identity and active roles.
That distinction explains two errors learners often mix together. “Access denied for user …” during connection is primarily an identity/authentication problem. “SELECT command denied …” after a successful login is an authorization problem. Fixing the first with GRANT ALL is conceptually wrong because privileges cannot repair a failed stage-1 identity match.
| Term | Meaning | What to observe |
|---|---|---|
| Account | A MySQL identity written as 'user'@'host' | The matching row in account metadata |
| Authentication plugin | The mechanism that verifies credentials | plugin in account metadata; client compatibility |
| Authorization | Privilege checking after the session exists | SHOW GRANTS, active roles, positive/negative statements |
| Effective account | The account whose privileges MySQL is checking | CURRENT_USER() |
| Client identity | The user/host information presented for the session | USER(), connection endpoint/transport |
Build the lab and inspect security defaults
-- Run as a local MySQL administrator in a disposable development instance.DROP DATABASE IF EXISTS servicehub_security_lab;CREATE DATABASE servicehub_security_lab CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;CREATE TABLE servicehub_security_lab.sites ( site_id BIGINT UNSIGNED PRIMARY KEY, site_name VARCHAR(80) NOT NULL) ENGINE=InnoDB;CREATE TABLE servicehub_security_lab.work_orders ( work_order_id BIGINT UNSIGNED PRIMARY KEY, site_id BIGINT UNSIGNED NOT NULL, status ENUM('OPEN','IN_PROGRESS','CLOSED') NOT NULL DEFAULT 'OPEN', summary VARCHAR(200) NOT NULL, private_note VARCHAR(300) NULL, CONSTRAINT fk_wo_site FOREIGN KEY (site_id) REFERENCES servicehub_security_lab.sites(site_id)) ENGINE=InnoDB;INSERT INTO servicehub_security_lab.sites VALUES (1,'North Plant'),(2,'Harbor Workshop');INSERT INTO servicehub_security_lab.work_orders (work_order_id,site_id,status,summary,private_note) VALUES (1001,1,'OPEN','Inspect pump seal','Vendor quote pending'), (1002,1,'IN_PROGRESS','Replace motor bearing','Night shift approved'), (1003,2,'CLOSED','Crane brake inspection','Internal audit complete');SELECT VERSION() AS server_version, @@hostname AS server_host, @@port AS tcp_port, @@authentication_policy AS authentication_policy;SELECT component_urnFROM mysql.componentORDER BY component_urn;SHOW VARIABLES LIKE 'validate_password.%';SHOW SESSION STATUS LIKE 'Ssl_cipher';On a normal 8.4 server whose first authentication-policy element is *, accounts created with IDENTIFIED BY use caching_sha2_password. Do not make the lesson depend on the password-validation component being present. If SHOW VARIABLES LIKE 'validate_password.%' returns no rows, the component is not loaded; if it returns variables, those values describe the active policy.
Create a deliberately narrow account and prove the host part
DROP USER IF EXISTS 'svc11_app'@'127.0.0.1';CREATE USER 'svc11_app'@'127.0.0.1' IDENTIFIED BY 'LabOnly-ChangeMe!2026';SHOW CREATE USER 'svc11_app'@'127.0.0.1';SHOW GRANTS FOR 'svc11_app'@'127.0.0.1';SELECT User,Host,plugin,account_locked,password_expiredFROM mysql.userWHERE User='svc11_app';A new account has no application privilege merely because it can authenticate. SHOW GRANTS should initially show only the account's baseline usage state. Connect explicitly over TCP so the lab is not confused by local socket/named-pipe behavior:
mysql --protocol=TCP -h 127.0.0.1 -P 3306 -u svc11_app -p-- Enter the disposable lab password when prompted; do not put it on the command line.SELECT USER(), CURRENT_USER(), CONNECTION_ID();SHOW SESSION STATUS LIKE 'Ssl_cipher';SELECT COUNT(*) FROM servicehub_security_lab.work_orders;The connection should succeed, while the final SELECT should fail because no SELECT privilege has been granted. This is the cleanest demonstration that authentication and authorization are separate.
Wrong repair: widen the account or grant everything
Do not “fix” an access denial by changing the host to % and granting ALL ON *.*. That changes two independent controls at once—where the account may authenticate from and what it may do after authentication—so you lose the evidence needed to diagnose the original problem.
GRANT SELECT,INSERT,UPDATEON servicehub_security_lab.work_ordersTO 'svc11_app'@'127.0.0.1';SHOW GRANTS FOR 'svc11_app'@'127.0.0.1';Reconnect in a fresh session and repeat the SELECT. It should now succeed. A destructive statement such as DROP TABLE servicehub_security_lab.work_orders should still fail. That negative test is as important as the positive test: least privilege means proving both required capability and forbidden capability.
Account lifecycle: rotate, expire, lock, and remove
ALTER USER 'svc11_app'@'127.0.0.1' IDENTIFIED BY 'LabOnly-Rotated!2026';ALTER USER 'svc11_app'@'127.0.0.1' PASSWORD EXPIRE INTERVAL 90 DAY;ALTER USER 'svc11_app'@'127.0.0.1' ACCOUNT LOCK;SHOW CREATE USER 'svc11_app'@'127.0.0.1';ALTER USER 'svc11_app'@'127.0.0.1' ACCOUNT UNLOCK;SHOW CREATE USER 'svc11_app'@'127.0.0.1';Password rotation clears the caching_sha2_password cache entry for the account. A first subsequent authentication may therefore need a secure transport or RSA-based password exchange depending on client/server configuration. In production, test connector compatibility before rotating thousands of accounts.
Locking is a deliberate offboarding or incident-control tool: it prevents successful connection without silently rewriting grants. Dropping is final cleanup. Prefer a lock-and-observe window for many real offboarding workflows before deletion, especially when you need evidence that no dependency still uses the identity.
Production judgment and monitoring
| Question | Good production evidence |
|---|---|
| Which identity authenticated? | USER(), CURRENT_USER(), account metadata, connection source |
| Which authentication method? | Account plugin, authentication_policy, connector capability |
| Was transport encrypted? | Ssl_cipher / TLS session status; Chapter 12 will harden this |
| Is password validation present? | Installed component plus its current variables; never infer from package name alone |
| Is the account stale? | Login/application telemetry, ownership records, rotation age, explicit offboarding status |
Everything mandatory in this lesson is available with Community Server on one local instance. Enterprise authentication plugins can integrate PAM/LDAP/other systems, but they are not required for the course lab.
Hands-on acceptance test
- Bootstrap the schema.
- Create
svc11_app@127.0.0.1with no privileges. - Prove login succeeds and table access fails.
- Grant only
SELECT, INSERT, UPDATEon the one table and prove the read succeeds. - Prove
DROP TABLEis still denied. - Rotate the password, lock the account, verify login denial, unlock it, and verify login again.
- Record
USER(),CURRENT_USER(),CURRENT_ROLE(), andSsl_cipherin your lab notes.
Knowledge check
- Why is
svc11_appdifferent fromsvc11_app@127.0.0.1? - What does a successful login prove?
- What is the default authentication method to expect on a normal MySQL 8.4 policy?
- Why can password rotation change first-login behavior?
- Why should
validate_passwordbe detected rather than assumed?
Reveal answers
- MySQL account identity includes both user and host; the host match participates in stage-1 connection verification.
- Only that stage-1 identity/authentication succeeded and the account is not locked; it does not prove authorization to application objects.
caching_sha2_password, unlessauthentication_policyis configured differently.- It clears the authentication cache entry, so a full authentication path is required again.
- It is a component whose installation depends on the deployment/package and can be absent.
Summary and bridge to Lesson 2
You now have a precise account lifecycle: identify the host-scoped account, authenticate with the current plugin, observe transport and effective identity, grant only the application capability, and manage rotation/lock/removal as explicit operations. Lesson 2 adds roles and dynamic privileges so those permissions can be composed and delegated without collapsing back into global administrator accounts.