{"course":{"slug":"agents-and-automation","title":"Agents and Automation","url":"https://addaly.com/learn/agents-and-automation","level":"intermediate","track":"build"},"lesson":{"slug":"least-privilege-for-agents","title":"Give it the smallest key that works","url":"https://addaly.com/learn/agents-and-automation/least-privilege-for-agents","position":40,"of":72,"minutes":8,"answer":"An agent can do exactly what its credentials allow; scope the database role, the token, the channel and the network egress before you touch the prompt.","keyIdea":"An agent can do exactly what its credentials allow; scope the database role, the token, the channel and the network egress before you touch the prompt."},"body":{"format":"markdown","text":"## Write down the union\n\nAn agent's power is not what its prompt says. It is the union of the permissions of every credential its tools hold. Write that union out as a list of plain sentences (*can read all customer rows including password hashes; can send email as support@; can reach any host on the internet*) and the design review usually finishes itself.\n\n## A read-only database role, concretely\n\n```sql\nCREATE ROLE agent_ro LOGIN PASSWORD 'use-a-generated-one';\nGRANT CONNECT ON DATABASE app TO agent_ro;\nGRANT USAGE ON SCHEMA public TO agent_ro;\nGRANT SELECT ON ALL TABLES IN SCHEMA public TO agent_ro;\nALTER DEFAULT PRIVILEGES IN SCHEMA public\n  GRANT SELECT ON TABLES TO agent_ro;\nALTER ROLE agent_ro SET statement_timeout = '10s';\n```\n\nTwo lines there are the ones people miss.\n\n**The default privileges line.** Without it, `agent_ro` can read today's tables and not tomorrow's. Six weeks later somebody adds a table, the agent gets a permission error, and the quickest fix (granting a broader role) undoes the whole design. Set it now.\n\n**The statement timeout.** A model can write a query with an accidental cross join. Ten seconds later it is cancelled, instead of holding your database down for four minutes.\n\nAnd note what `SELECT ON ALL TABLES` still includes: password hashes, personal data, other customers' rows. Least privilege on the verb is not least privilege on the data. Give the agent **views** that expose only the columns it needs, and grant on the views.\n\n> Figure: The union of what its credentials allow. A person's login, borrowed: Every table that person can read, password hashes included; Sends as them, so the audit log names a human for four hundred rows changed at 2am; Payroll, because the person who wrote the script sits in finance; Revoked only by disabling their account, so nobody revokes it; Still running after they change teams, and after they leave. A service account per automation, per environment: SELECT granted on three views, not on all tables; Its own mailbox with an empty contacts list; A fine-grained token scoped to one repository and two permissions; statement_timeout of ten seconds, so a cross join is cancelled not endured; Default privileges set, so tomorrow's tables are covered without widening the role. An agent's power is not what its prompt says. Write the union out as plain sentences and the design review usually finishes itself.\n\n## Scoped tokens and separate accounts\n\n- A GitHub **fine-grained personal access token**, scoped to one repository with the two permissions it needs, rather than a classic token carrying full repository scope.\n- A bot user in **one** channel.\n- A dedicated email account with an empty contacts list, not a person's mailbox.\n- A cloud role with the four API actions it uses, not an administrator policy.\n\nNever the founder's personal credential. Beyond the obvious, it makes the audit log lie: every action the agent takes is attributed to a person, and six months later nobody can tell which is which.\n\n## Network egress\n\nDeny by default; allowlist the hosts the tools need. This is one control doing two jobs: it stops a compromised dependency phoning home, and it is the same wall that stops the exfiltration in the previous lesson.\n\n## Spend limits at the provider\n\nSet the cap in the provider's own dashboard, not only in your counter. Your counter is code, and code has bugs in it; the provider's limit holds even when your loop does not. Add an alert at half the cap, because you want to hear about it during the day.\n\n## Short lives, and rotation\n\nIf a run takes ten minutes, a credential valid for an hour is generous. Prefer short-lived tokens issued per run. Rotate the long-lived ones on a schedule you actually keep, and revoke whatever an abandoned project left behind.\n\n## The question to end a design review with\n\n*If this agent were fully controlled by whoever wrote the last document it read, what could it do?*\n\nThe answer is your blast radius. Not the worst case you imagine: the list of permissions you just wrote down."},"check":{"question":"An analytics agent is given a Postgres role with `GRANT SELECT ON ALL TABLES IN SCHEMA public`. Two months later it starts failing with permission errors on a newly added `subscriptions` table. What is the correct fix?","options":[{"text":"Grant the agent a broader read-everything role so this cannot recur.","correct":false,"tempting":"Solves recurrence by removing the boundary. It is the fix teams reach for under time pressure, and the reason least privilege erodes."},{"text":"Have the agent create the missing grants itself when it hits a permission error.","correct":false,"tempting":"Gives the agent the power to widen its own permissions, which makes the boundary decorative."},{"text":"Re-run the grant for the new table and add `ALTER DEFAULT PRIVILEGES ... GRANT SELECT ON TABLES` so future tables are covered without widening the role.","correct":true},{"text":"Move the agent to the application's existing connection, which already has the access it needs.","correct":false,"tempting":"Trades a scoped read-only role for a read-write application credential, which is a far larger blast radius sold as convenience."}]},"prev":{"slug":"browser-agents-and-the-open-web","title":"The web is the most hostile input there is","url":"https://addaly.com/learn/agents-and-automation/browser-agents-and-the-open-web"},"next":{"slug":"data-boundaries-and-redaction","title":"What leaves the building","url":"https://addaly.com/learn/agents-and-automation/data-boundaries-and-redaction"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Give it the smallest key that works', in Agents and Automation. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/agents-and-automation/least-privilege-for-agents (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Give it the smallest key that works","courseTitle":"Agents and Automation","module":{"index":5,"slug":"safety-and-blast-radius","title":"Permissions, injection and blast radius"},"answer":null,"keyIdea":"An agent can do exactly what its credentials allow; scope the database role, the token, the channel and the network egress before you touch the prompt.","hero":{"kind":"figure","figure":{"t":"figure","label":"Figure","figure":{"left":{"title":"A person’s login, borrowed","items":["Every table that person can read, password hashes included","Sends as them, so the audit log names a human for four hundred rows changed at 2am","Payroll, because the person who wrote the script sits in finance","Revoked only by disabling their account, so nobody revokes it","Still running after they change teams, and after they leave"]},"type":"compare","right":{"title":"A service account per automation, per environment","items":["SELECT granted on three views, not on all tables","Its own mailbox with an empty contacts list","A fine-grained token scoped to one repository and two permissions","statement_timeout of ten seconds, so a cross join is cancelled not endured","Default privileges set, so tomorrow’s tables are covered without widening the role"]},"title":"The union of what its credentials allow","caption":"An agent’s power is not what its prompt says. Write the union out as plain sentences and the design review usually finishes itself."},"alt":"Figure: The union of what its credentials allow. A person’s login, borrowed: Every table that person can read, password hashes included; Sends as them, so the audit log names a human for four hundred rows changed at 2am; Payroll, because the person who wrote the script sits in finance; Revoked only by disabling their account, so nobody revokes it; Still running after they change teams, and after they leave. A service account per automation, per environment: SELECT granted on three views, not on all tables; Its own mailbox with an empty contacts list; A fine-grained token scoped to one repository and two permissions; statement_timeout of ten seconds, so a cross join is cancelled not endured; Default privileges set, so tomorrow’s tables are covered without widening the role. An agent’s power is not what its prompt says. Write the union out as plain sentences and the design review usually finishes itself."}},"blocks":[{"t":"h2","id":"write-down-the-union","runs":[{"text":"Write down the union"}]},{"t":"p","runs":[{"text":"An agent’s power is not what its prompt says. It is the union of the permissions of every credential its tools hold. Write that union out as a list of plain sentences ("},{"text":"can read all customer rows including password hashes; can send email as support@; can reach any host on the internet","i":true},{"text":") and the design review usually finishes itself."}]},{"t":"h2","id":"a-read-only-database-role-concretely","runs":[{"text":"A read-only database role, concretely"}]},{"t":"code","lang":"sql","label":"SQL","text":"CREATE ROLE agent_ro LOGIN PASSWORD 'use-a-generated-one';\nGRANT CONNECT ON DATABASE app TO agent_ro;\nGRANT USAGE ON SCHEMA public TO agent_ro;\nGRANT SELECT ON ALL TABLES IN SCHEMA public TO agent_ro;\nALTER DEFAULT PRIVILEGES IN SCHEMA public\n  GRANT SELECT ON TABLES TO agent_ro;\nALTER ROLE agent_ro SET statement_timeout = '10s';"},{"t":"p","runs":[{"text":"Two lines there are the ones people miss."}]},{"t":"p","runs":[{"text":"The default privileges line.","b":true},{"text":" Without it, "},{"text":"agent_ro","code":true},{"text":" can read today’s tables and not tomorrow’s. Six weeks later somebody adds a table, the agent gets a permission error, and the quickest fix (granting a broader role) undoes the whole design. Set it now."}]},{"t":"p","runs":[{"text":"The statement timeout.","b":true},{"text":" A model can write a query with an accidental cross join. Ten seconds later it is cancelled, instead of holding your database down for four minutes."}]},{"t":"p","runs":[{"text":"And note what "},{"text":"SELECT ON ALL TABLES","code":true},{"text":" still includes: password hashes, personal data, other customers’ rows. Least privilege on the verb is not least privilege on the data. Give the agent "},{"text":"views","b":true},{"text":" that expose only the columns it needs, and grant on the views."}]},{"t":"h2","id":"scoped-tokens-and-separate-accounts","runs":[{"text":"Scoped tokens and separate accounts"}]},{"t":"ul","items":[[{"text":"A GitHub "},{"text":"fine-grained personal access token","b":true},{"text":", scoped to one repository with the two permissions it needs, rather than a classic token carrying full repository scope."}],[{"text":"A bot user in "},{"text":"one","b":true},{"text":" channel."}],[{"text":"A dedicated email account with an empty contacts list, not a person’s mailbox."}],[{"text":"A cloud role with the four API actions it uses, not an administrator policy."}]]},{"t":"p","runs":[{"text":"Never the founder’s personal credential. Beyond the obvious, it makes the audit log lie: every action the agent takes is attributed to a person, and six months later nobody can tell which is which."}]},{"t":"h2","id":"network-egress","runs":[{"text":"Network egress"}]},{"t":"p","runs":[{"text":"Deny by default; allowlist the hosts the tools need. This is one control doing two jobs: it stops a compromised dependency phoning home, and it is the same wall that stops the exfiltration in the previous lesson."}]},{"t":"h2","id":"spend-limits-at-the-provider","runs":[{"text":"Spend limits at the provider"}]},{"t":"p","runs":[{"text":"Set the cap in the provider’s own dashboard, not only in your counter. Your counter is code, and code has bugs in it; the provider’s limit holds even when your loop does not. Add an alert at half the cap, because you want to hear about it during the day."}]},{"t":"h2","id":"short-lives-and-rotation","runs":[{"text":"Short lives, and rotation"}]},{"t":"p","runs":[{"text":"If a run takes ten minutes, a credential valid for an hour is generous. Prefer short-lived tokens issued per run. Rotate the long-lived ones on a schedule you actually keep, and revoke whatever an abandoned project left behind."}]},{"t":"h2","id":"the-question-to-end-a-design-review-with","runs":[{"text":"The question to end a design review with"}]},{"t":"p","runs":[{"text":"If this agent were fully controlled by whoever wrote the last document it read, what could it do?","i":true}]},{"t":"p","runs":[{"text":"The answer is your blast radius. Not the worst case you imagine: the list of permissions you just wrote down."}]}],"check":{"question":[{"text":"An analytics agent is given a Postgres role with "},{"text":"GRANT SELECT ON ALL TABLES IN SCHEMA public","code":true},{"text":". Two months later it starts failing with permission errors on a newly added "},{"text":"subscriptions","code":true},{"text":" table. What is the correct fix?"}],"options":[{"text":[{"text":"Grant the agent a broader read-everything role so this cannot recur."}],"correct":false,"tempting":[{"text":"Solves recurrence by removing the boundary. It is the fix teams reach for under time pressure, and the reason least privilege erodes."}]},{"text":[{"text":"Have the agent create the missing grants itself when it hits a permission error."}],"correct":false,"tempting":[{"text":"Gives the agent the power to widen its own permissions, which makes the boundary decorative."}]},{"text":[{"text":"Re-run the grant for the new table and add "},{"text":"ALTER DEFAULT PRIVILEGES ... GRANT SELECT ON TABLES","code":true},{"text":" so future tables are covered without widening the role."}],"correct":true,"tempting":null},{"text":[{"text":"Move the agent to the application’s existing connection, which already has the access it needs."}],"correct":false,"tempting":[{"text":"Trades a scoped read-only role for a read-write application credential, which is a far larger blast radius sold as convenience."}]}],"order":[1,0,3,2],"why":null},"prev":{"slug":"browser-agents-and-the-open-web","title":"The web is the most hostile input there is","minutes":10},"next":{"slug":"data-boundaries-and-redaction","title":"What leaves the building","minutes":9}}}