Перейти до вмісту

Ви увійшли як

Вхід у Curious Dev Learn

Увійдіть, щоб перевіряти лаби, зберігати прогрес і отримати сертифікат. Уроки відкриті й без акаунта.

Модуль 032 год 15 хвЛаба

Конфігурація, секрети та ідентичність сервісу

Винесіть секрет із коду в Secret Manager, читайте його під час виконання зі змонтованого тому й запускайте Sniplink під окремим сервіс-акаунтом, який може читати рівно один секрет.

У цьому лозі накладаються дві окремі помилки. Перша: секрет живе у файлі, який версіонують, копіюють і форкають, як будь-який інший вихідний код. Видалити його наступним комітом нічого не дає: ключ лишається в історії git, у кожному клоні та у форку, який тепер належить незнайомцю. Єдиний вихід полягає в ротації ключа, і тут команда з’ясовує, що не має жодного уявлення, як це робиться, бо ніхто ніколи цього не робив.

Друга помилка тихіша й гірша. Сервіс працював під дефолтним compute сервіс-акаунтом, який у багатьох проєктах має roles/editor на все. Будь-хто, хто отримає виконання коду в цьому контейнері або вкраде з нього токен, може видаляти бази даних, читати кожен бакет і створювати VM для майнінгу. Злитий API-ключ був одним секретом. Ідентичність сервісу була ключами від усієї будівлі.

Цей модуль відповідає на два питання. Де має жити секрет, щоб ніколи не потрапити в образ, env-файли чи git і щоб його можна було ротувати без деплою? І під якою ідентичністю має працювати Sniplink, щоб компрометація сервісу означала компрометацію майже нічого? Наприкінці Sniplink читатиме лабораторний ключ із Secret Manager під час виконання, працюватиме як sniplink-runtime з правом читати лише цей один секрет, і ви доведете обидва факти ззовні.

Terminal window
export PROJECT_ID=your-course-project-id
export REGION=europe-west1

Конфігурація в .NET і де не місце секретам

Section titled “Конфігурація в .NET і де не місце секретам”

ASP.NET Core збирає один IConfiguration зі стеку джерел. Зі стандартним WebApplication.CreateBuilder порядок приблизно такий: appsettings.json, appsettings.{Environment}.json, user secrets (лише в Development), змінні середовища, потім аргументи командного рядка. Пізніші джерела перекривають попередні, а __ в імені змінної середовища відповідає : у ключі, тож LabSecret__Path стає LabSecret:Path.

Ця модель добре підходить для конфігурації: рівні логування, feature flags, URL інших сервісів, таймаути. Тобто для значень, які нормально побачити в pull request. Секрет є іншою річчю. Моє робоче визначення: значення, яке дає доступ до чогось, тож будь-хто, хто його прочитав, може діяти від вашого імені. Під нього підпадають API-ключі, паролі до баз даних, ключі підпису, секрети вебхуків і client secret для OAuth. Рядок підключення з паролем усередині є секретом, хоч і виглядає як конфігурація.

Ось чому кожна типова схованка не працює:

  • git (appsettings.json, випадково закомічений .env). Історія вічна, форки публічні, а сканери секретів, які запускають і захисники, і зловмисники, знаходять ключі за лічені хвилини після push.
  • Образ контейнера. Усе, що ви додаєте через COPY або запікаєте як ENV, може прочитати будь-хто, хто здатен зробити pull образу, а шари зберігають навіть видалені файли. Права на registry стають правами на ваші секрети, а вони зазвичай значно ширші.
  • Env-файли та звичайні змінні середовища в конфігурації деплою. Краще, ніж образ, але значення тепер лежить у вашому IaC-коді або змінних CI, його видно в консолі Cloud Run будь-кому з доступом viewer, і його виведе перший-ліпший debug-endpoint, що дампить оточення.
  • User secrets (dotnet user-secrets). Нормально для локальної розробки, саме для неї вони й існують. Це JSON-файл у вашій домашній директорії, а не продакшн-сховище.

Натомість вам потрібне сховище, яке зберігає секрет зашифрованим, контролює, хто може читати кожен окремий секрет, логує кожен доступ, тримає версії, щоб ротація була повноцінною операцією, і передає значення запущеному сервісу так, щоб воно не проходило ні через git, ні через образ, ні через конфігурацію деплою. У Google Cloud таким сховищем є Secret Manager.

Secret Manager у п’яти поняттях

