Хранение данных в S3 часто воспринимается как нечто простое: загрузили объект, при необходимости обновили или удалили. Но на практике это хранилище обычно используется в сценариях, в которых цена ошибки слишком высока. Вы же хотите случайно удалить или перезаписать бэкапы, логи, артефакты сборки или данные, подпадающие под регуляторные требования. Чтобы защититься от подобных ситуаций, в S3 есть механизм Object Lock.
Базовые термины
Object Lock — это механизм S3, который позволяет защитить объекты от удаления и перезаписи на заданный срок или бессрочно. Он реализует модель WORM (Write Once, Read Many): объект можно записать один раз, после чего его содержимое нельзя изменить или удалить, пока действует блокировка.
Эта функциональность обычно используется для выполнения регуляторных требований, где требуется WORM-хранилище, а также как дополнительный уровень защиты от случайных или ошибочных действий — например, удаления бэкапов или логов.
Object Lock поддерживает два способа блокировки версии объекта: период хранения (retention) и юридическую блокировку (legal hold). Для каждой версии объекта можно использовать один из них или оба одновременно.
Чтобы никто не запутался, давайте определимся с терминологией, которая встретится далее в статье:
- retention — временное удержание версии объекта;
- GOVERNANCE или COMPLIANCE — режимы retention;
- legal hold — бессрочная юридическая блокировка.
Все эти механизмы являются частью Object Lock.
Период хранения (retention)
Retention работает в одном из двух режимов:
- GOVERNANCE — позволяет пользователям с соответствующими правами сократить срок удержания или удалить объект с использованием специального заголовка;
- COMPLIANCE — запрещает изменение или удаление объекта до истечения срока удержания даже для пользователей с максимальными правами.
Эти режимы определяют строгость применения retention, но не являются отдельными типами блокировки.
Период хранения задает фиксированный промежуток времени, в течение которого конкретная версия объекта остается заблокированной. До истечения этого срока объект нельзя удалить или перезаписать.
Период хранения можно:
- задавать индивидуально для отдельных объектов;
- настраивать по умолчанию для всего бакета;
- ограничивать минимальное и максимальное значение через политику бакета с использованием условия
s3:object-lock-remaining-retention-days.
Юридическая блокировка(legal hold)
Юридическая блокировка обеспечивает ту же защиту, что и период хранения, но не имеет срока действия. Она действует до тех пор, пока вы явно не снимете ее вручную. Юридические блокировки применяются к отдельным версиям объектов и не зависят от настроек retention.
Object Lock работает только в бакетах с включенным версионированием S3. При установке блокировки информация о ней сохраняется в метаданных конкретной версии объекта. Важно учитывать, что блокировка:
- защищает только указанную версию объекта;
- не запрещает создание новых версий;
- не предотвращает появление delete-marker’ов при удалении объекта.
Если загрузить объект с тем же ключом в бакет, где уже существует защищенная версия, S3 создаст новую версию. При этом ранее защищенная версия останется заблокированной в соответствии с заданными настройками хранения.
Теперь перейдем к практической части и посмотрим, как пользоваться Object Lock на примере S3 Selectel.
Часть работы в панели управления Selectel
В этой статье все операции с бакетами и Object Lock мы будем выполнять через API, поэтому заранее создавать бакет в панели управления не требуется.
Перед началом работы нам нужно только получить учетные данные для доступа к S3 — Access Key и Secret Key.
Для этого создадим сервисного пользователя и выпустим S3-ключ.
Кратко перечислю основные шаги:
- Перейдите в панель управления Selectel → Сервисные пользователи.
- Нажмите «Добавить сервисного пользователя».
- Укажите имя пользователя и сгенерируйте пароль (или задайте свой).
- В разделе настройки доступов выберите нужный проект и назначьте роль member.
Эта роль необходима, в том числе, для операций с Object Lock (например, для использования BypassGovernanceRetention). - Нажмите «Добавить пользователя».
- Откройте созданного пользователя → вкладка «Доступ» → в разделе S3-ключи нажмите «Добавить ключ».
- Выберите проект, укажите имя ключа и нажмите «Сгенерировать».
- Сохраните Access Key и Secret Key.
Обратите внимание: ключи не хранятся в системе и отображаются только один раз. Сохраните их в надежном месте.
После этого вы можете использовать полученные ключи для работы с S3 через SDK или CLI и выполнять все дальнейшие действия, включая создание бакетов, настройку Object Lock и управление политиками удержания, непосредственно из кода.
Инициализация S3-клиента
Во всех примерах в этой статье мы будем использовать один и тот же код инициализации S3-клиента на Go. Он полностью совпадает с тем, что использовался в моей предыдущей статье про условные операции записи.
Для работы с S3 используется SDK от Amazon — aws-sdk-go-v2. Вспомогательная функция NewS3Client загружает конфигурацию из стандартных файлов (~/.aws/config и ~/.aws/credentials), проверяет регион и валидность учетных данных, после чего возвращает готовый к использованию клиент S3.
Этот клиент будет использоваться во всех последующих примерах — для создания бакетов, настройки Object Lock, установки политик удержания и управления объектами.
Создание бакета с Object Lock
Object Lock можно включить либо сразу при создании бакета, либо позже — для уже существующего. Рассмотрим оба варианта.
Создание нового бакета с Object Lock
Если вы создаете бакет с нуля, самый простой способ — сразу включить Object Lock при вызове CreateBucket. Для этого достаточно передать параметр ObjectLockEnabledForBucket.
Пример вспомогательной функции:
func createBucket(ctx context.Context, client *s3.Client, bucketName string) error {
if _, err := client.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: aws.String(bucketName),
ObjectLockEnabledForBucket: aws.Bool(true),
}); err != nil {
return err
}
return nil
}
Такой бакет будет создан с поддержкой Object Lock и готов к дальнейшей настройке правил удержания объектов.
Включение Object Lock для существующего бакета
Если бакет уже существует, включить Object Lock можно, но с одним важным условием: в бакете обязательно должно быть включено версионирование.
Сначала включим версионирование с помощью PutBucketVersioning:
func enableVersioning(ctx context.Context, client *s3.Client, bucketName string) error {
if _, err := client.PutBucketVersioning(ctx, &s3.PutBucketVersioningInput{
Bucket: aws.String(bucketName),
VersioningConfiguration: &types.VersioningConfiguration{
Status: types.BucketVersioningStatusEnabled,
},
}); err != nil {
return err
}
return nil
}
После этого можно включить Object Lock через PutObjectLockConfiguration:
func putObjectLockConfiguration(ctx context.Context, client *s3.Client, bucketName string) error {
if _, err := client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{
Bucket: aws.String(bucketName),
ObjectLockConfiguration: &types.ObjectLockConfiguration{
ObjectLockEnabled: types.ObjectLockEnabledEnabled,
},
}); err != nil {
return err
}
return nil
}
После выполнения этих операций бакет будет настроен для работы с Object Lock.
Настройка retention по умолчанию
Дальше есть два варианта:
- задавать retention индивидуально для каждого объекта;
- определить правила по умолчанию, которые будут применяться ко всем новым объектам в бакете.
Стандартные правила retention можно указать сразу при вызове PutObjectLockConfiguration. Например, следующий код задает retention-период в пять дней в режиме GOVERNANCE для всех новых объектов:
func putObjectLockConfiguration(ctx context.Context, client *s3.Client, bucketName string) error {
if _, err := client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{
Bucket: aws.String(bucketName),
ObjectLockConfiguration: &types.ObjectLockConfiguration{
ObjectLockEnabled: types.ObjectLockEnabledEnabled,
Rule: &types.ObjectLockRule{
DefaultRetention: &types.DefaultRetention{
Days: aws.Int32(5),
Mode: types.ObjectLockRetentionModeGovernance,
},
},
},
}); err != nil {
return err
}
return nil
}
Такой подход удобен, если вы хотите сразу защитить все новые объекты от случайного удаления или перезаписи без необходимости настраивать retention для каждого из них вручную.
Установка политики удержания на объект
После включения Object Lock на уровне бакета можно задавать правила удержания уже для конкретных объектов или их версий.
Установка retention и legal hold при загрузке объекта
Политику удержания и юридическую блокировку можно задавать не только после загрузки объекта, но и сразу в момент записи. Это предпочтительный вариант, так как он исключает временное окно, в котором объект существует без защиты.
При загрузке объекта через PutObject можно сразу указать:
- режим и срок временного удержания (retention);
- юридическую блокировку (legal hold);
- или оба механизма одновременно.
Установка retention при загрузке
В примере ниже объект загружается с политикой удержания сроком на 7 дней в режиме COMPLIANCE:
func putObjectWithRetention(
ctx context.Context,
client *s3.Client,
bucketName, objectKey string,
) error {
retainUntil := time.Now().AddDate(0, 0, 7)
_, err := client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
Body: strings.NewReader("hello"),
ObjectLockMode: types.ObjectLockRetentionModeCompliance,
ObjectLockRetainUntilDate: aws.Time(retainUntil),
})
if err != nil {
return err
}
return nil
}
В этом случае объект с момента создания будет защищен от удаления и перезаписи до указанной даты.
Установка legal hold при загрузке
Аналогично можно установить юридическую блокировку сразу при загрузке объекта:
func putObjectWithLegalHold(
ctx context.Context,
client *s3.Client,
bucketName, objectKey string,
) error {
_, err := client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
Body: strings.NewReader("hello"),
ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatusOn,
})
if err != nil {
return err
}
return nil
}
Юридическая блокировка не имеет срока действия и будет активна до тех пор, пока вы явно не снимете ее с помощью PutObjectLegalHold. При необходимости оба механизма можно использовать вместе. Такой подход позволяет гарантировать, что объект никогда не окажется в незащищенном состоянии даже на короткое время между загрузкой и последующей установкой политики удержания.
Установка retention и legal hold для multipart-объектов
При multipart-загрузке параметры Object Lock задаются на этапе создания загрузки, то есть при вызове CreateMultipartUpload. Именно в этот момент определяется конфигурация будущего объекта. Метод CompleteMultipartUpload используется только для завершения загрузки и не позволяет задать или изменить параметры Object Lock.
В следующем примере multipart-загрузка создается с:
- политикой удержания сроком на 7 дней;
- юридической блокировкой (legal hold), действующей до явного снятия.
func createMultipartUploadWithObjectLock(
ctx context.Context,
client *s3.Client,
bucketName, objectKey string,
) (*string, error) {
retainUntil := time.Now().AddDate(0, 0, 7)
out, err := client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
ObjectLockMode: types.ObjectLockRetentionModeCompliance,
ObjectLockRetainUntilDate: aws.Time(retainUntil),
ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatusOn,
})
if err != nil {
return nil, err
}
return out.UploadId, nil
}
После загрузки всех частей и вызова CompleteMultipartUpload объект будет создан с заданным сроком удержания и бессрочной юридической блокировкой. Удалить или изменить такой объект можно будет только после снятия legal-hold и с учетом режима политики удержания.
Временное удержание (retention)
Для установки временного периода удержания используется метод PutObjectRetention. В запросе необходимо указать режим удержания и дату, до которой объект будет заблокирован. Помимо ключа объекта можно также передать VersionId, если требуется применить правило не к последней версии.
Пример установки удержания сроком на одну неделю:
func putObjectRetention(ctx context.Context, client *s3.Client, bucketName, objectKey string) error {
week := time.Now().AddDate(0, 0, 7)
if _, err := client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
VersionId: nil, // не обязательный параметр
BypassGovernanceRetention: aws.Bool(true), // не обязательный параметр
Retention: &types.ObjectLockRetention{
Mode: types.ObjectLockRetentionModeCompliance,
RetainUntilDate: aws.Time(week),
},
}); err != nil {
return err
}
return nil
}
Важно учитывать несколько моментов:
- Политика удержания применяется к конкретной версии объекта.
- В режиме COMPLIANCE срок удержания нельзя сократить или снять до его окончания.
- В режиме GOVERNANCE такие операции возможны, но только при наличии соответствующих прав.
Для этого в PutObjectRetention предусмотрен параметр BypassGovernanceRetention. Он позволяет:
- уменьшить срок удержания для объектов под политикой GOVERNANCE;
- удалить объект, даже если период удержания еще не истек.
При этом не все сервисные пользователи имеют право использовать этот параметр. В Selectel S3 такая возможность доступна только пользователю с ролью member.
Юридическая блокировка (legal hold)
Для установки или снятия бессрочной блокировки используется метод PutObjectLegalHold. В отличие от retention, юридическая блокировка не имеет срока действия и остается активной до тех пор, пока вы явно не отключите ее.
Статус блокировки задается значением ON или OFF:
func putObjectLegalHold(ctx context.Context, client *s3.Client, bucketName, objectKey string) error {
if _, err := client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
LegalHold: &types.ObjectLockLegalHold{
Status: types.ObjectLockLegalHoldStatusOn,
},
VersionId: nil,
}); err != nil {
return err
}
return nil
}
Как и в случае с retention, юридическая блокировка применяется к конкретной версии объекта и не мешает созданию новых версий с тем же ключом.
Удаление объекта, находящегося под блокировкой
Удаление объекта, защищенного с помощью Object Lock, напрямую зависит от типа блокировки, которая применяется к конкретной версии объекта. Перед удалением важно понять, какой тип блокировки применен к версии объекта: retention (и в каком режиме) или legal hold.
Возможны три варианта:
- юридическая блокировка (legal hold);
- временное удержание в режиме GOVERNANCE;
- временное удержание в режиме COMPLIANCE.
Рассмотрим каждый случай отдельно.
Удаление объекта под юридической блокировкой (legal hold)
Юридическая блокировка не имеет срока действия и действует до тех пор, пока вы явно не снимете ее. Поэтому для удаления объекта сначала необходимо отключить legal hold, а затем удалить нужную версию объекта. Сначала снимаем юридическую блокировку:
func putObjectLegalHold(ctx context.Context, client *s3.Client, bucketName, objectKey, versionID string) error {
if _, err := client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
LegalHold: &types.ObjectLockLegalHold{
Status: types.ObjectLockLegalHoldStatusOff,
},
VersionId: aws.String(versionID),
}); err != nil {
return err
}
return nil
}
После этого можно удалить конкретную версию объекта:
func deleteObject(ctx context.Context, client *s3.Client, bucketName string, objectKey, versionID string) error {
if _, err := client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
VersionId: aws.String(versionID),
}); err != nil {
return err
}
return nil
}
Удаление объекта под временным удержанием в режиме GOVERNANCE
В режиме GOVERNANCE объект можно удалить до истечения срока удержания, но только при наличии прав на использование заголовка BypassGovernanceRetention.
Если такие права есть, удаление выполняется одним вызовом DeleteObject:
func deleteObject(ctx context.Context, client *s3.Client, bucketName string, objectKey, versionID string) error {
if _, err := client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
BypassGovernanceRetention: aws.Bool(true),
VersionId: aws.String(versionID),
}); err != nil {
return err
}
return nil
}
Если же у пользователя нет разрешения на использование BypassGovernanceRetention, удалить объект до истечения срока удержания не получится — операция завершится ошибкой.
Удаление объекта под временным удержанием в режиме COMPLIANCE
Режим COMPLIANCE — самый строгий. Объекты, находящиеся под такой блокировкой, невозможно удалить или изменить до истечения заданного срока удержания.
Даже пользователь с максимальными правами не сможет обойти это ограничение.
Единственный возможный вариант — дождаться окончания периода удержания и только после этого выполнить запрос DeleteObject.
Bucket policy и Object Lock
Помимо настройки Object Lock на уровне бакета и объектов, на поведение блокировок можно дополнительно влиять с помощью bucket policy. Это позволяет централизованно разрешать или запрещать определенные операции, в том числе такие, которые могут быть нежелательны с точки зрения безопасности или стоимости хранения данных.
Типичный пример — ограничение минимального срока удержания при загрузке объектов. Это полезно, если вы хотите гарантировать, что данные не будут сохраняться на слишком короткий срок, даже если клиент попытается задать меньший retention.
Ниже приведен пример bucket policy, которая запрещает установку политики удержания с количеством дней меньше заданного значения:
func putBucketPolicy(ctx context.Context, client *s3.Client, bucketName string, minDays uint) error {
if _, err := client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: aws.String(bucketName),
Policy: aws.String(fmt.Sprintf(`
{
"Version":"2012-10-17",
"Id": "SetRetentionLimits",
"Statement": [
{
"Sid": "SetRetentionPeriod",
"Effect": "Deny",
"Principal": "*",
"Action": [
"s3:PutObjectRetention"
],
"Resource": "arn:aws:s3:::%s/*",
"Condition": {
"NumericGreaterThan": {
"s3:object-lock-remaining-retention-days": "%d"
}
}
}
]
}
`, bucketName, minDays)),
}); err != nil {
return err
}
return nil
}
В этом примере любая попытка установить срок удержания меньше указанного значения будет отклонена на уровне политики бакета, независимо от того, кто выполняет запрос и с какими правами.
Ключи условий Object Lock в bucket policy
При работе с Object Lock в bucket policy можно использовать следующие ключи условий:
s3:object-lock-mode,s3:object-lock-retain-until-date,s3:object-lock-remaining-retention-days,s3:object-lock-legal-hold.
С их помощью можно строить более сложные правила — например, разрешать только определенные режимы удержания, запрещать установку legal hold или ограничивать диапазон допустимых дат хранения.
Механизмы WORM и Object Lock постепенно становятся стандартом для систем хранения. При этом на практике проверить такие сценарии проще всего в объектных хранилищах с расходами по модели «pay-as-you-go» и возможностью протестировать разные режимы retention.
Если вы хотите самостоятельно оценить, как это работает, можно начать с бесплатного тестирования: в Selectel доступен тестовый период на месяц без ограничений по объему хранения.