Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/dav/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
class ComposerStaticInitDAV
{
public static $prefixLengthsPsr4 = array (
'O' =>
'O' =>
array (
'OCA\\DAV\\' => 8,
),
);

public static $prefixDirsPsr4 = array (
'OCA\\DAV\\' =>
'OCA\\DAV\\' =>
array (
0 => __DIR__ . '/..' . '/../lib',
),
Expand Down
1 change: 1 addition & 0 deletions apps/settings/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
'OCA\\Settings\\SetupChecks\\RandomnessSecure' => $baseDir . '/../lib/SetupChecks/RandomnessSecure.php',
'OCA\\Settings\\SetupChecks\\ReadOnlyConfig' => $baseDir . '/../lib/SetupChecks/ReadOnlyConfig.php',
'OCA\\Settings\\SetupChecks\\RepairSanitizeSystemTagsAvailable' => $baseDir . '/../lib/SetupChecks/RepairSanitizeSystemTagsAvailable.php',
'OCA\\Settings\\SetupChecks\\S3SseCEncryption' => $baseDir . '/../lib/SetupChecks/S3SseCEncryption.php',
'OCA\\Settings\\SetupChecks\\SchedulingTableSize' => $baseDir . '/../lib/SetupChecks/SchedulingTableSize.php',
'OCA\\Settings\\SetupChecks\\SecurityHeaders' => $baseDir . '/../lib/SetupChecks/SecurityHeaders.php',
'OCA\\Settings\\SetupChecks\\ServerIdConfig' => $baseDir . '/../lib/SetupChecks/ServerIdConfig.php',
Expand Down
1 change: 1 addition & 0 deletions apps/settings/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ class ComposerStaticInitSettings
'OCA\\Settings\\SetupChecks\\RandomnessSecure' => __DIR__ . '/..' . '/../lib/SetupChecks/RandomnessSecure.php',
'OCA\\Settings\\SetupChecks\\ReadOnlyConfig' => __DIR__ . '/..' . '/../lib/SetupChecks/ReadOnlyConfig.php',
'OCA\\Settings\\SetupChecks\\RepairSanitizeSystemTagsAvailable' => __DIR__ . '/..' . '/../lib/SetupChecks/RepairSanitizeSystemTagsAvailable.php',
'OCA\\Settings\\SetupChecks\\S3SseCEncryption' => __DIR__ . '/..' . '/../lib/SetupChecks/S3SseCEncryption.php',
'OCA\\Settings\\SetupChecks\\SchedulingTableSize' => __DIR__ . '/..' . '/../lib/SetupChecks/SchedulingTableSize.php',
'OCA\\Settings\\SetupChecks\\SecurityHeaders' => __DIR__ . '/..' . '/../lib/SetupChecks/SecurityHeaders.php',
'OCA\\Settings\\SetupChecks\\ServerIdConfig' => __DIR__ . '/..' . '/../lib/SetupChecks/ServerIdConfig.php',
Expand Down
2 changes: 2 additions & 0 deletions apps/settings/lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
use OCA\Settings\SetupChecks\RandomnessSecure;
use OCA\Settings\SetupChecks\ReadOnlyConfig;
use OCA\Settings\SetupChecks\RepairSanitizeSystemTagsAvailable;
use OCA\Settings\SetupChecks\S3SseCEncryption;
use OCA\Settings\SetupChecks\SchedulingTableSize;
use OCA\Settings\SetupChecks\SecurityHeaders;
use OCA\Settings\SetupChecks\ServerIdConfig;
Expand Down Expand Up @@ -212,6 +213,7 @@ public function register(IRegistrationContext $context): void {
$context->registerSetupCheck(RandomnessSecure::class);
$context->registerSetupCheck(ReadOnlyConfig::class);
$context->registerSetupCheck(RepairSanitizeSystemTagsAvailable::class);
$context->registerSetupCheck(S3SseCEncryption::class);
$context->registerSetupCheck(SecurityHeaders::class);
$context->registerSetupCheck(ServerIdConfig::class);
$context->registerSetupCheck(SchedulingTableSize::class);
Expand Down
76 changes: 76 additions & 0 deletions apps/settings/lib/SetupChecks/S3SseCEncryption.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Settings\SetupChecks;

use OCP\IConfig;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\SetupCheck\ISetupCheck;
use OCP\SetupCheck\SetupResult;

/**
* Warns when the primary or multibucket S3 object store is configured to use SSE-C
* (customer-provided key) server-side encryption, deprecated since Nextcloud 34.
*
* Not to be confused with LegacySSEKeyFormat, which checks Nextcloud's own
* server-side-encryption app's legacy key file format and is unrelated to S3.
*/
class S3SseCEncryption implements ISetupCheck {
public function __construct(
private IL10N $l10n,
private IConfig $config,
private IURLGenerator $urlGenerator,
) {
}

#[\Override]
public function getName(): string {
return $this->l10n->t('S3 SSE-C encryption');
}

#[\Override]
public function getCategory(): string {
return 'security';
}

/**
* @param array $arguments the 'arguments' of an 'objectstore'/'objectstore_multibucket' config
*/
private function isSseCInEffect(array $arguments): bool {
$sse = $arguments['sse'] ?? '';
if ($sse !== '') {
return $sse === 'sse-c';
}

// No explicit 'sse' argument: matches the precedence in
// OC\Files\ObjectStore\S3ConnectionTrait::parseEncryptionParams() -- the legacy
// 'sse_c_key' argument wins over 'sse_kms_enabled' whenever both are set.
return !empty($arguments['sse_c_key']);
}

#[\Override]
public function run(): SetupResult {
foreach (['objectstore', 'objectstore_multibucket'] as $key) {
$objectStore = $this->config->getSystemValue($key, null);
if (!is_array($objectStore) || ($objectStore['class'] ?? null) !== 'OC\\Files\\ObjectStore\\S3') {
continue;
}

if ($this->isSseCInEffect($objectStore['arguments'] ?? [])) {
return SetupResult::warning(
$this->l10n->t('This instance uses S3 SSE-C (customer-provided key) encryption, deprecated since Nextcloud 34: Amazon S3 disabled SSE-C by default for new buckets in April 2026. Migrate to SSE-KMS instead.'),
$this->urlGenerator->linkToDocs('admin-s3-sse-c'),
);
}
}

return SetupResult::success($this->l10n->t('No deprecated S3 SSE-C encryption configuration found.'));
}
}
74 changes: 74 additions & 0 deletions apps/settings/tests/SetupChecks/S3SseCEncryptionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Settings\Tests\SetupChecks;

use OCA\Settings\SetupChecks\S3SseCEncryption;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\SetupCheck\SetupResult;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;

class S3SseCEncryptionTest extends TestCase {
private IL10N&MockObject $l10n;
private IConfig&MockObject $config;
private IURLGenerator&MockObject $urlGenerator;

#[\Override]
protected function setUp(): void {
parent::setUp();

$this->l10n = $this->createMock(IL10N::class);
$this->l10n->method('t')
->willReturnCallback(function ($message, array $replace = []) {
return vsprintf($message, $replace);
});
$this->config = $this->createMock(IConfig::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
}

public static function dataRun(): array {
$s3 = static fn (array $arguments): array => ['class' => 'OC\\Files\\ObjectStore\\S3', 'arguments' => $arguments];

return [
'no objectstore configured' => [null, null, SetupResult::SUCCESS],
'non-S3 objectstore class' => [['class' => 'OC\\Files\\ObjectStore\\Swift', 'arguments' => ['sse_c_key' => 'x']], null, SetupResult::SUCCESS],
'S3 with no sse* arguments' => [$s3([]), null, SetupResult::SUCCESS],
'S3 with sse-kms' => [$s3(['sse' => 'sse-kms']), null, SetupResult::SUCCESS],
'S3 with sse-kms-dsse' => [$s3(['sse' => 'sse-kms-dsse']), null, SetupResult::SUCCESS],
'S3 with legacy sse_c_key only' => [$s3(['sse_c_key' => 'x']), null, SetupResult::WARNING],
'S3 with explicit sse => sse-c' => [$s3(['sse' => 'sse-c', 'sse_c_key' => 'x']), null, SetupResult::WARNING],
'S3 with both legacy keys set (sse_c_key wins by precedence)' => [$s3(['sse_c_key' => 'x', 'sse_kms_enabled' => true]), null, SetupResult::WARNING],
'S3 with explicit sse overriding legacy sse_c_key' => [$s3(['sse' => 'sse-kms', 'sse_c_key' => 'x']), null, SetupResult::SUCCESS],
'multibucket S3 with legacy sse_c_key only' => [null, $s3(['sse_c_key' => 'x']), SetupResult::WARNING],
'multibucket S3 with sse-kms' => [null, $s3(['sse' => 'sse-kms']), SetupResult::SUCCESS],
];
}

#[\PHPUnit\Framework\Attributes\DataProvider('dataRun')]
public function testRun(?array $objectStore, ?array $objectStoreMultibucket, string $expected): void {
$this->urlGenerator->method('linkToDocs')->willReturn('admin-s3-sse-c');

$this->config->method('getSystemValue')
->willReturnCallback(function (string $key, $default = null) use ($objectStore, $objectStoreMultibucket) {
return match ($key) {
'objectstore' => $objectStore ?? $default,
'objectstore_multibucket' => $objectStoreMultibucket ?? $default,
default => $default,
};
});

$check = new S3SseCEncryption($this->l10n, $this->config, $this->urlGenerator);

$result = $check->run();
$this->assertEquals($expected, $result->getSeverity());
}
}
27 changes: 27 additions & 0 deletions config/config.sample.php
Original file line number Diff line number Diff line change
Expand Up @@ -2116,6 +2116,33 @@
// using Amazon S3 (or any other implementation that supports it) we recommend enabling it by using "when_supported".
'request_checksum_calculation' => 'when_required',
'response_checksum_validation' => 'when_required',
// optional: Server-side encryption of objects at rest.
// Valid values are:
// - '' (default): no server-side encryption requested by Nextcloud
// - 'sse-s3': S3-managed AES256 key
// - 'sse-c': customer-provided key, see 'sse_c_key' below (deprecated, see below)
// - 'sse-kms': single layer of AWS KMS-managed encryption
// - 'sse-kms-dsse': dual layer of AWS KMS-managed encryption (DSSE-KMS), higher cost
// and latency than 'sse-kms' but required by some compliance standards
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingDSSEncryption.html
'sse' => '',
// optional: customer-provided key for 'sse' => 'sse-c', base64 encoded.
// Deprecated since Nextcloud 34: Amazon S3 disabled SSE-C by default for new
// buckets in April 2026. Use 'sse' => 'sse-kms' or 'sse-kms-dsse' instead.
'sse_c_key' => '',
// optional: ARN of the AWS KMS key to use for 'sse-kms' / 'sse-kms-dsse'.
// If omitted, the bucket's default KMS key (or the AWS-managed 'aws/s3' key) is used.
// The key must be a symmetric key in the same AWS region as the bucket.
'sse_kms_key_id' => '',
// optional: 'sse-kms' / 'sse-kms-dsse' only. Additional AWS KMS encryption context,
// e.g. to scope key usage to a tenant via a matching kms:EncryptionContext condition
// on the key policy. This is stored alongside the object and visible in AWS CloudTrail,
// so it must not contain secrets.
'sse_kms_encryption_context' => [],
// optional: 'sse-kms' only, not supported for 'sse-kms-dsse'. Enables S3 Bucket Keys,
// which can reduce AWS KMS request costs by up to 99% for SSE-KMS.
'sse_kms_bucket_key' => false,
],
],

Expand Down
1 change: 1 addition & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -1884,6 +1884,7 @@
'OC\\Files\\ObjectStore\\S3' => $baseDir . '/lib/private/Files/ObjectStore/S3.php',
'OC\\Files\\ObjectStore\\S3ConfigTrait' => $baseDir . '/lib/private/Files/ObjectStore/S3ConfigTrait.php',
'OC\\Files\\ObjectStore\\S3ConnectionTrait' => $baseDir . '/lib/private/Files/ObjectStore/S3ConnectionTrait.php',
'OC\\Files\\ObjectStore\\S3EncryptionMode' => $baseDir . '/lib/private/Files/ObjectStore/S3EncryptionMode.php',
'OC\\Files\\ObjectStore\\S3ObjectTrait' => $baseDir . '/lib/private/Files/ObjectStore/S3ObjectTrait.php',
'OC\\Files\\ObjectStore\\S3Signature' => $baseDir . '/lib/private/Files/ObjectStore/S3Signature.php',
'OC\\Files\\ObjectStore\\StorageObjectStore' => $baseDir . '/lib/private/Files/ObjectStore/StorageObjectStore.php',
Expand Down
1 change: 1 addition & 0 deletions lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -1925,6 +1925,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Files\\ObjectStore\\S3' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/S3.php',
'OC\\Files\\ObjectStore\\S3ConfigTrait' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/S3ConfigTrait.php',
'OC\\Files\\ObjectStore\\S3ConnectionTrait' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/S3ConnectionTrait.php',
'OC\\Files\\ObjectStore\\S3EncryptionMode' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/S3EncryptionMode.php',
'OC\\Files\\ObjectStore\\S3ObjectTrait' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/S3ObjectTrait.php',
'OC\\Files\\ObjectStore\\S3Signature' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/S3Signature.php',
'OC\\Files\\ObjectStore\\StorageObjectStore' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/StorageObjectStore.php',
Expand Down
12 changes: 12 additions & 0 deletions lib/private/Files/ObjectStore/S3ConfigTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,16 @@ trait S3ConfigTrait {
private bool $useMultipartCopy = true;

protected int $retriesMaxAttempts;

protected S3EncryptionMode $sseMode = S3EncryptionMode::None;

protected ?string $sseKmsKeyId = null;

/** Already base64-encoded JSON, ready to hand to the AWS SDK */
protected ?string $sseKmsEncryptionContext = null;

protected bool $sseKmsBucketKey = false;

/** @var string[] Non-fatal `sse*` configuration issues found by parseEncryptionParams(), logged by logEncryptionWarnings() */
protected array $encryptionWarnings = [];
}
Loading
Loading