Three ways to authenticate a deploy
When your laptop or build server needs to act on a Google Cloud API (including the Play Console's upload endpoints), it can present three kinds of credentials:
| Kind | What it is | Who uses it |
|---|---|---|
| User account | You, the human, logged in via OAuth consent in a browser. | Interactive tools. Doesn't work for unattended deploys. |
| API key | A single long-lived string that authenticates but doesn't identify a specific actor. | Mostly deprecated for Google APIs. Still used for public-readable endpoints (Maps, YouTube Data). |
| Service account | A non-human identity with its own email address, its own set of keys, and its own IAM permissions. | Automated deploys, CI, LingCode. |
What a service account actually is
A service account is an identity in Google Cloud's Identity and Access Management (IAM) system, the same database that tracks your personal Google account. It has:
- An email address. Something like
lingcode-deploy@your-project-123.iam.gserviceaccount.com. This is how other systems refer to it when granting permissions. - One or more keys. Each key is a JSON file containing an RSA private key. You can rotate them independently — create a new one, deploy the new one, revoke the old one.
- IAM role bindings. Every permission it has comes from an explicit grant: "this service account has the `Release Manager` role on this one Play Console app." No implicit authority, no global scope.
- An audit trail. Every API call it makes shows up in Google Cloud audit logs, attributed to its email.
Why Google prefers service accounts to API keys
Four reasons — each solving a specific failure mode of long-lived bearer secrets:
1. Narrow scoping. An API key typically gives bearer rights to the whole project. A service account holds only the exact permissions you granted — "release to testing tracks" but not "change billing." If the JSON key leaks, the damage is bounded.
2. Attribution. Every action shows up in audit logs as lingcode-deploy@...iam.gserviceaccount.com, not some anonymous "API key user." When things go wrong, you can answer who did what, when — including which automation.
3. Short-lived credentials. The JSON key isn't what authenticates API calls directly. Instead, LingCode signs a JWT with the private key, exchanges it at oauth2.googleapis.com/token for an access token valid for an hour, and then uses that on API calls. Even if the access token leaks from a log, it expires quickly. (More on JWTs here.)
4. Rotation without coordination. Want to rotate? Generate a new key in Cloud Console → IAM → Service accounts → Keys. Deploy it to LingCode. Delete the old key. Any process still holding the old key fails — and nothing else breaks, because the service account itself didn't change. With an API key, rotation means coordinating with every system that stored the key.
How this binds to the Play Console app
Here's the subtle bit: a service account is a Google Cloud identity, but the Play Console is a separate system. Creating the account in Cloud Console isn't enough — you have to link it from Play Console's Setup → API access page, and grant it app-specific release permissions there.
That link is why LingCode's remote preflight catches the developerDoesNotOwnApplication error so cleanly: the JSON key is valid (Google Cloud authenticates it fine), but the Play Console side hasn't authorized this service account for this particular package. It's a two-factor authorization — Cloud proves identity, Play proves app-level permission — and both have to succeed.
A minimum-privilege setup
When you create the service account for LingCode, grant it exactly these permissions and no more:
- In Google Cloud IAM: no project-wide roles. The service account doesn't need to do anything outside Play Console.
- In Play Console → Setup → API access → Account permissions: Release apps to testing tracks. Add Release apps to production only when you're ready to push live.
- In Play Console → App permissions: only the specific apps this key should deploy.
This reduces blast radius: even if the JSON key leaks, the attacker can't edit billing, can't delete data, can't touch other apps. They can only do the one thing the account was made for — upload builds to the apps you explicitly linked.
The takeaway
A service account is a Google identity for machines. The one sentence to internalize: "An email address with cryptographic keys, narrow IAM permissions, and a full audit trail." Everything else — the JSON key, the JWT flow, the access-token exchange, the Play Console linking — is machinery for proving you hold that identity and that the identity is authorized to do the specific thing it's doing right now.