Skip to content

laravel cloud support#35

Closed
tanthammar wants to merge 2 commits into
olssonm:mainfrom
tanthammar:fix/resolve-cert-path-on-local-disk
Closed

laravel cloud support#35
tanthammar wants to merge 2 commits into
olssonm:mainfrom
tanthammar:fix/resolve-cert-path-on-local-disk

Conversation

@tanthammar

Copy link
Copy Markdown

Suggesting solution to support storing certs on external buckets.

fix #34

@olssonm

olssonm commented Jun 15, 2026

Copy link
Copy Markdown
Owner

Awesome! Will look into it, just have to make sure that it wont clash with the other ways of loading (I know @vinkla among other had specific use cases as in #18) to provide backwards compatibility.

Will check it out as soon as possible.

@vinkla

vinkla commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Laravel Cloud is supported and I solved it by running the custom command php artisan swish:deploy in my build commands. However, it was necessary to set a custom environment variable before the command, which was provided by the support team. I'm not sure if I'm allowed to share it. I believe it was @joedixon who shared the environment variable in Plain issue T-116155.

<?php

declare(strict_types=1);

namespace App\Console\Commands;

use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;

final class SwishDeploy extends Command
{
    protected $signature = 'swish:deploy';

    protected $description = 'Deploy the Swish certificates from the bucket to the instance disk';

    public function handle()
    {
        $this->info('Starting deployment of Swish certificates...');

        try {
            // Get all files from the swish directory in the bucket
            $files = Storage::allFiles('swish');

            if (count($files) === 0) {
                $this->info('No files found in the swish directory.');

                return 0;
            }

            $this->info('Found ' . count($files) . ' files to copy.');

            // Copy each file from bucket to local storage private directory
            foreach ($files as $file) {
                $contents = Storage::get($file);

                Storage::disk('local')->put($file, $contents);

                $this->line("Copied: {$file}");
            }

            $this->info('Swish certificates deployment completed successfully.');

            return 0;
        } catch (Exception $exception) {
            $this->error('Deployment failed: ' . $exception->getMessage());

            return 1;
        }
    }
}

@olssonm

olssonm commented Jun 16, 2026

Copy link
Copy Markdown
Owner

@vinkla Thanks for your input and info!

@tanthammar I think an external copy/deployment is the way to go. Feels odd that this package should handle the file syncing and/or copying of the certificates. While a great and nifty idea that solves a real issue – it's still outside of this package's scope. It would also introduce issues with "cold starts" for the initial copy and such, and any such errors is not something that should be managed or debugged within this package.

I would happily take a separate PR that adds info to the readme.md regarding Laravel Cloud-specific deployments and how one would go about and deploy it. I myself have yet to try it, most of my apps that utilize Swish still runs on Forge 😅

And idea for a future version would be to implement CURLOPT_SSLCERT_BLOB (https://www.php.net/manual/en/curl.constants.php#constant.curlopt-sslcert-blob), where the certificate could be stored as a blob/string and then be injected at runtime in Laravel Cloud, either directly via S3, via a secrets manager or similar, or even stored as an environment variable in .env.

@olssonm olssonm closed this Jun 18, 2026
@tanthammar

Copy link
Copy Markdown
Author

@olssonm — Thank you for the feedback, I agree that the suggested solution to copy certs is awkward.

I have an external deploy-command route like you and @vinkla suggested, and it works. One extra wrinkle worth flagging for the docs/blob discussion though.

The "missing cert" errors come from the service provider resolving the path via app('filesystem')->path(), i.e. the default disk. On Laravel Cloud the default disk is S3, whose path() returns the bare object key (swish/client.pem), which cURL can't open. Two ways around it: like @olssonm says, set an absolute path (which isAbsolutePath() passes through untouched), or resolve against local explicitly.

The other gotcha is Laravel Cloud replicas: a deploy command runs once in a single container, but replicas web instances boot from the built Docker image and never run the deploy commands — each has its own ephemeral disk. So the cert has to be copied onto the instance at runtime (a small JIT copy on first use), not just at deploy time.

CURLOPT_SSLCERT_BLOB would be the clean long-term answer. In the meantime, would you accept a small PR adding a swish.certificates.disk config key, so resolvePath() can target a named disk instead of the default? No file-syncing in the package — just disk selection.

I tried setting the cert as base64 env var but Laravel Cloud quietly removed it when trying to save env settings. Probably some length issue. I reported it but have yet to get a support answer.


For reference, here's what I currently have.

Deploy command (copies the cert from the private S3 bucket onto the local disk):

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;

/**
 * Laravel cloud is serverless, this is used as deploy command.
 *
 * Copies the Swish client certificate from the private S3 bucket onto the
 * defined local path — cURL needs a real local filesystem path for client mTLS.
 *
 * Local host must have test cert on path
 */
class InstallSwishCertificate extends Command
{
    protected $signature = 'swish:install-certificate';

    protected $description = 'Fetch the Swish client certificate from the private S3 bucket onto the local disk';

    public function handle(): int
    {
        /* the package's own config key — keeps command + package in sync */
        $path = config('swish.certificates.client');

        if (blank($path)) {
            $this->warn('SWISH_CLIENT_CERTIFICATE_PATH not set — skipping.');

            return self::SUCCESS;
        }

        /* the real filesystem path cURL will use for mTLS — log it so deploys are verifiable */
        $absolutePath = Storage::disk('local')->path($path);

        if (Storage::disk('local')->exists($path)) {
            $this->info("Swish client certificate already at {$absolutePath}");

            return self::SUCCESS;
        }

        Storage::disk('local')->put($path, Storage::disk('private')->get($path));

        if (Storage::disk('local')->exists($path)) {
            $this->info("Verified that certificate is at {$absolutePath}");
        } else {
            $this->error("Failed to write Swish client certificate to {$absolutePath}");

            return self::FAILURE;
        }

        $this->info("Swish client certificate fetched from private bucket → {$absolutePath}");

        return self::SUCCESS;
    }
}

AppServiceProvider — the JIT copy + resolving against the local disk explicitly, since replicas never run the deploy command:

private function registerSwishClient(): void
{
    $this->app->singleton('swish', function (): SwishClient {
        $key = config('swish.certificates.client');

        if (filled($key)
            && ! Storage::disk('local')->exists($key)
            && Storage::disk('private')->exists($key)
        ) {
            Storage::disk('local')->put($key, Storage::disk('private')->get($key));
        }

        $certificate = new Certificate(
            clientPath: filled($key) ? Storage::disk('local')->path($key) : null,
            passphrase: config('swish.certificates.password'),
            rootPath: config('swish.certificates.root'),
            signingPath: config('swish.certificates.signing'),
            signingPassphrase: config('swish.certificates.signing_password'),
        );

        return new SwishClient($certificate, config('swish.endpoint'));
    });
}

@olssonm

olssonm commented Jun 18, 2026

Copy link
Copy Markdown
Owner

@tanthammar Of course! Sound like a good idea; just a simple swish.certificates.disk-option and env (SWISH_CERTIFICATE_DISK) would be a solid addition. Use filesystems.default as the fallback to not introduce backcompat.-issues. Something like this I guess:

$disk = $config->get('swish.certificates.disk') ?: $config->get('filesystems.default', 'local');

Thank you so much for your info regarding Laravel Cloud too, did not know about those caveats 👍 Will definitely need to look into that as more and more people will probably run into the same issue (I do get quite a lot of support mail regarding Swish-certficates and such).

@vinkla

vinkla commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

@tanthammar You should be able to run the Swish certification file copying as a build command, which means you won't need to load it at runtime. As you mentioned, it needs to be loaded with an absolute path. To run it as a build command, you need a specific Laravel Cloud environment variable enabled, which you add in front of your build command. Please ask support about this, as I'm not sure if I'm allowed to share it on GitHub.

@tanthammar

Copy link
Copy Markdown
Author

@vinkla Interesting, will ask support and report back here. Won't submit any PR until then:)
Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Client cert path breaks when the default filesystem disk isn't local (e.g. Laravel Cloud)

3 participants