Section titled “Secret Manager у п’яти поняттях”
  1. Секрет. Іменований контейнер, наприклад sniplink-lab-key. Він містить метадані (лейбли, політику реплікації, IAM-політику), але сам по собі значення не має.

  2. Версія. Власне байти. Версії нумеруються 1, 2, 3 і незмінні: значення ніколи не редагують, а додають нову версію. Кожна версія має стан ENABLED, DISABLED або DESTROYED. Вимкнену можна ввімкнути знову, а знищену ні.

  3. latest. Аліас, що вказує на останню створену версію. Споживачі, які запитують latest, автоматично підхоплюють ротації. Споживачі, які запитують 3, лишаються на 3, доки хтось не змінить їхню конфігурацію.

  4. IAM на рівні секрету. roles/secretmanager.secretAccessor можна видати на проєкт (усі секрети) або на один секрет. У цьому курсі прив’язка завжди на один секрет. Роль accessor дозволяє читати значення, але не дає переглядати інші секрети, змінювати IAM чи додавати версії.

  5. Реплікація. Автоматична реплікація дозволяє Google самому обирати, де зберігати зашифровані дані. Реплікація, якою керує користувач, прив’язує їх до вибраних вами регіонів, і це важливо, якщо у вас є вимоги до резидентності даних. Для Sniplink правильний вибір: автоматична.

Вартість: ви платите за кожну активну версію секрету на місяць і за кожну операцію доступу, є невеликий безкоштовний ліміт. У цьому модулі рахунок становитиме щонайбільше кілька центів. Але перш ніж переносити це припущення на продакшн, перевірте актуальні ціни на сторінці тарифів Secret Manager, особливо якщо гарячий шлях читає секрет на кожен запит (ще одна причина кешувати, про це нижче).

Секрети в конфігурації Pulumi: передати значення, ніде його не записавши

Section titled “Секрети в конфігурації Pulumi: передати значення, ніде його не записавши”

Вам потрібно, щоб лабораторний ключ потрапив у Secret Manager, створений через Pulumi, і при цьому ніколи не з’явився в infra/Program.cs чи у відкритому конфіг-файлі. У Pulumi є рівно той інструмент, що треба: config secrets. У модулі 0 ви створили ключ Cloud KMS і налаштували stack із secrets provider gcpkms://. Кожне значення, задане з --secret, шифрується цим KMS-ключем перед записом у Pulumi.<stack>.yaml і лишається зашифрованим у бакеті зі state.

Відкрийте на сайті курсу сторінку лаби m03-secrets. Там показано згенерований для вас лабораторний ключ. Скопіюйте його, а потім:

Terminal window
cd infra
# Omitting the value makes Pulumi prompt for it, so the key never lands in shell history
pulumi config set --secret sniplink:labKey
# Paste the lab key at the prompt

Подивіться на файл stack. Ви побачите щось на кшталт sniplink:labKey: secure: v1:AAAA.... Цей шифротекст можна спокійно комітити: щоб його розшифрувати, потрібен cloudkms.cryptoKeyVersions.useToDecrypt на вашому KMS-ключі. Під час запуску Pulumi розшифровує значення в пам’яті й вважає його секретним output, тому воно маскується в preview, логах і outputs stack.

Тепер створіть секрет і його першу версію. Додайте це у свою програму Pulumi поруч із репозиторієм Artifact Registry з модуля 1.

infra/Program.cs (excerpt)
// … config is the Config("sniplink") from module 1
var labKeyValue = config.RequireSecret("labKey"); // Output<string>, marked secret
var labSecret = new Gcp.SecretManager.Secret("lab-key", new()
{
SecretId = "sniplink-lab-key",
Replication = new Gcp.SecretManager.Inputs.SecretReplicationArgs
{
Auto = new Gcp.SecretManager.Inputs.SecretReplicationAutoArgs(),
},
});
var labKeyVersion = new Gcp.SecretManager.SecretVersion("lab-key-version", new()
{
Secret = labSecret.Id, // full name: projects/…/secrets/sniplink-lab-key
SecretData = labKeyValue,
// When the value changes, Pulumi replaces this resource.
// DISABLE keeps the old version recoverable instead of destroying it.
DeletionPolicy = "DISABLE",
});
// …

