The error didn’t even try to be helpful: FAILED_PRECONDITION: Key creation is not allowed on this service account. No link, no suggestion, no explanation of which policy was doing this or why. I’d asked Google Cloud for a JSON key file for a brand-new service account on a brand-new project I owned outright, and it said no. This is the story of the two hours I spent trying to fight that, and the one afternoon I saved myself by giving up and doing it a different way instead.
Why I wanted a service account key in the first place
Every Google Cloud tutorial that touches server-to-server automation reaches for the same pattern: create a service account, generate a key, drop the JSON file next to your script, point GOOGLE_APPLICATION_CREDENTIALS at it, done. It’s the default advice for a reason — it’s non-interactive, it doesn’t expire the way OAuth tokens can feel like they will, and there’s no browser consent screen involved. For a script that was going to call the Text-to-Speech API on a timer with nobody watching, it looked like exactly the right tool.
So I created the service account. I opened the “Keys” tab. I clicked “Add key” → “Create new key” → JSON, the same three clicks I’d done on other projects without thinking about it. And instead of a download prompt, I got that error.
The part where I assumed it was my mistake
My first instinct wasn’t “this is a policy,” it was “I did something wrong.” I checked the IAM roles on the service account — fine. I checked billing was actually enabled — it was. I checked whether the API itself needed to be enabled first — it was already on. I regenerated the service account from scratch in case the first one was somehow corrupted. Same error, verbatim, every time.
It took an embarrassingly long search before I found the actual cause: an organization policy constraint called iam.disableServiceAccountKeyCreation. If your Google account sits inside a Google Workspace or Cloud org (mine did, through a domain I’d set up long before this project), that constraint is very often on by default, inherited from the org level, and it blocks key creation across every project underneath it — including ones you personally own and administer. You don’t get an email about it. You don’t see a warning when you create the project. You just hit the wall the first time you actually try to generate a key.
The two hours I don’t recommend
Knowing the name of the constraint, my next move was to try to turn it off. This is the part I’d skip if I were doing it again. Changing an org policy constraint requires the orgpolicy.policyAdmins role at the organization or folder level — not the project level, no matter how much of an owner you are on the project itself. I went looking for that permission in the Google Cloud console, then in the Google Workspace admin console, then back again, trying to figure out which of my own logins was supposed to have organization-level admin rights over a domain I’d registered years earlier for an unrelated reason. I found a path that technically could have worked — reassign myself an org policy admin role, override the constraint at the project level, generate the key, then presumably leave that override in place forever as a small permanent hole in an otherwise sane default.
I got about ten minutes from actually doing that before I stopped and asked a more useful question: did I actually need a service account key, or did I need “a script that authenticates without a human present,” and were those actually the same thing?
They weren’t the same thing
They’re not, and the gap between them is OAuth’s installed-app flow. It’s a pattern most people associate with interactive login — a browser window pops up, you click “Allow,” and a web app gets a token. What’s less obvious is that the exact same flow works fine for a personal automation script, and it sidesteps the org policy entirely, because it authenticates as you, using your own account’s ordinary permissions, not as a service account subject to org-level key restrictions.
The shape of it in Python is small enough that it fits in one paste:
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
import pickle
from pathlib import Path
SCOPES = ["https://www.googleapis.com/auth/cloud-platform"]
TOKEN_PATH = Path("token.pickle")
def get_credentials():
creds = None
if TOKEN_PATH.exists():
creds = pickle.loads(TOKEN_PATH.read_bytes())
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file("client_secret.json", SCOPES)
creds = flow.run_local_server(port=0)
TOKEN_PATH.write_bytes(pickle.dumps(creds))
return creds
You run this once, a browser tab opens, you click through your own account’s consent screen, and a refresh token gets cached to disk. Every run after that is silent — no browser, no human, the refresh token renews the access token automatically for as long as you keep using it. Functionally, for a solo automation script, this behaves exactly like the service account key I was originally trying to get: something a scheduled task can use without anyone sitting at the keyboard. It just doesn’t touch the constraint that was blocking me, because it was never a service account to begin with.
What I’d tell myself before starting
The lesson isn’t “OAuth is better than service accounts” in general — for actual server-to-server systems, especially ones other people operate, service accounts with scoped IAM roles are still the right default, and disabling key creation org-wide is a genuinely reasonable security policy, not a bug. The lesson is narrower: if a script is going to run under your own identity, on your own machine, for your own project, and you hit a wall trying to mint a service account key, check whether you actually need the service account at all before you go looking for a way around an org policy that’s very likely there on purpose. The workaround that respects the policy is usually less work than the one that fights it.