🇬🇧 English | 🇺🇦 Українська
3. Облікові дані¶
Найважливіший розділ документації. Помилитися тут легко, а наслідки незворотні: ключ, який потрапив у зібрану гру, вважайте оприлюдненим назавжди.
Розділ відповідає на три питання: де ключі не можна тримати ніколи, де їх тримати для кожного способу постачання, і як саме плагін їх шукає у процесі роботи.
Головне правило¶
Усе, що є всередині клієнтської збірки, можна видобути.
Це не перестраховка, а технічна даність. Щоб підписати запит, ключ мусить опинитися в пам'яті
у відкритому вигляді — отже, його дістає дамп пам'яті або HTTPS-проксі з підставленим
сертифікатом. Шифрування ассетів, обфускація, «сховати в .pak» не допомагають: ключ
розшифрування лежить у тому самому бінарнику, який ви віддаєте гравцеві.
Тому питання не «як сховати ключ», а «як зробити, щоб його там не було».
Саме тому плагін ніде не дає поля, куди ключ можна вписати «назавжди»:
| Місце | Чому там немає полів для ключів |
|---|---|
| Project Settings → S3 Compatible Storage | Зберігається в DefaultGame.ini — їде в збірку й у систему контролю версій |
| Ассет S3 Storage Profile | Кукається у збірку так само, як будь-який ассет |
Обидва зберігають звідки брати ключі, а не самі ключі.
Що саме таке «облікові дані»¶
| Складова | Призначення |
|---|---|
| Access Key Id | Ідентифікатор ключа. Не секрет — потрапляє в кожен запит відкритим текстом |
| Secret Access Key | Справжній секрет. Ним підписується запит; сам ключ ніколи не надсилається |
| Session Token | Є лише в тимчасових ключів. Надсилається як x-amz-security-token |
Про Session Token варто знати одну річ: тимчасовий ключ без свого токена не працює — провайдер відповість, що такого ключа не існує. Якщо ви отримали трійку від STS, передавайте всі три значення, а не два.
Чотири сценарії постачання¶
A. Код працює на сервері¶
Виділений сервер Unreal, бекенд-сервіс, збірковий агент, консольна утиліта.
Єдиний випадок, де довгоживучі ключі безпечні: бінарник не потрапляє до чужих рук. Ключі живуть:
- у змінних оточення процесу;
- у ролі IAM самого інстанса (EC2, ECS, EKS) — тоді ключів немає взагалі, а SDK бере тимчасові з метаданих; для цього плагіна це означає, що змінні вам проставить середовище;
- у менеджері секретів або у примонтованому файлі, який ваш скрипт запуску перетворює на змінні оточення.
Налаштування: Credential Source → Environment variables.
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=... # лише для тимчасових ключів
Значення читаються перед кожним запитом, тож ротація ключів не потребує перезапуску процесу.
Імена змінних починаються з
AWS_за домовленістю, а не тому, що потрібен Amazon. Це ті самі імена, які читаютьaws,mc(MinIO),rcloneі решта інструментів, з яким би сервісом ви не працювали.
Кілька сховищ на одному сервері. Задайте кожному профілю свій Environment Variable
Prefix: профіль із префіксом ARCHIVE читає ARCHIVE_ACCESS_KEY_ID і
ARCHIVE_SECRET_ACCESS_KEY, не заважаючи основному.
B. Клієнт гри плюс ваш бекенд¶
Звичайний випадок для гри. Два робочі підходи, і обидва лишають клієнт без довгоживучих ключів.
B1. Підписані посилання — у клієнта немає ключів узагалі¶
Клієнт просить ваш сервер: «дай посилання, щоб залити збереження». Сервер, у якого ключі є, підписує посилання й віддає його. Клієнт робить звичайний HTTP-запит за цим посиланням — підписувати нічого не треба, бо підпис уже всередині адреси.
Найбезпечніший варіант: видобувати з клієнта просто нічого.
Налаштування на клієнті: Credential Source → Anonymous.
На боці сервера посилання створюється так:
FS3PresignedUrlResult Url = Client->GeneratePresignedUrl(
TEXT("my-bucket"), TEXT("saves/player-42.sav"),
ES3HttpMethod::PUT, 900); // дійсне 15 хвилин
Що варто врахувати:
- Посилання дійсне лише для того методу, яким його підписали. PUT-посилання, відкрите у браузері, не спрацює — браузер надсилає GET.
- Максимальний строк життя за специфікацією — 7 діб (604800 секунд).
- Посилання дає доступ будь-кому, хто його має. Робіть строк коротким.
- Підпис обчислюється локально, без звернення до провайдера, тож це швидко й не може «не вдатися» через мережу.
B2. Короткоживучі ключі¶
Клієнт автентифікується у вашому бекенді (квиток Steam чи Epic, JWT — що завгодно), бекенд
викликає AWS STS AssumeRole і повертає трійку з терміном життя від 15 хвилин до 12 годин
і вузькою політикою — наприклад, лише s3:PutObject на bucket/users/${user_id}/*.
Ці ключі живуть тільки в пам'яті й на диск не пишуться.
Налаштування: Credential Source → Supplied in code.
З Blueprint:
(після успішної автентифікації у вашому бекенді)
│
└─► Get S3 Subsystem → Set Runtime Credentials
Access Key Id : (від бекенда)
Secret Access Key : (від бекенда)
Session Token : (від бекенда)
Expires In Seconds : 3600
Плагін припиняє користуватися ключами трохи раніше за вказаний строк, щоб запит, підписаний під самий кінець, не прибув уже після його завершення.
Для виходу з облікового запису — Clear Runtime Credentials.
Через ассет профілю потрібна інша нода.
Set Runtime Credentialsналаштовує лише клієнта за замовчуванням. Клієнт, отриманий черезGet S3 Client For Profile, її не бачить: він бере ключі виключно зCredential Sourceсвого ассета.Для профілю викличте Set Static Credentials на самому клієнті — вона приймає ті самі чотири значення й діє на той клієнт, на якому викликана:
Get S3 Client For Profile (SP_PlayerSaves) → Set Static CredentialsРозгорнуті приклади під кожну конфігурацію гри — у 13. Профілі на практиці.
З C++ можна піти далі й поставити провайдера, який сам ходить по нові ключі:
auto Provider = MakeShared<FS3CredentialsProvider_Callback, ESPMode::ThreadSafe>(
FS3CredentialsProvider_Callback::FS3CredentialsFetch::CreateLambda(
[](FS3CredentialsResolved OnFetched)
{
// Запит до вашого бекенда; коли відповідь прийде - викличте OnFetched.
// Рівно один раз і в будь-якому разі, зокрема на невдачі: перший параметр
// саме про це. Викликати лише на успіху - означає лишити всі передавання,
// що чекають на ключі, висіти назавжди.
MyBackend::RequestS3Credentials(
[OnFetched](bool bOk, const FString& Key, const FString& Secret,
const FString& Token, int32 TtlSeconds)
{
OnFetched.ExecuteIfBound(bOk, FS3Credentials(
Key, Secret, Token,
FDateTime::UtcNow() + FTimespan::FromSeconds(TtlSeconds)));
});
}));
Client->SetCredentialsProvider(Provider);
Такий провайдер сам оновлює ключі перед закінченням строку й склеює одночасні запити: якщо двадцять передавань стартують на протермінованих ключах, ваш бекенд отримає один запит, а не двадцять. Без цього сплеск передавань перетворювався б на сплеск запитів до вас.
C. Публічний контент лише для читання¶
Роздача патчів, ассетів, оновлень із бакета, вміст якого й так публічний.
Тут припустимо вшити ключ — але лише з політикою, яка дозволяє винятково s3:GetObject
на одному бакеті. Витік такого ключа коштує вам трафіку, а не даних.
Налаштування: Anonymous, якщо бакет справді відкритий на читання, або
Supplied in code із вшитим ключем лише на читання.
Перевірте себе одним питанням: що станеться, якщо цей ключ завтра з'явиться на форумі? Якщо відповідь «нічого страшного» — вбудовувати можна.
D. Однокористувацький застосунок без сервера¶
Настільний інструмент, переглядач ассетів, будь-що з екраном «підключитися до мого сховища». Бакет належить самому користувачеві, тож ключі — його, а не ваші. Їх не можна ані вшивати, ані вимагати для них бекенд.
Налаштування: Credential Source → Local user store (encrypted).
Ваш екран налаштувань викликає:
(натиснуто «Підключитися»)
│
└─► Save S3 Credentials
Profile Name : default
Access Key Id : (з поля вводу)
Secret Access Key : (з поля вводу)
І на старті застосунку:
Has S3 Credentials (default)
├─ True → одразу до основного екрана
└─ False → показати екран підключення
Для виходу — Clear S3 Credentials.
Як саме це зберігається¶
Файл лежить під теками налаштувань поточного користувача — поза проєктом — і зашифрований AES-256. Ключ шифрування виводиться з ідентифікатора машини та назви вашого застосунку.
Чесно про те, від чого це захищає:
- ✅ від читання файлу очима, від випадкового витоку через резервну копію чи спільний конфіг;
- ✅ від перенесення файлу на іншу машину — там ключ шифрування не відтвориться;
- ❌ не захищає від того, хто вже виконує код на цій машині від імені цього користувача: логіка виведення ключа лежить у бінарнику.
Це той самий рівень гарантій, який дають більшість настільних застосунків без інтеграції із
системним сховищем ключів. Вбудованого сховища ключів в Unreal немає, тож якщо потрібен саме
він — реалізуйте інтерфейс IS3CredentialsStore поверх Keychain, DPAPI чи libsecret, не
змінюючи решти плагіна.
Шлях до файлу повертає нода Get S3 Credentials File Path — зручно для дії «показати, де зберігаються мої налаштування».
Кілька сховищ. Дайте кожному своє Local Store Profile Name: ключі зберігаються під цим іменем, і сховище A не побачить ключів сховища B.
Підсумкова таблиця¶
| Сценарій | Що в збірці | Що на диску в користувача | Що в пам'яті |
|---|---|---|---|
| A — сервер | нічого | змінні оточення чи роль інстанса | так, довго |
| B1 — підписані посилання | нічого | нічого | лише посилання |
| B2 — короткоживучі ключі | нічого | нічого | так, хвилини |
| C — публічне читання | вшитий ключ лише на читання | у .pak |
так |
| D — застосунок користувача | нічого | зашифрований файл | так |
Як плагін шукає ключі під час роботи¶
Порядок фіксований, і знати його варто — саме він пояснює, чому інколи «працює не той ключ».
1. Ключі, задані в коді, перекривають усе. Set Runtime Credentials або власний провайдер
на клієнті мають найвищий пріоритет: вони прийшли від вашого бекенда й актуальні.
2. Далі діє те, що вибрано в Credential Source:
| Credential Source | Звідки береться |
|---|---|
Environment variables |
Змінні оточення з відповідним префіксом |
Local user store |
Зашифрований файл користувача, за іменем профілю |
Anonymous |
Нічого — запити йдуть без підпису |
Supplied in code |
Нічого не встановлюється; чекає на ваш провайдер |
3. У редакторі — і лише в редакторі — джерело Environment variables спершу дивиться на
ключі, вписані вручну:
- ключі самого профілю (поля
Editor Access Key Id/Editor Secret Access Keyна ассеті) — якщо вони є; - інакше спільна секція Project Settings → S3 Credentials (Editor Only) — але лише коли
профіль читає стандартний префікс
AWS.
Це дає змогу натиснути Test Connection одразу, не пишучи коду й не заносячи ключі у власні
налаштування проєкту. Обидва місця зберігаються в
Saved/Config/<Платформа>/EditorPerProjectUserSettings.ini: файл особистий, виключений із
системи контролю версій стандартним .gitignore проєкту й не кукається. Тобто вписане
туди не поїде ані у збірку, ані до колеги.
Це найчастіша причина плутанини за кількох сховищ. Профіль без власних ключів візьме спільні — і запит до Amazon піде підписаний ключем від MinIO. Провайдер відповість, що такого ключа не існує, і помилка звинуватить ключ, хоча винна конфігурація.
Перший рядок звіту завжди називає джерело:
---- https://s3.us-east-2.amazonaws.com, Static(editor keys for SP_Archive), region us-east-2
Static(editor keys for <профіль>)— ключі профілю.Static(editor settings)— спільна секція.Environment— справжні змінні оточення.LocalUserStore(<ім'я>)— файл користувача.
4. Зібрана гра нічого з пункту 3 не бачить — жодна з редакторських секцій не кукається. Там працює лише те, що описано в пунктах 1–2.
Ключі профілю: у редакторі та у зібраній грі¶
Найчастіше питання про ассет S3 Storage Profile звучить так: якщо я вписав ключі просто в поля ассета, а ассет кукається у збірку — як їх звідти прочитає зібрана гра? Відповідь: ніяк, і не тому що «не реалізовано», а тому що їх там фізично немає.
Чому вони не можуть потрапити у збірку¶
Три поля Editor Access Key Id / Editor Secret Access Key / Editor Session Token захищені чотирма незалежними механізмами. Незалежними — тобто помилка в будь-якому одному не відкриває решту:
| Механізм | Що саме робить |
|---|---|
#if WITH_EDITORONLY_DATA |
У збірці без редактора цих полів немає як членів класу взагалі — компілятор їх прибирає |
Transient |
Навіть у редакторі рушій ніколи не серіалізує їх у .uasset. Ассет кукається — але цих полів у ньому немає |
| Ключі лежать не в ассеті | Вони зберігаються окремо, у Saved/Config/<Платформа>/EditorPerProjectUserSettings.ini, під іменем цього ассета. Файл особистий, виключений із системи контролю версій і не кукається |
#if WITH_EDITOR навколо самого читання |
Код, який підставляє ці ключі замість змінних оточення, у зібраній грі відсутній цілком — підставляти нема чим і нема куди |
Практичний наслідок, який видно очима: поля порожні щоразу, коли ассет завантажується заново — рушій їх не зберігає. Плагін заповнює їх сам, з того ini, одразу після завантаження ассета. Це не збій, а якраз видима ознака того, що в самому ассеті ключів немає.
Записуються вони назад лише тоді, коли ви змінили саме одне з цих трьох полів. Правка будь-чого іншого на профілі, у якого поля зараз порожні, збережених ключів не стирає. Очистити всі три поля — це і є спосіб видалити ключі зі сховища.
Що читається у зібраній грі¶
Ассет несе у збірку тільки конфігурацію підключення: провайдера, адресу, регіон, стиль адресації й параметри передавання. Облікових даних у ньому немає за побудовою — він називає джерело, з якого їх узяти:
| Credential Source | У редакторі | У зібраній грі |
|---|---|---|
| Environment variables | Спершу ключі самого профілю; якщо їх немає, а префікс стандартний AWS — спільна редакторська секція; інакше справжні змінні оточення |
Тільки справжні змінні оточення: <ПРЕФІКС>_ACCESS_KEY_ID, <ПРЕФІКС>_SECRET_ACCESS_KEY, <ПРЕФІКС>_SESSION_TOKEN |
| Local user store | Однаково | Однаково: зашифрований файл у теці користувача, за Local Store Profile Name |
| Anonymous | Однаково | Однаково: запити не підписуються |
| Supplied in code | Однаково | Однаково: нічого не встановлюється, провайдера ставить ваш код |
Тобто відрізняється лише один рядок таблиці — той, де редакторські ключі стоять замість
змінних оточення. Решта джерел поводиться в редакторі точно так само, як у бою, і саме тому
Local user store варто перевіряти прямо в редакторі: що ви там побачите, те й буде у грі.
Змінні оточення читаються перед кожним запитом, а не один раз на старті процесу — тож ротація ключів на сервері діє без перезапуску.
Якщо у зібраній грі змінних немає, операція не «зависає мовчки»: джерело облікових даних
повертає невдачу, і операція завершується з Authentication Error, а в повідомленні названо, яке саме
джерело опитували.
Поля редакторських ключів діють лише за
Credential Source = Environment variables. За інших джерел вони лишаються доступними для введення, але не читаються: профіль ізLocal user storeвізьме ключі із зашифрованого файлу користувача, а вписане в ці поля просто проігнорує. Якщо Test Connection не бачить щойно введених ключів — перевірте спочатку саме це поле.
Ротація і відкликання¶
- Змінні оточення читаються перед кожним запитом — заміна діє одразу.
- Короткоживучі ключі оновлюються самі, якщо ви задали
Expires In Secondsабо поставили callback-провайдер. - Після відмови через сам ключ (
SignatureDoesNotMatch,InvalidAccessKeyId,ExpiredToken— типово 401) плагін скидає закешовані ключі, і наступна спроба питає їх наново: відкликаний чи протермінований ключ не програється по колу. Звичайний403 AccessDenied— це не те саме: ключ підписав запит правильно, йому просто бракує прав, тож скидати тут нічого — новий запит по ті самі ключі дав би той самий результат, а якщо ключі приходять від бекенда, це лише даремно навантажило б його. - Ключі користувача прибирає
Clear S3 Credentials; усі профілі одразу —Clear All S3 Credentials.
Чого не варто робити¶
| Спокуса | Чому погано | Що замість |
|---|---|---|
Вписати ключ у DefaultGame.ini «тимчасово» |
Він поїде в git і у збірку; «тимчасово» переживає реліз | Секція Editor Only |
Покласти ключі в DataTable чи ассет |
Кукається у збірку так само | Сценарій B |
| Свій «шифрувальник» ключа у грі | Ключ розшифрування поруч, у тому самому бінарнику | Сценарій B |
| Один ключ на всіх гравців із правом запису | Один витік — і будь-хто перезапише чужі збереження | STS із політикою на ${user_id} |
| Довгий строк підписаного посилання «щоб напевно» | Посилання працює в будь-кого, хто його отримав | Хвилини, не доба |
Далі: 4. Операції у Blueprint · див. також 10. Сценарії розгортання і 13. Профілі на практиці, де ці сценарії розібрано на конкретних ассетах профілів