Чесна примітка щодо вкладки Terraform. sensitive = true приховує значення у виводі plan, але саме значення все одно зберігається відкритим текстом у state Terraform, тож ваш state-бекенд стає сховищем секретів і потребує такого самого захисту. Свіжі версії Terraform і Google-провайдера додають write-only аргументи, які не зберігають значення в state; перевірте документацію провайдера для своєї версії. Pulumi за замовчуванням шифрує секретні значення в state вашим KMS-ключем, і це одна з причин, чому я тут використовую саме його.

Власна ідентичність сервісу

Section titled “Власна ідентичність сервісу”

Кожна ревізія Cloud Run працює під якимось сервіс-акаунтом. Код у контейнері отримує облікові дані цього акаунта від metadata server, і кожна клієнтська бібліотека Google використовує їх автоматично. Якщо нічого не задати, Cloud Run використовує дефолтний compute сервіс-акаунт PROJECT_NUMBER-compute@developer.gserviceaccount.com.

Проблема дефолтного акаунта в тому, що він спільний і має надлишкові привілеї. Спільний, бо його використовують усі сервіси Cloud Run, VM Compute Engine та деякі інші продукти в проєкті, якщо не вказано інше, тож ви не можете видати йому щось для одного сервісу, не видавши для всіх. Надлишкові привілеї, бо в багатьох проєктах він автоматично отримав roles/editor на проєкт, коли вмикали Compute API. Новіші організації застосовують політику, яка блокує цю автоматичну видачу, тож у вашому курсовому проєкті її може й не бути. Перевірте, перш ніж розслаблятися: так чи інакше, зі спільною ідентичністю мінімальні привілеї неможливі.

Рішення: окремий сервіс-акаунт на кожне навантаження з правами лише на те, що цьому навантаженню потрібно. Sniplink потребує від Google Cloud рівно одного: читати лабораторний ключ. Отже:

infra/Program.cs (excerpt)
// …
var runtimeSa = new Gcp.ServiceAccount.Account("sniplink-runtime", new()
{
AccountId = "sniplink-runtime",
DisplayName = "Sniplink API runtime identity",
});
// Resource-level binding: this SA can read this one secret, nothing else.
var labKeyAccess = new Gcp.SecretManager.SecretIamMember("runtime-reads-lab-key", new()
{
SecretId = labSecret.SecretId,
Role = "roles/secretmanager.secretAccessor",
Member = runtimeSa.Email.Apply(email => $"serviceAccount:{email}"),
});
// …

Використовуйте SecretIamMember, а не SecretIamBinding чи SecretIamPolicy. Ресурс member додає одного принципала до однієї ролі й не чіпає нічого іншого. Ресурс binding є авторитетним для ролі на цьому секреті, а ресурс policy для всієї IAM-політики секрету; обидва без вагань видалять права, які створив хтось інший. Адитивний підхід є безпечним дефолтом.

Зверніть увагу, чого runtime-акаунт не отримує. Йому не потрібна роль для запису логів: Cloud Run сам збирає stdout і stderr. Йому не потрібен доступ до Artifact Registry: образ витягує Cloud Run service agent, тобто акаунт, яким керує Google, а не ваша runtime-ідентичність. Йому нічого не потрібно, щоб випустити ID-токен, який LabKit використовує для /_lab/verify: сервіс-акаунт завжди може отримати identity-токени для себе від metadata server. Одна роль на одному ресурсі.

Два способи передати секрет у Cloud Run

Section titled “Два способи передати секрет у Cloud Run”

Cloud Run може віддати секрет із Secret Manager вашому контейнеру двома способами. Обидва налаштовуються на сервісі, і обидва вимагають, щоб runtime сервіс-акаунт мав secretAccessor на секрет.

Як змінну середовища. Cloud Run читає версію секрету під час старту інстансу й підставляє значення як env-змінну. Ваш код читає її як будь-яке інше значення конфігурації. Протягом життя інстансу значення не змінюється.

Як том. Cloud Run монтує секрет як файл у вибрану вами директорію. Значення береться із Secret Manager, коли ваш код читає файл. Якщо том посилається на latest, нова версія секрету стає видимою при наступному читанні, у тій самій ревізії, на тому самому інстансі.

