Skip to content

🇬🇧 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. Профілі на практиці, де ці сценарії розібрано на конкретних ассетах профілів