The default token has write access to most of the repository, and declaring a permissions block switches the job to exactly what is listed.
permissions:
contents: read
jobs:
deploy:
permissions:
contents: read
id-token: write # for OIDC
packages: write # to push an image
steps: [...]
Declaring it at the workflow level sets a floor and each job can narrow further, which is the arrangement that survives somebody adding a job. The id-token: write permission is the one people meet by error message rather than by reading, because without it the OIDC token request fails with a message about a missing token rather than a missing permission.