{"record":{"id":"e2270ca760097e1e","repo":"getgrav/grav","slug":"failed-to-commit-nonce-key-file","errorCode":null,"errorMessage":"Failed to commit nonce key file","messagePattern":"Failed to commit nonce key file","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"system/src/Grav/Common/Security.php","lineNumber":2027,"sourceCode":"    private static function writeNonceKey(string $path, string $value): void\n    {\n        $escaped = var_export($value, true);\n        $contents = \"<?php\\n\\n// Auto-generated private secret. Do NOT commit to version control.\\n// Used for CSRF nonce signing and admin rate-limit hashing. Regenerate by\\n// deleting this file; the next request will write a new value.\\n\\nreturn {$escaped};\\n\";\n\n        $dir = dirname($path);\n        if (!is_dir($dir)) {\n            Folder::create($dir);\n        }\n\n        // Atomic write: stage to a temp file, fsync via rename.\n        $tmp = $path . '.tmp';\n        if (@file_put_contents($tmp, $contents, LOCK_EX) === false) {\n            throw new RuntimeException('Failed to write nonce key file');\n        }\n        @chmod($tmp, 0600);\n        if (!@rename($tmp, $path)) {\n            @unlink($tmp);\n            throw new RuntimeException('Failed to commit nonce key file');\n        }\n    }\n}\n","sourceCodeStart":2009,"sourceCodeEnd":2031,"githubUrl":"https://github.com/getgrav/grav/blob/6040efed04efa69b8209448ed81308e7c24147c2/system/src/Grav/Common/Security.php#L2009-L2031","documentation":"Thrown by Security::writeNonceKey() when the staged temp file (user/config/security-private.php.tmp) cannot be renamed over user/config/security-private.php. Grav generates that file once to hold the secret used for CSRF nonce signing and admin rate-limit hashing, writing it atomically (temp file + LOCK_EX + rename) so a crash never leaves a half-written key. A failure here means the filesystem refused the final commit of the rename, not the content write (a failure creating the temp file throws the distinct 'Failed to write nonce key file' one line earlier).","triggerScenarios":"First request after a fresh install, after deleting security-private.php to rotate the key, or during the one-time migration from <= 2.0.0-beta.2 while user/config/ is not writable by the PHP user; an existing security-private.php or leftover .tmp owned by root or another user (run bin/grav as root, then serve as www-data) so rename() cannot replace it; read-only mount for user/ in a container; on Windows, the destination file locked by a concurrent request or antivirus scanner.","commonSituations":"Skipping Grav's permissions setup on install; deploying with root-created files then serving as the web-server user; Docker/Kubernetes images mounting user/ read-only; racing first requests both regenerating the key; CI pipelines writing the config folder as a different user.","solutions":["Make the config folder writable by the web-server/PHP user: chown -R www-data:www-data user/config && chmod 755 user/config, then reload the page","Remove stale foreign-owned artifacts and let Grav regenerate: rm -f user/config/security-private.php user/config/security-private.php.tmp (rotation invalidates in-flight CSRF nonces and, with system.session.uniqueness: security, existing sessions)","If user/ is on a read-only mount, remount it (or at least user/config) on a writable volume","Verify as the serving user before retrying: sudo -u www-data php -r 'var_dump(is_writable(\"user/config\"));'"],"exampleFix":"# before: key file created by root during a root-run CLI task\n$ ls -l user/config/security-private.php\n-rw------- 1 root root 4096 security-private.php\n\n# after: hand ownership to the web server user and rotate the key\n$ chown -R www-data:www-data user/config\n$ rm -f user/config/security-private.php user/config/security-private.php.tmp\n$ sudo -u www-data php index.php   # next request writes a fresh key","handlingStrategy":"validation","validationCode":"// before triggering key generation/rotation (first request, migration), verify the config folder:\n$configFolder = $grav['locator']->findResource('config://', true);\nif (!is_dir($configFolder) || !is_writable($configFolder)) {\n    throw new RuntimeException(\"user/config ({$configFolder}) must exist and be writable by the web server user\");\n}","typeGuard":null,"tryCatchPattern":"try { Grav\\Common\\Security::getNonceKey(); } catch (RuntimeException $e) { // report permissions problem, point at user/config ownership\n    log_error($e->getMessage()); serve_503_retry_after_fix(); }","preventionTips":["Run bin/grav and cron jobs as the same user that serves HTTP requests","Apply Grav's documented permissions (user/ owned by the web-server user) as part of every deploy","Mount user/ on a writable volume in containers; never mount it read-only","Watch for leftover security-private.php.tmp files after crashes and remove them"],"tags":["filesystem","permissions","installation","csrf","nonce"],"backgroundTag":"file-write-permission-denied","analyzedSha":"6040efed04efa69b8209448ed81308e7c24147c2","analyzedAt":"2026-08-17T05:07:31.593Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}