Змінна середовища Монтування тому
Коли читається значення Один раз, під час старту інстансу Коли ваш код читає файл
Нова версія з latest Бачать лише інстанси, запущені після зміни; ті, що вже працюють, лишаються зі старим значенням, тож одна ревізія може одночасно віддавати два значення Наступне читання файлу бачить її, на всіх інстансах
Рекомендоване посилання на версію Зафіксований номер (3), який змінюється новою ревізією latest підходить
Чи потрібна нова ревізія для ротації Так, для консистентності Ні
Зміни в коді Жодних: це просто IConfiguration Читання файлу (або джерело конфігурації key-per-file)
Що буде без доступу Інстанс не стартує Читання падає під час виконання; старт може пройти успішно
Витоки через дампи оточення та crash reports Так, значення в оточенні процесу Ні

Google сам радить фіксувати конкретну версію, коли ви використовуєте env-змінну, саме тому, що з latest значення залежить від того, коли інстанс випадково стартував. Це робить env-змінні передбачуваними, але перетворює кожну ротацію на деплой.

Для Sniplink я використовую том із трьох причин. Ротація стає операцією в Secret Manager, а не релізом. Значення не потрапляє в оточення процесу, а саме туди першими заглядають debug-endpoint-и та crash dumps. І це дозволяє довести читання під час виконання, на що спирається лаба. Ціною є кілька рядків коду й режим відмови, який доводиться обробляти під час читання, а не на старті.

Ось сервіс Cloud Run з модуля 1 з доданими ідентичністю та томом. Змінюється лише template. Відтепер образ завжди існує, тож сервіс показано без гілки «образу ще немає» з модуля 1 (а у вкладці Terraform зникає count).

infra/Program.cs (excerpt)
// …
var service = new Gcp.CloudRunV2.Service("sniplink-api", new()
{
Name = "sniplink-api",
Location = region,
DeletionProtection = false,
Ingress = "INGRESS_TRAFFIC_ALL",
Template = new Gcp.CloudRunV2.Inputs.ServiceTemplateArgs
{
ServiceAccount = runtimeSa.Email,
Volumes =
{
new Gcp.CloudRunV2.Inputs.ServiceTemplateVolumeArgs
{
Name = "lab-key",
Secret = new Gcp.CloudRunV2.Inputs.ServiceTemplateVolumeSecretArgs
{
Secret = labSecret.SecretId,
Items =
{
new Gcp.CloudRunV2.Inputs.ServiceTemplateVolumeSecretItemArgs
{
Version = "latest",
Path = "lab-key", // file name inside the mount
},
},
},
},
},
Containers =
{
new Gcp.CloudRunV2.Inputs.ServiceTemplateContainerArgs
{
Image = image,
Ports = new Gcp.CloudRunV2.Inputs.ServiceTemplateContainerPortsArgs
{
ContainerPort = 8080,
},
Envs =
{
new Gcp.CloudRunV2.Inputs.ServiceTemplateContainerEnvArgs
{
Name = "LABKIT_STUDENT_ID",
Value = studentId,
},
},
VolumeMounts =
{
new Gcp.CloudRunV2.Inputs.ServiceTemplateContainerVolumeMountArgs
{
Name = "lab-key",
MountPath = "/secrets", // file appears at /secrets/lab-key
},
},
},
},
},
}, new CustomResourceOptions
{
// Cloud Run checks secret access when it creates the revision.
DependsOn = { repo, labKeyAccess, labKeyVersion },
});
// … public invoker binding and outputs from module 1, unchanged

DependsOn тут важливий. Без нього Pulumi може спробувати створити ревізію паралельно з IAM-прив’язкою; Cloud Run перевіряє, чи має runtime-акаунт доступ до секрету, і ревізія падає з помилкою доступу, яка зникає при наступному pulumi up. Нестабільна інфраструктура гірша за зламану, тож задайте порядок явно.

Запустіть pulumi up. Preview має показати чотири створення (секрет, версія, сервіс-акаунт, IAM member) і одне оновлення сервісу. Оновлення створює нову ревізію, бо змінився template. Потім перевірте з CLI, у режимі лише читання:

Terminal window
gcloud secrets versions list sniplink-lab-key --project "$PROJECT_ID"
gcloud run services describe sniplink-api \
--region "$REGION" --project "$PROJECT_ID" \
--format 'value(spec.template.spec.serviceAccountName)'

Друга команда має вивести sniplink-runtime@PROJECT_ID.iam.gserviceaccount.com.

Читання секрету: ваш ILabSecretSource

Section titled “Читання секрету: ваш ILabSecretSource”

