Skip to content

Commit dae5715

Browse files
committed
feat(settings): warn about deprecated S3 SSE-C encryption in setup checks
SSE-C is deprecated since Nextcloud 34 because Amazon S3 disabled SSE-C by default for new buckets in April 2026. Adds a setup check surfaced in Settings > Overview that warns when an S3 object store uses SSE-C, whether selected via `sse` or the deprecated `sse_c_key` argument, following the same precedence as S3ConnectionTrait::parseEncryptionParams(). The check inspects every configured S3 store in both the legacy single-store shape and the new-style objectstore map ('default', 'server1', ...), for `objectstore` and `objectstore_multibucket`, and skips invalid or non-S3 entries instead of failing. Assisted-by: ClaudeCode:claude-opus-5 Assisted-by: ClaudeCode:claude-sonnet-5-5 Signed-off-by: Stephen Cuppett <steve@cuppett.com>
1 parent 2c2a4bc commit dae5715

5 files changed

Lines changed: 195 additions & 0 deletions

File tree

‎apps/settings/composer/composer/autoload_classmap.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@
135135
'OCA\\Settings\\SetupChecks\\RandomnessSecure' => $baseDir . '/../lib/SetupChecks/RandomnessSecure.php',
136136
'OCA\\Settings\\SetupChecks\\ReadOnlyConfig' => $baseDir . '/../lib/SetupChecks/ReadOnlyConfig.php',
137137
'OCA\\Settings\\SetupChecks\\RepairSanitizeSystemTagsAvailable' => $baseDir . '/../lib/SetupChecks/RepairSanitizeSystemTagsAvailable.php',
138+
'OCA\\Settings\\SetupChecks\\S3SseCEncryption' => $baseDir . '/../lib/SetupChecks/S3SseCEncryption.php',
138139
'OCA\\Settings\\SetupChecks\\SchedulingTableSize' => $baseDir . '/../lib/SetupChecks/SchedulingTableSize.php',
139140
'OCA\\Settings\\SetupChecks\\SecurityHeaders' => $baseDir . '/../lib/SetupChecks/SecurityHeaders.php',
140141
'OCA\\Settings\\SetupChecks\\ServerIdConfig' => $baseDir . '/../lib/SetupChecks/ServerIdConfig.php',

‎apps/settings/composer/composer/autoload_static.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ class ComposerStaticInitSettings
150150
'OCA\\Settings\\SetupChecks\\RandomnessSecure' => __DIR__ . '/..' . '/../lib/SetupChecks/RandomnessSecure.php',
151151
'OCA\\Settings\\SetupChecks\\ReadOnlyConfig' => __DIR__ . '/..' . '/../lib/SetupChecks/ReadOnlyConfig.php',
152152
'OCA\\Settings\\SetupChecks\\RepairSanitizeSystemTagsAvailable' => __DIR__ . '/..' . '/../lib/SetupChecks/RepairSanitizeSystemTagsAvailable.php',
153+
'OCA\\Settings\\SetupChecks\\S3SseCEncryption' => __DIR__ . '/..' . '/../lib/SetupChecks/S3SseCEncryption.php',
153154
'OCA\\Settings\\SetupChecks\\SchedulingTableSize' => __DIR__ . '/..' . '/../lib/SetupChecks/SchedulingTableSize.php',
154155
'OCA\\Settings\\SetupChecks\\SecurityHeaders' => __DIR__ . '/..' . '/../lib/SetupChecks/SecurityHeaders.php',
155156
'OCA\\Settings\\SetupChecks\\ServerIdConfig' => __DIR__ . '/..' . '/../lib/SetupChecks/ServerIdConfig.php',

