feat(security): add THROTTLE_KEY env-var fallback for throttle.key (#4707)

checkThrottleEncryption() now consults the THROTTLE_KEY environment
variable when throttle.key is empty, mirroring the ENCRYPTION_KEY
fallback in Config/Encryption. This lets Docker/Compose deployments
supply the throttle HMAC secret without writing a shared value into a
read-only .env. An explicit throttle.key always takes precedence.

Adds regression tests to the existing security_helperTest suite and
documents THROTTLE_KEY in .env.example.
This commit is contained in:
jekkos authored and GitHub committed 2026-09-22 23:30:05 +02:00
1 parent edcb4bb655
commit 00b97c3302
3 files changed
+67 -1

No files matched your search

+4
View File
@@ -62,6 +62,10 @@ database.tests.DBPrefix='ospos_'
encryption.key=''
# Persistent secret for HMAC-hashing login-throttle cache keys. Left blank and
# provisioned on startup (php spark env:provision); independent of encryption.key.
# For Docker/Compose, pass it via the THROTTLE_KEY env var instead:
# docker run -e THROTTLE_KEY="$(openssl rand -hex 32)" opensourcepos
# THROTTLE_KEY is read as a fallback when throttle.key is empty, so no
# shared secret needs to be committed or baked into the shipped image.
throttle.key=''
#--------------------------------------------------------------------
+10 -1
View File
@@ -327,12 +327,16 @@ function checkEncryption(?CI3SecretConverter $converter = null): bool
* Returns the persistent HMAC secret used to hash login-throttle cache keys.
*
* Behaviour:
* - Key already present: returned immediately (no I/O).
* - Key already present (throttle.key or THROTTLE_KEY): returned immediately (no I/O).
* - Key missing and .env IS writable: generates a fresh key and persists it.
* - Key missing and .env NOT writable:
* throws — the key was presumably already provisioned externally
* (e.g. `php spark env:provision` at container startup).
*
* THROTTLE_KEY is a real environment-variable fallback (mirroring ENCRYPTION_KEY):
* it is consulted only when throttle.key is empty, so it can be supplied via
* Docker/Compose without writing a shared secret into .env.
*
* @return string the throttle key
* @throws RuntimeException if the key cannot be provisioned
*/
@@ -340,6 +344,11 @@ function checkThrottleEncryption(): string
{
$key = (string) env('throttle.key', '');
if ($key === '') {
$envKey = getenv('THROTTLE_KEY');
$key = $envKey === false ? '' : $envKey;
}
if ($key !== '') {
return $key;
}
+53
View File
@@ -467,6 +467,59 @@ class security_helperTest extends CIUnitTestCase
}
}
public function testCheckThrottleEncryptionFallsBackToThrottleKeyEnvVar(): void
{
$throttleKey = bin2hex(random_bytes(32));
// Clear the dot-notation throttle.key so the THROTTLE_KEY fallback is
// exercised, and leave a .env with no throttle.key so any accidental
// provisioning would be visible in the file assertion below.
putenv('throttle.key');
unset($_ENV['throttle.key'], $_SERVER['throttle.key']);
file_put_contents($this->envPath, "# tmp\n");
$previous = getenv('THROTTLE_KEY');
putenv("THROTTLE_KEY=$throttleKey");
try {
$result = checkThrottleEncryption();
$this->assertSame($throttleKey, $result);
$this->assertStringNotContainsString('throttle.key=', file_get_contents($this->envPath), 'the THROTTLE_KEY fallback must not trigger provisioning/write');
} finally {
if ($previous === false) {
putenv('THROTTLE_KEY');
} else {
putenv("THROTTLE_KEY=$previous");
}
}
}
public function testCheckThrottleEncryptionPrefersThrottleKeyOverEnvVar(): void
{
$explicit = bin2hex(random_bytes(32));
$fallback = bin2hex(random_bytes(32));
putenv("throttle.key=$explicit");
$_ENV['throttle.key'] = $explicit;
$_SERVER['throttle.key'] = $explicit;
$previous = getenv('THROTTLE_KEY');
putenv("THROTTLE_KEY=$fallback");
try {
$result = checkThrottleEncryption();
$this->assertSame($explicit, $result, 'an explicit throttle.key must take precedence over the THROTTLE_KEY fallback');
} finally {
if ($previous === false) {
putenv('THROTTLE_KEY');
} else {
putenv("THROTTLE_KEY=$previous");
}
}
}
// -- rotateEncryptionKey() — provisioning path, does write --
public function testRotateEncryptionKeyPersistsNewKey(): void