POST /_lab/secret-proof у LabKit потребує лабораторного ключа, щоб обчислити доказ, і за правилом «обв’язка платформі, навичка вам» сам ключ не читає. Він запитує в DI-контейнера ILabSecretSource і викликає GetKeyAsync(). Правильно читати секрет під час виконання і є саме тією навичкою, якої вчить модуль, тож ця частина ваша.

Вимоги до вашої реалізації:

  • Читати ключ із файлу, який монтує Cloud Run: /secrets/lab-key. Зробіть шлях конфігурованим, щоб під час розробки вказати на локальний файл.
  • Обрізати пробільні символи. Кінцевий перенос рядка є найчастішою причиною того, що правильний ключ дає неправильний HMAC.
  • Гучно падати, якщо файлу немає або він порожній, із повідомленням, яке називає шлях, але ніколи не вміст.
  • Не читати файл на кожен виклик завжди, але й не кешувати його на все життя процесу. Детальніше нижче.

Спробуйте написати самі, перш ніж дивитися. Ось моя еталонна реалізація.

src/Sniplink.Api/FileLabSecretSource.cs
using CuriousDev.LabKit;
namespace Sniplink.Api;
public sealed class FileLabSecretSource(IConfiguration config, TimeProvider time) : ILabSecretSource
{
private static readonly TimeSpan CacheFor = TimeSpan.FromSeconds(30);
private readonly string _path = config["LabSecret:Path"] ?? "/secrets/lab-key";
private readonly SemaphoreSlim _gate = new(1, 1);
private volatile CachedKey? _cached;
public async ValueTask<string> GetKeyAsync(CancellationToken cancellationToken = default)
{
var current = _cached;
if (current is not null && time.GetUtcNow() - current.ReadAt < CacheFor)
{
return current.Value;
}
await _gate.WaitAsync(cancellationToken);
try
{
current = _cached; // another caller may have refreshed it while we waited
if (current is not null && time.GetUtcNow() - current.ReadAt < CacheFor)
{
return current.Value;
}
if (!File.Exists(_path))
{
throw new InvalidOperationException($"Lab secret file not found at '{_path}'.");
}
var value = (await File.ReadAllTextAsync(_path, cancellationToken)).Trim();
if (value.Length == 0)
{
throw new InvalidOperationException($"Lab secret file at '{_path}' is empty.");
}
_cached = new CachedKey(value, time.GetUtcNow());
return value;
}
finally
{
_gate.Release();
}
}
private sealed record CachedKey(string Value, DateTimeOffset ReadAt);
}

Зареєструйте його в Program.cs перед AddLabKit():

src/Sniplink.Api/Program.cs
using Sniplink.Api; // top-level statements live in the global namespace
// …
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddSingleton<ILabSecretSource, FileLabSecretSource>();
builder.Services.AddLabKit();
// …

Для локального запуску покладіть фейковий ключ у файл поза репозиторієм і вкажіть на нього конфігурацією: LabSecret__Path=/tmp/lab-key dotnet run. Ніколи не кладіть справжній лабораторний ключ в appsettings.Development.json: увесь модуль саме про те, щоб так не робити.

Кешування проти ротації

Section titled “Кешування проти ротації”

Кожне читання /secrets/lab-key у Cloud Run означає звернення до Secret Manager, яке трохи коштує, додає затримку й рахується в квоти. Читати на кожен запит коректно, але марнотратно на гарячому шляху. Кешувати назавжди дешево, але це зводить нанівець увесь сенс тому: ви ротуєте секрет, а запущений процес і далі використовує старе значення, доки інстанс не замінять. Це та сама поведінка env-змінної, тільки з додатковими кроками.

Компромісом є короткий кеш. З 30 секундами ротований ключ підхоплюється на кожному інстансі протягом 30 секунд, а навантажений сервіс робить щонайбільше два читання на хвилину на інстанс. Вибирайте вікно, виходячи з вашого сценарію ротації: як довго старе й нове значення можуть використовуватися одночасно? Якщо відповідь «старе має перестати працювати негайно», вам потрібен інший механізм (відкликання на боці провайдера, короткоживучі токени), а не коротший кеш.

volatile-посилання на незмінний record зроблено навмисно: читачі бачать або стару закешовану пару, або нову, але ніколи не розірвану суміш нового значення зі старою міткою часу. Семафор не дає сплеску запитів у момент завершення кешу одночасно кинутися читати файл.

