url() only joins the disk's url base and the path; it promises no access. A private file needs a temporary URL carrying its own expiry and signature, which the local disk issues because the Laravel 13 2,157 skeleton gives it 'serve' => true:
$disk = Storage::disk('local');
$path = 'invoices/1.pdf';
echo $disk->size($path), ' bytes, ', $disk->mimeType($path), ', md5 ', $disk->checksum($path);
$url = $disk->temporaryUrl($path, now()->plus(minutes: 5));
echo str_replace(['?', '&'], ["\n ?", "\n &"], "\n$url"), PHP_EOL;Output
593 bytes, application/pdf, md5 ff17391b4794340095cde46a94d350fe http://127.0.0.1:8317/storage/invoices/1.pdf ?expires=1790153994 &signature=1213968e60496edcfa2d976fae186f4a10667a44df865dc00ffb62fb7fdc37c0
curl 3,008 on that link returned 200 application/pdf; with 1.pdf edited to 2.pdf, or the query string removed, it returned 403.
Storage::download($path, $name) returns an attachment response for a controller that authorizes first, and temporaryUploadUrl() signs an upload instead.