‎apps/settings/lib/AppInfo/Application.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
use OCA\Settings\SetupChecks\RandomnessSecure;
7070
use OCA\Settings\SetupChecks\ReadOnlyConfig;
7171
use OCA\Settings\SetupChecks\RepairSanitizeSystemTagsAvailable;
72+
use OCA\Settings\SetupChecks\S3SseCEncryption;
7273
use OCA\Settings\SetupChecks\SchedulingTableSize;
7374
use OCA\Settings\SetupChecks\SecurityHeaders;
7475
use OCA\Settings\SetupChecks\ServerIdConfig;
@@ -212,6 +213,7 @@ public function register(IRegistrationContext $context): void {
212213
$context->registerSetupCheck(RandomnessSecure::class);
213214
$context->registerSetupCheck(ReadOnlyConfig::class);
214215
$context->registerSetupCheck(RepairSanitizeSystemTagsAvailable::class);
216+
$context->registerSetupCheck(S3SseCEncryption::class);
215217
$context->registerSetupCheck(SecurityHeaders::class);
216218
$context->registerSetupCheck(ServerIdConfig::class);
217219
$context->registerSetupCheck(SchedulingTableSize::class);
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Settings\SetupChecks;
11+
12+
use OCP\IConfig;
13+
use OCP\IL10N;
14+
use OCP\IURLGenerator;
15+
use OCP\SetupCheck\ISetupCheck;
16+
use OCP\SetupCheck\SetupResult;
17+
18+
/**
19+
* Warns when the primary or multibucket S3 object store is configured to use SSE-C
20+
* (customer-provided key) server-side encryption, deprecated since Nextcloud 34.
21+
*
22+
* Checks every configured S3 store, in both the legacy single-store and the new-style
23+
* objectstore map shapes.
24+
*
25+
* Not to be confused with LegacySSEKeyFormat, which checks Nextcloud's own
26+
* server-side-encryption app's legacy key file format and is unrelated to S3.
27+
*/
28+
class S3SseCEncryption implements ISetupCheck {
29+
public function __construct(
30+
private IL10N $l10n,
31+
private IConfig $config,
32+
private IURLGenerator $urlGenerator,
33+
) {
34+
}
35+
36+
#[\Override]
37+
public function getName(): string {
38+
return $this->l10n->t('S3 SSE-C encryption');
39+
}
40+
41+
#[\Override]
42+
public function getCategory(): string {
43+
return 'security';
44+
}
45+
46+
/**
47+
* @param array $arguments the 'arguments' of an 'objectstore'/'objectstore_multibucket' config
48+
*/
49+
private function isSseCInEffect(array $arguments): bool {
50+
// An explicit 'sse' argument always wins, including 'sse' => '' (no encryption),
51+
// matching the precedence in S3ConnectionTrait::parseEncryptionParams().
52+
if (array_key_exists('sse', $arguments)) {
53+
return $arguments['sse'] === 'sse-c';
54+
}
55+
56+
// No explicit 'sse' argument: the legacy 'sse_c_key' argument wins over
57+
// 'sse_kms_enabled' whenever both are set, as it did in Nextcloud 34.
58+
return !empty($arguments['sse_c_key']);
59+
}
60+
61+
/**
62+
* Collect the 'arguments' of every S3 store in one objectstore config key.
63+
*
64+
* Covers both shapes the configuration can take: the legacy single-store form
65+
* (`['class' => ..., 'arguments' => [...]]`) and the new-style map, where
66+
* 'default'/'root'/'preview' reference named stores such as 'server1' (see
67+
* OC\Files\ObjectStore\PrimaryObjectStoreConfig::getObjectStoreConfigs()).
68+
* Invalid or non-S3 entries are skipped instead of thrown, so a broken object
69+
* store config cannot take the whole setup check down with it.
70+
*
71+
* @return array[] the 'arguments' of each configured S3 store
72+
*/
73+
private function getS3Arguments(string $key): array {
74+
$config = $this->config->getSystemValue($key, null);
75+
if (!is_array($config)) {
76+
return [];
77+
}
78+
79+
// The new-style map is recognised by 'default' naming another entry, exactly as
80+
// PrimaryObjectStoreConfig does; anything else is a single store definition.
81+
$stores = (isset($config['default']) && is_string($config['default'])) ? $config : [$config];
82+
83+
$arguments = [];
84+
foreach ($stores as $store) {
85+
if (is_array($store) && ($store['class'] ?? null) === 'OC\\Files\\ObjectStore\\S3') {
86+
$arguments[] = is_array($store['arguments'] ?? null) ? $store['arguments'] : [];
87+
}
88+
}
89+
90+
return $arguments;
91+
}
92+
93+
#[\Override]
94+
public function run(): SetupResult {
95+
$arguments = array_merge(
96+
$this->getS3Arguments('objectstore'),
97+
$this->getS3Arguments('objectstore_multibucket'),
98+
);
99+
100+
foreach ($arguments as $argument) {
101+
if ($this->isSseCInEffect($argument)) {
102+
return SetupResult::warning(
103+
$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.'),
104+
$this->urlGenerator->linkToDocs('admin-s3-sse-c'),
105+
);
106+
}
107+
}
108+
109+
return SetupResult::success($this->l10n->t('No deprecated S3 SSE-C encryption configuration found.'));
110+
}
111+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Settings\Tests\SetupChecks;
11+
12+
use OCA\Settings\SetupChecks\S3SseCEncryption;
13+
use OCP\IConfig;
14+
use OCP\IL10N;
15+
use OCP\IURLGenerator;
16+
use OCP\SetupCheck\SetupResult;
17+
use PHPUnit\Framework\MockObject\MockObject;
18+
use Test\TestCase;
19+
20+
class S3SseCEncryptionTest extends TestCase {
21+
private IL10N&MockObject $l10n;
22+
private IConfig&MockObject $config;
23+
private IURLGenerator&MockObject $urlGenerator;
24+
25+
#[\Override]
26+
protected function setUp(): void {
27+
parent::setUp();
28+
29+
$this->l10n = $this->createMock(IL10N::class);
30+
$this->l10n->method('t')
31+
->willReturnCallback(function ($message, array $replace = []) {
32+
return vsprintf($message, $replace);
33+
});
34+
$this->config = $this->createMock(IConfig::class);
35+
$this->urlGenerator = $this->createMock(IURLGenerator::class);
36+
}
37+
38+
public static function dataRun(): array {
39+
$s3 = static fn (array $arguments): array => ['class' => 'OC\\Files\\ObjectStore\\S3', 'arguments' => $arguments];
40+
41+
return [
42+
'no objectstore configured' => [null, null, SetupResult::SUCCESS],
43+
'non-S3 objectstore class' => [['class' => 'OC\\Files\\ObjectStore\\Swift', 'arguments' => ['sse_c_key' => 'x']], null, SetupResult::SUCCESS],
44+
'S3 with no sse* arguments' => [$s3([]), null, SetupResult::SUCCESS],
45+
'S3 with sse-kms' => [$s3(['sse' => 'sse-kms']), null, SetupResult::SUCCESS],
46+
'S3 with sse-kms-dsse' => [$s3(['sse' => 'sse-kms-dsse']), null, SetupResult::SUCCESS],
47+
'S3 with legacy sse_c_key only' => [$s3(['sse_c_key' => 'x']), null, SetupResult::WARNING],
48+
'S3 with explicit sse => sse-c' => [$s3(['sse' => 'sse-c', 'sse_c_key' => 'x']), null, SetupResult::WARNING],
49+
'S3 with both legacy keys set (sse_c_key wins by precedence)' => [$s3(['sse_c_key' => 'x', 'sse_kms_enabled' => true]), null, SetupResult::WARNING],
50+
'S3 with explicit sse overriding legacy sse_c_key' => [$s3(['sse' => 'sse-kms', 'sse_c_key' => 'x']), null, SetupResult::SUCCESS],
51+
'multibucket S3 with legacy sse_c_key only' => [null, $s3(['sse_c_key' => 'x']), SetupResult::WARNING],
52+
'multibucket S3 with sse-kms' => [null, $s3(['sse' => 'sse-kms']), SetupResult::SUCCESS],
53+
'S3 with explicit empty sse overriding legacy sse_c_key' => [$s3(['sse' => '', 'sse_c_key' => 'x']), null, SetupResult::SUCCESS],
54+
'new-style map with sse-c on the default store' => [['default' => 'server1', 'server1' => $s3(['sse' => 'sse-c', 'sse_c_key' => 'x'])], null, SetupResult::WARNING],
55+
'new-style map with sse-c on a non-default store' => [['default' => 'server1', 'server1' => $s3([]), 'server2' => $s3(['sse_c_key' => 'x'])], null, SetupResult::WARNING],
56+
'new-style map without sse-c' => [['default' => 'server1', 'server1' => $s3(['sse' => 'sse-kms'])], null, SetupResult::SUCCESS],
57+
'new-style map skipping non-S3 and malformed entries' => [['default' => 'server1', 'server1' => ['class' => 'OC\\Files\\ObjectStore\\Swift', 'arguments' => ['sse_c_key' => 'x']], 'server2' => 'invalid', 'server3' => ['class' => 'OC\\Files\\ObjectStore\\S3']], null, SetupResult::SUCCESS],
58+
'new-style multibucket map with sse-c' => [null, ['default' => 'server1', 'server1' => $s3(['sse_c_key' => 'x'])], SetupResult::WARNING],
59+
];
60+
}
61+
62+
#[\PHPUnit\Framework\Attributes\DataProvider('dataRun')]
63+
public function testRun(?array $objectStore, ?array $objectStoreMultibucket, string $expected): void {
64+
$this->urlGenerator->method('linkToDocs')->willReturn('admin-s3-sse-c');
65+
66+
$this->config->method('getSystemValue')
67+
->willReturnCallback(function (string $key, $default = null) use ($objectStore, $objectStoreMultibucket) {
68+
return match ($key) {
69+
'objectstore' => $objectStore ?? $default,
70+
'objectstore_multibucket' => $objectStoreMultibucket ?? $default,
71+
default => $default,
72+
};
73+
});
74+
75+
$check = new S3SseCEncryption($this->l10n, $this->config, $this->urlGenerator);
76+
77+
$result = $check->run();
78+
$this->assertEquals($expected, $result->getSeverity());
79+
}
80+
}

0 commit comments

Comments
 (0)