Основи безпеки
Ваш API-ключ витік. Порядок дій
API-ключ витік, і ви не знаєте, що робити першим. Не кожен ключ у вашому фронтенді є витоком, а порядок важить більше за швидкість.

Коротко
- Якщо API-ключ витік, перша дія залежить від того, який це ключ. Публічні ключі живуть у вашому фронтенді, і робити з ними не треба нічого.
- Якщо це справжній секрет, зʼясуйте, чи може ключ витрачати гроші. Це єдина частина цієї історії, на якій цокає годинник.
- Зупинити ключ і замінити ключ у кожного постачальника є двома різними органами керування, і більшість дає вікно, у якому працюють обидва ключі.
- Потім прочитайте журнал постачальника за період, коли ключ був назовні, і не чіпайте історію Git, доки ключ не мертвий.
Повідомлення найчастіше приходить зі звіту сканера, від GitHub, який пише, що в одному з ваших репозиторіїв знайшовся секрет, або від когось, хто натиснув F12 на вашому сайті й надіслав скриншот. Хай яким шляхом воно до вас дійшло, тепер ви знаєте, що ваш API-ключ витік, і не знаєте, що робити першим.
Ось частина, яку посібник за посібником розуміє неправильно: вони дають одну відповідь на будь-який ключ, і ця відповідь завжди «ротуйте». Деякі з тих ключів мають бути публічними, і робити з ними не треба нічого. Серед решти одні тихо нарощують рахунок, поки ви це читаєте, а інші завдали всієї своєї шкоди в день деплою. Це три різні ситуації, і перший крок у кожній свій.
Спершу: чи цей ключ узагалі секрет?
Часто ні, і числа перекошені достатньо, щоб сказати це прямо.
Ми просканували 31 056 живих застосунків і прочитали JavaScript, який кожен із них віддає браузеру. Перевірка, що шукає облікові дані, змогла відповісти на всіх, і 1 332 повернулися щонайменше з одним ключем, вартим попередження. 1 142 з них були ключами Google API — єдиний тип у цій групі, який зазвичай лежить саме там, де йому й місце.
Ключ Google API на вебсторінці — це те, завдяки чому працює Google Maps. Ключ каже, який проєкт платить, а те, що не дає незнайомцеві витрачати ваші гроші, — це обмеження на ключі, а не його таємність. Власна настанова Google пряма щодо обох половин: "Unrestricted API keys are insecure" та "You are financially responsible for charges caused by abuse of unrestricted API keys." Полагодити такий ключ означає відкрити його в Cloud Console і додати два обмеження: одне називає ваш сайт, друге — ті API, які ви справді викликаєте. Сам ключ при цьому не змінюється ніколи.
Друга родина, якій місце в браузері, — публічні ключі. Ключ Stripe pk_live_ і
ключ Supabase sb_publishable_ або anon зроблені так, щоб їх читав кожен ваш
відвідувач.
Які ключі безпечні у вашому фронтенді
— це версія тієї ж перевірки на чотири символи.
Якщо не хочеться переглядати бандл руками, наше безкоштовне сканування читає ваш живий сайт, перелічує ключі, які видно ззовні, і каже, якого типу кожен із них: просканувати застосунок. Це займає близько 20 секунд і не потребує облікового запису.
Мій API-ключ витік. Що робити першим?
Зʼясуйте, чи є на ключі лічильник.
Ключ із лічильником тарифікує вам кожен запит. Ключ Google, ключ OpenAI, ключ
Anthropic і ключ AWS з неправильними правами позаду — усе це лічильники: чужий
трафік падає у ваш рахунок, у темпі, який обирає та людина і якого ви не
бачите. Ключ без лічильника натомість читає або пише ваші дані. Ключ Supabase
service_role — найчистіший приклад, і ніщо в ньому не коштує грошей за
годину.
Це одне питання і задає порядок.
Якщо на ключі є лічильник, зупиніть його зараз. Функція, яка ним користувалася, буде зламана, доки ви не викотите заміну, і це правильний обмін, бо рахунок — єдина частина цієї історії, яка продовжує рости, поки ви плануєте.
Якщо ключ читає дані й був опублікований у вашому бандлі, читання вже сталося. Копія є в кожного відвідувача, який завантажив сторінку, і в кожного робота, що шукав саме цей рядок. Нічого не погіршиться, поки ви встановлюєте правильний порядок, а зробити треба головне: не замкнути себе поза власним застосунком дорогою.
Що насправді вміє кожен тип ключа
| Ключ | Витрачає гроші | Читає ваші дані | Чи можна натомість обмежити? | Чи ламає застосунок заміна? |
|---|---|---|---|---|
Supabase sb_publishable_ або anon | Ні | Лише рядки, які дозволяють ваші правила | Його місце в браузері | Не стосується |
Stripe pk_live_ | Ні | Ні | Його місце в браузері | Не стосується |
Google AIza… | Так, у ваш рахунок Cloud | Ні | Так, і Google радить спершу спробувати це | Обмеження ні. Ротація може. |
| Ключ OpenAI або Anthropic | Так, у темпі незнайомця | Файли та асистенти в проєкті | Публічного варіанта не існує | Так, доки його не триматиме ваш сервер |
Stripe sk_live_ або rk_live_ | Так | Так, клієнти й записи про платежі | Обмежений ключ і є вузькою версією | Ні, є вікно на сім днів |
AWS AKIA… разом із секретною половиною | Так, зокрема обчислення погодинно | Ваші бакети S3 | Deactivate, і це оборотно | Ні, можна тримати два ключі водночас |
Supabase sb_secret_ або service_role | Ні | Кожен рядок у кожній таблиці | Ні | Так, на старішому проєкті |
Дві примітки до цієї таблиці, бо обидві змінюють наступний крок.
Ключ доступу AWS — це два рядки: ідентифікатор, що починається з AKIA, і
секретна половина, і AWS вимагає їх разом, щоб підписати запит. Тож рядок
AKIA сам по собі в бандлі використати не можна, а причина однаково вважати це
терміновим у тому, що обидві половини майже завжди вставляють разом. Відкрийте
файл і пошукайте другу, перш ніж вирішувати, у якому ви випадку.
Подробиці щодо кожного постачальника живуть у нього ж: ключ OpenAI, ключ Google API, секретний ключ Stripe і ключ service_role у Supabase, єдиний зі своєю власною процедурою.
Зупинити ключ і замінити ключ — це дві різні кнопки
Кожен постачальник у тій таблиці дає обидві, а паніка хапається за другу.
- Google. Обмеження ключа не змінює сам рядок, тож ваш застосунок працює далі. Їхня настанова з безпеки ставить це попереду всього: "First try to restrict your API keys", а ротація йде третім варіантом згори, на випадок, коли обмеження неможливе.
- Stripe. Expire key зупиняє ключ сам по собі, без жодної заміни. Їхня позиція щодо того, коли цим користуватися, без жодних півтонів: "If a restricted or secret API key is exposed or compromised, rotate it immediately even if you aren't sure anyone saw it." Вони ще й розводять два слова, які всі вживають як синоніми. Exposure — це коли ключ став видимим там, де не мав би. Compromise — це доказ, що хтось ним скористався.
- AWS. Deactivate — це той самий орган керування, і корисне в ньому те, що дію можна скасувати. AWS каже взагалі не видаляти старий ключ, поки ви ще перевіряєте: "we recommend that you do not immediately delete the first access key. Instead, choose Actions and then choose Deactivate." Якщо виявиться, що щось забуте ним досі користується, ви вмикаєте його назад.
- Supabase, на старішому проєкті. Перемикач один, і він накриває обидва
застарілі ключі.
anonіservice_roleпідписані одним секретом, тож вимикаючи той, якого ви позбуваєтеся, ви зупиняєте й той, яким користується ваш фронтенд. Чотири кроки, які цього уникають варто прочитати до того, як торкатися перемикача, а не після.
Прочитати журнал за період, коли ключ був назовні
Вікно відкривається деплоєм, який уперше випустив ключ, і закривається тоді, коли ви його зупинили. Саме за цим проміжком дат ви фільтруєте журнал постачальника.
Журнал веде кожен постачальник із таблиці. Stripe показує журнали запитів окремого ключа — через меню з трьома крапками поруч із ним на сторінці API Keys. AWS без жодного налаштування ставить дату останнього використання на кожен ключ доступу в консолі IAM і записує самі виклики в CloudTrail. Google веде графіки використання по ключах у Cloud Console, і це та сама перевірка, яку їхня настанова радить зробити, перш ніж щось міняти в ключі. Supabase зберігає журнали API та бази даних проєкту, а як звузити вікно на ключі Supabase каже, що саме в них шукати.
Ви шукаєте трафік, якого не можете пояснити: запити до таблиць чи ендпоїнтів, яких ваш застосунок ніколи не торкається, обсяг у години, коли ним ніхто не користувався, видалення, яких ніхто не робив. Далі подивіться на самі дані, бо звернення в підтримку про запис, що змінився сам собою, — це спосіб, у який більшість таких історій і викривають, і приходить воно за кілька тижнів.
Часто певності бути не може, і Stripe каже те саме про власне виявлення: "Stripe doesn't guarantee detection of all exposed or compromised keys." Тож «нічого в журналі» — це результат, і його варто записати разом із проміжком дат.
Ротація без зупинки застосунку
Троє з чотирьох постачальників вище дають вікно, у якому старий і новий ключі працюють обидва, і саме це рятує від простою.
Stripe. "When you rotate a key in the Dashboard, both the old and new keys work for up to 7 days." У діалозі є ще варіант Now, і їхня документація каже прямо: якщо обрати його, старий ключ буде видалено. Це кнопка для аварійного випадку з лічильником, а не для спокійної міграції. Їхня порада, коли відпускати старий, — це вимірювання, а не дата: перевірте його журнали запитів і дайте йому спливти лише після того, як обсяг запитів кілька годин чи днів тримався на нулі.
Google. Ротація створює новий ключ з усіма обмеженнями старого і, їхніми словами, "both the old and new key are accepted" протягом вікна, поки ви переводите застосунки. Якщо видалити старий зарано і щось зламається, дорога назад є: видалений ключ Google API можна відновити протягом 30 днів.
AWS. Послідовність є в їхній документації, і вона починається з нового ключа: створіть другий ключ доступу, поки перший ще активний, переведіть на нього кожен застосунок, перевірте дату останнього використання старого, деактивуйте його і лише тоді видаліть. Стеля, яку треба врахувати: користувач IAM може тримати щонайбільше два ключі доступу, тож третьому застосунку, який досі на старому ключі, подітися нікуди.
Supabase, на старішому проєкті. Вікна немає. Пряма ротація застарілих
ключів anon і service_role більше не підтримується, тож зробити один із них
недійсним — це міграція на нову пару ключів, і саме порядок чотирьох кроків
тримає застосунок на ногах.
Він досі у вашій історії Git
Так і є, і документація GitHub про це починається з того, що відправляє вас деінде.
Їхня сторінка про видалення чутливих даних повертає вас до ключа: "if the sensitive data you need to remove is a secret (e.g. password/token/credential), as is often the case, then as a first step you need to revoke and/or rotate that secret", а далі: "Once the secret is revoked or rotated, it can no longer be used for access, and that may be sufficient to solve your problem. Going through the extra steps to rewrite the history and remove the secret may not be warranted."
Причина, яку вони називають, — це те, куди force push не дістає. Після переписування історії старі коміти лишаються "In any clones or forks of your repository" і "Directly via their SHA-1 hashes in cached views on GitHub". Підтримка може прибрати кешовані перегляди й посилання в pull request, якщо попросити, і проводить власну межу: вона "will only assist in the removal of sensitive data in cases where we determine that the risk can't be mitigated by rotating affected credentials." Форк залишає копію собі в будь-якому разі, а контактів його власника GitHub вам не дасть.
Тож порядок, який вони описують, і є практичним: відкликати ключ, а потім вирішувати, чи варте переписування історії своїх побічних ефектів. Відкликання дістає до копій, куди переписування не дістає: зокрема до тієї, що в клоні, про який ви не знаєте, і до тієї, що на скриншоті, який хтось зберіг.
Як не пустити наступний у бандл
Ключ потрапляє у ваш бандл через деплой, а деплої не припиняються. Кожен із них — ще один шанс, що значення, яке ви вписали в панель секретів, опиниться у файлі, який завантажує браузер. Саме тому перевірка, зроблена минулого місяця, описує застосунок минулого місяця.
Наше безкоштовне сканування відповідає на питання, з яким ви сюди прийшли.
- девʼять перевірок на вашій живій URL-адресі, приблизно за 20 секунд
- кожен ключ, який видно ззовні, не просто знайдений, а віднесений до типу
- оцінка і знахідки, без облікового запису
Reeve Monitor проганяє ті самі перевірки знову, без ваших нагадувань.
- усі девʼять перевірок щогодини, для трьох застосунків
- повідомлення, коли результат змінюється, щоб ключ, який пішов із нічним деплоєм, не чекав, поки ви зазирнете
- чи відповідає застосунок, кожні 60 секунд
- щомісячний звіт про побачене
Reeve Care тримає копію вашої бази даних Supabase — для тих ключів, які вміють писати.
- зашифрована копія щоночі, у місці, куди ваш проєкт не дістає
- кожна копія перевіряється, перш ніж її зарахувати: ми рахуємо рядки в кожній таблиці
- відновлення в один клік, коли воно знадобиться
- а також ваші завантажені файли, щойно ви підключите облікові дані Storage
- усе, що робить Monitor
Ключ, який витік і вміє лише читати, залишає ваші дані там, де вони були. Ключ
service_role чи ключ AWS із правом запису здатен спорожнити таблицю, і жодна
ротація після цього не поверне рядки.
День, коли ШІ-агент видалив робочу базу даних
показує, як це виглядає зсередини.
Що зробити просто зараз
Що робити
- Визначте ключ, перш ніж його чіпати. Ключ Supabase
anonчиsb_publishable_і ключ Stripepk_live_лежать там, де мають, а з 1 332 застосунків, де ми знайшли ключ, вартий попередження, у 1 142 був ключ Google API, якому потрібне обмеження, а не ротація. - Запитайте, чи є на ньому лічильник. Якщо чужі запити падають у ваш рахунок, зупиніть ключ зараз і дайте функції зламатися.
- Користуйтеся органом керування, що зупиняє, так само як тим, що замінює: Expire key у Stripe, Deactivate в AWS, обмеження за сайтом і за API в Google.
- Потім замініть його в межах вікна постачальника. Stripe дає сім днів з обома живими ключами, Google приймає обидва, поки ви мігруєте, а AWS дозволяє тримати два ключі доступу водночас.
- Прочитайте журнал постачальника за період між деплоєм і зупинкою і запишіть, що знайшли, зокрема й «нічого».
- Історію Git залиште наостанок. Щойно ключ відкликано, власна настанова GitHub каже, що переписувати історію, можливо, узагалі не варто.
Якщо вам зручніше пройти все це списком, 10-хвилинний чеклист безпеки охоплює цю тему й решту того, що варто вимкнути у щойно запущеному застосунку.
Поширені запитання
Мій API-ключ витік. Що робити першим?
Зʼясуйте, чи може ключ витрачати гроші. Ключ, який тарифікує кожен запит, коштує вам грошей просто зараз, тож зупиніть його негайно і прийміть, що функція, яка ним користувалася, кілька хвилин не працюватиме. Ключ, який лише читає дані, уже прочитано, якщо він був публічним, тож спокійно робіть заміну в порядку, який не замкне вас поза власним застосунком.
Спершу відкликати чи ротувати?
Спершу відкликати, якщо на ключі є лічильник. Зупинити старий ключ і випустити новий у кожного постачальника є двома окремими органами керування, і діру закриває лише перший. Виняток становить ключ, який можна натомість обмежити: Google радить спробувати обмеження за сайтом і за API ще до того, як узагалі ротувати ключ Google API.
Як дізнатися, чи хтось користувався моїм ключем?
Звузьте вікно до періоду між деплоєм, який випустив ключ, і моментом, коли ви його зупинили, і прочитайте журнал постачальника за цей проміжок. Stripe показує журнали запитів по кожному ключу, AWS показує дату останнього використання на ключі доступу і записує виклики в CloudTrail, Google веде графіки використання по ключах, а Supabase зберігає журнали API та бази даних. Ви шукаєте запити до того, чого ваш застосунок ніколи не торкається, і трафік у години, коли ним ніхто не користувався. Часто певності бути не може, і це нормальний результат.
Чи зламає ротація ключа мій застосунок?
Зазвичай ні, якщо скористатися вікном, яке дає постачальник. Stripe тримає старий і новий ключ робочими разом до семи днів. Google випускає новий ключ з обмеженнями старого і приймає обидва, поки ви мігруєте. AWS дозволяє тримати два ключі доступу водночас, тож новий уже живий до того, як піде старий. Виняток становить старіший проєкт Supabase, де перемикач, що вимикає застарілі ключі, забирає з собою й ключ вашого фронтенду.
Ключ лежить у моїй історії Git. Чи досить видалити файл?
Ні, і переписування історії теж навряд чи є відповіддю. Документація GitHub каже спершу відкликати або ротувати секрет, і що після цього вплутуватися в переписування історії може бути вже недоцільно. Force push не дістає до копій у форках і клонах, ані до кешованих переглядів, доступних за хешем коміту. Зробити ключ непридатним покриває всі копії за один раз.