Секрет, який ви жодного разу не ротували, є секретом, який ви не зможете ротувати під час інциденту. Команда з логу інциденту дізналася це о 10:06. Ротація має бути нудною, відрепетируваною операцією, і з томом плюс latest вона такою і є:

  1. Додайте нову версію. Нове значення стає latest. Споживачі, що читають latest, підхоплюють його при наступному читанні; з 30-секундним кешем протягом пів хвилини.

  2. Дочекайтеся розкатки. Переконайтеся, що споживачі використовують нове значення: логи, health-сигнал або, у цій лабі, успішний доказ. Усе, що фіксує старий номер версії, спершу потребує нової ревізії.

  3. Вимкніть стару версію. Не знищуйте, а вимкніть. Якщо щось, про що ви забули, досі від неї залежить, воно впаде помітно, і ви зможете ввімкнути версію назад за секунди.

  4. Знищте пізніше. Після періоду тиші знищте стару версію або доручіть це політиці.

У Pulumi найпростіша форма цього вже у вас написана: змініть значення в конфігурації й запустіть pulumi up. Оскільки SecretData не можна оновити на місці, Pulumi замінює ресурс SecretVersion. Спочатку він створює нову версію, потім видаляє старий ресурс, а DeletionPolicy = "DISABLE" перетворює це видалення на вимкнення замість знищення. Одна команда виконує кроки 1 і 3.

Для Sniplink цього достатньо, бо єдиний споживач читає latest і кешує на 30 секунд. Для секрету зі споживачами, яких ви не контролюєте, розбийте це на два запуски pulumi up: додайте другий ресурс SecretVersion для нового значення й залиште старий, а коли розкатка завершиться, встановіть на старому ресурсі Enabled = false. Код трохи довший, зате інцидент значно коротший.

Як лаба все це доводить

Section titled “Як лаба все це доводить”

Лаба має довести три речі ззовні вашого проєкту, ніколи не бачачи ні вашого ключа, ні вашої конфігурації: ви читаєте ключ із Secret Manager, ви читаєте його під час виконання, і сервіс працює під окремою ідентичністю.

secretProof. Платформа надсилає випадковий nonce на POST /_lab/secret-proof. LabKit викликає ваш ILabSecretSource, обчислює HMAC-SHA256(key, nonce) і повертає HMAC у hex разом із поточним K_REVISION. Платформа знає ваш лабораторний ключ v1, обчислює такий самий HMAC і порівнює. З вашого сервісу виходить лише HMAC. HMAC не розкриває ключ, а оскільки nonce щоразу новий, стару відповідь не можна відтворити повторно. Платформа також запам’ятовує ревізію.

secretRotation. Коли запуск доходить до цієї перевірки, він ставиться на паузу, а сторінка лаби показує ваш лабораторний ключ v2. Ви задаєте його в конфігурації й запускаєте pulumi up:

Terminal window
pulumi config set --secret sniplink:labKey # paste lab key v2
pulumi preview # expect: SecretVersion replaced, service unchanged
pulumi up

Прочитайте preview. Ви маєте побачити заміну версії секрету й жодних змін у sniplink-api. Якщо сервіс показує оновлення, щось у вашому template залежить від значення секрету, і перевірка не пройде. Зачекайте хвилину, поки сплине кеш, і натисніть «Продовжити». Платформа надсилає новий nonce й очікує HMAC, обчислений із ключем v2, і ту саму ревізію, яку вона запам’ятала в першій перевірці.

Чому ця комбінація переконлива:

  • Захардкоджений ключ у коді чи appsettings.json пройти не може: ключа v2 не існувало, коли ви збирали образ, а перезбірка з ним створює нову ревізію.
  • Ключ у звичайній env-змінній пройти не може: його зміна створює нову ревізію.
  • Том, що читає latest, проходить природно: нова версія, та сама ревізія, нове значення при наступному читанні.

Чесно про межі, як я й обіцяв у модулі 0: env-змінна, що посилається на секрет із latest, може пройти, якщо Cloud Run випадково запустить новий інстанс між вашою ротацією та перевіркою, бо нові інстанси заново резолвлять latest. Лаба не намагається спіймати цей випадок. Це саме та неконсистентність, про яку попереджає таблиця порівняння, і це не той дизайн, якого вчить модуль.

