43 external services held standing access to our Slack, Drive, code, and contacts — signed in modals, forgotten in sprints. The sweep per platform, the grant inventory, export-over-connect, vendor funerals, and breach-news as a revocation trigger.
After the secrets rotation and the docs recon, a vendor breach headline asked the question I couldn't un-ask: who outside this building can read us right now? Not hackers — guests. The answer was forty-three external services with standing access to our words, files, code, or customers. Most I recognized. One was a "free CRM trial" from 2024 that had been syncing our contacts, continuously, for two years.
OAuth is a supply chain we sign ourselves
Every grant is a key handed to another company — to their security posture, their employees, their future acquirers, their worst day. When a vendor gets breached, the attacker doesn't need your password; they need the vendor's, and your grant does the rest.
The audit's greatest hits: a 2021 marketing integration still reading every public Slack channel; a Chrome extension with Drive access installed by someone who left in 2023; a deprecated GitHub App owned by a vendor that got acquired (our code's readers now include a company we never chose); and the two-year CRM sync.
The sweep
# GitHub: every app installed on the org, with its permissions and age
gh api orgs/$ORG/installations --jq '.installations[] |
"\(.app_slug)\t\(.created_at)\t\(.permissions | to_entries |
map("\(.key)=\(.value)") | join(","))"'
Then the human pages (no API tells the whole truth):
Slack → Manage apps (workspace admin)
Google Workspace → Security → Access and data controls → Third-party apps & services
Browser: every extension logged into a company account
Payments, DNS, registrar, monitoring dashboards — the boring crown
The inventory (one doc, five columns that matter)
| vendor | surface | scopes | data reach | funeral plan |
|-------------|---------|------------------|------------|-----------------------|
| backup tool | drive | read | HIGH | revoke on migration |
| standup bot | slack | channels:history | MED | downgrade → post-only |
| crm-trial | contacts| read+sync | HIGH | KILL (done, RIP) |
Owner + "since" column too. You cannot revoke what you haven't listed, and a row with no owner is a grant nobody will ever bury.
Export over connect
# the analytics tool gets a monthly batch, not a standing read:
psql -U app app_db -c "\copy (select * from events) to stdout csv" \
| gzip > events-$(date +%F).csv.gz
# upload one-way; the connection you never open can't leak
Downgrades are free — most vendors don't notice, which tells you how little they needed the scope.
Funerals and triggers
- Vendors attend the same funeral as projects. Tool dies in our stack → its grant dies in the same sitting. A grant is just a project that lives in someone else's building.
- Breach news is a revocation trigger, not a newsletter. Vendor in headlines → grant reviewed that day.
- The re-audit is scheduled, not remembered:
0 9 1 * * echo "open the four connected-apps pages. count the keys."
The honest part
Some grants are load-bearing. SSO, backups, the payment processor. Tier vendors by data reach, review in that order, forever. Triage, not purity.
Small teams can't audit 43 vendors' internals. So minimize standing access instead — it requires no vendor questionnaire. You can't inspect their locks; you can stop handing out keys.
The walls still matter. None of this replaces no-public-IP defaults, own kernel per Cube, scoped secrets. Vendor hygiene shrinks the surface of trust; architecture shrinks the surface of reach. Both, always.
Forty-three is a normal number
We weren't careless. We were modern. Every grant was reasonable on the day it was signed; the risk was the accumulation of reasonable decisions, each still active, none still reviewed.
Every OAuth grant is a key you gave to another company's worst day. Count your keys.
Top comments (0)