tokenClaim. Claim email у Google ID-токені від /_lab/verify містить сервіс-акаунт, під яким працює ревізія. Перевірка вимагає, щоб він не закінчувався на -compute@developer.gserviceaccount.com (суфікс дефолтного compute-акаунта). Із sniplink-runtime це буде sniplink-runtime@PROJECT_ID.iam.gserviceaccount.com. Токен підписаний Google, тож сервіс не може видати себе за ідентичність, якої не має. Чого перевірка не бачить, так це ролей акаунта; це завдання рівня Verified, коли він з’явиться.

Мета: Sniplink працює як sniplink-runtime, читає лабораторний ключ із Secret Manager через том під час виконання й переживає ротацію ключа без нової ревізії.

  1. Відкрийте сторінку лаби m03-secrets і скопіюйте лабораторний ключ v1. Збережіть його через pulumi config set --secret sniplink:labKey.
  2. Додайте у свою програму Pulumi секрет, версію секрету, сервіс-акаунт sniplink-runtime і прив’язку secretAccessor на цей секрет.
  3. Оновіть сервіс Cloud Run: ServiceAccount = runtimeSa.Email, секретний том для sniplink-lab-key з item path lab-key і версією latest, змонтований у /secrets.
  4. Реалізуйте й зареєструйте ILabSecretSource, що читає /secrets/lab-key. Зберіть і запуште новий образ, потім pulumi up.
  5. Натисніть «Перевірити». Коли запуск дійде до secret-rotation, він стане на паузу, а сторінка покаже лабораторний ключ v2.
  6. Задайте лабораторний ключ v2 у конфігурації, переконайтеся через pulumi preview, що сервіс не змінюється, запустіть pulumi up, зачекайте хвилину, поки сплине кеш, і натисніть «Продовжити».

Перевірте лабу

Завантажуємо лабу…

Увійдіть, щоб перевірити цю лабу й зберегти прогрес. Усе вище працює без акаунта.

  • URL вашого сервісу Cloud Run
  • Лабораторний ключ v1

Перевірок для сервісу, який ви розгорнули: 5.

Що перевіряє кожна перевірка:

  • token (базова): валідний Google ID-токен для audience курсу з вашим ідентифікатором студента, виданий сервісу у вашому проєкті.
  • health: GET /health і далі повертає 200.
  • runtime-identity: email у токені не закінчується на -compute@developer.gserviceaccount.com, тобто ревізія не працює під дефолтним compute-акаунтом.
  • secret-proof: POST /_lab/secret-proof повертає HMAC-SHA256(lab key v1, nonce); платформа запам’ятовує ревізію.
  • secret-rotation: після ротації HMAC збігається з лабораторним ключем v2, а ревізія та сама, що й у secret-proof.

Жодна з них не самозвітна: HMAC можна отримати лише з ключем, а токен підписаний Google. Перевірки не доводять, що runtime-акаунт не має інших ролей; суворіша перевірка читала б IAM-політику проєкту, а для цього потрібен рівень Verified.

Залиште stack. Модуль 4 будується на тому самому сервісі, ідентичності й секреті.

Якщо ставите курс на паузу більш ніж на кілька днів, знищіть усе. Щоб повернутися, запустіть pulumi up (він заново створить репозиторій і впаде на сервісі, бо образ зник разом зі старим репозиторієм), знову запуште образ із тим самим тегом і запустіть pulumi up ще раз:

Terminal window
cd infra
pulumi destroy

DeletionPolicy = "DISABLE" захищає окремі версії під час ротації, але pulumi destroy видаляє і сам секрет, а видалений секрет забирає з собою всі свої версії. Для курсового проєкту це саме те, що треба. Ваші лабораторні ключі лишаються доступними на сторінці лаби.

  • Секретам не місце в git, образах, env-файлах чи звичайних env-змінних; їхнє місце у сховищі з контролем доступу на рівні кожного секрету, версіями й audit-логами, і в Google Cloud це Secret Manager.
  • Config secrets у Pulumi, зашифровані вашим ключем Cloud KMS, дозволяють передати значення в інфраструктурний код, ніде не записавши його відкритим текстом.
  • Окремий сервіс-акаунт з однією роллю на рівні ресурсу замінює спільний і часто перепривілейований дефолтний compute-акаунт.
  • Секрет, змонтований як том і прочитаний як latest, ротується без нової ревізії; короткий кеш балансує вартість і швидкість, з якою ротація набуває чинності.
  • Ротація складається з кроків додати, дочекатися, вимкнути, знищити пізніше, і її варто відрепетирувати до того, як вона знадобиться.