Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

rhand's avatar
Level 6

Add LE SSL to www subdomain in Laravel package

I need to upgrade a Laravel package to not only add Let's Encrypt SSL certificates for main domain but also for the https://github.com/smart48/le-ssl-laravel-package/pull/30ub.com/smart48/le-ssl-laravel-package/pull/30

Does anyone know if I need to add two challenge directories if I want to get a certificate for both www and non www or is this setup as shown below okay ? We are using basic CHALLENGE_TYPE_HTTP as not to depend on needed DNS changes by the way. Two, should the ssl_ciphers be unique per block or domain ?

Here is the new src/Core/SslService.php:

<?php

namespace Imagewize\SslManager\Core;

use Exception;
use stonemax\acme2\Client;
use stonemax\acme2\constants\CommonConstant;

class SslService
{
    /**
     * @var string
     */
    private $accountEmail;

    /**
     * @var string
     */
    private $storagePath;

    /**
     * @var string
     */
    private $challengeDirectory;

    /**
     * @var HttpService
     */
    private $httpServer;

    public function __construct(
        $accountEmail,
        $storagePath,
        $challengeDirectory,
        HttpService $httpService
    ) {
        $this->accountEmail = $accountEmail;
        $this->storagePath = $storagePath;
        $this->challengeDirectory = $challengeDirectory;
        $this->httpServer = $httpService;
    }

    public function updateCertificate($domain, $renew = true)
{
    $wwwDomain = "www." . $domain;
    
    echo "+ Starting ...\r\n";
    // staging letsencrypt service
    $staging = false;
    $client = new Client([$this->accountEmail], $this->storagePath, $staging);
    $renew = filter_var($renew, FILTER_VALIDATE_BOOLEAN);
    $order = $client->getOrder(
        [
            CommonConstant::CHALLENGE_TYPE_HTTP => [$domain, $wwwDomain], // Including both www and non-www domains
        ],
        CommonConstant::KEY_PAIR_TYPE_RSA,
        $renew
    );

    echo "+ Order expires " . $order->expires . "\r\n";

    $pendingChallenges = $order->getPendingChallengeList();

    // Update for non-www domain
    echo "+ Adding web server configuration for " . $domain . "\r\n";
    $certificateInfo = null;
    $this->httpServer->updateSite($domain, $certificateInfo);
    $this->httpServer->reloadConfiguration();

    // Update for www domain
    echo "+ Adding web server configuration for " . $wwwDomain . "\r\n";
    $this->httpServer->updateSite($wwwDomain, $certificateInfo);
    $this->httpServer->reloadConfiguration();

    echo "+ Starting challenges\r\n";
    foreach ($pendingChallenges as $challenge) {
        $challengeType = $challenge->getType();
        $credential = $challenge->getCredential();

        if ($challengeType == CommonConstant::CHALLENGE_TYPE_HTTP) {
            $domainChallengeDirectory = "{$this->challengeDirectory}/{$domain}";

            if (!file_exists($domainChallengeDirectory)) {
                mkdir($domainChallengeDirectory, 0755, true);
            }

            echo "+ Saving challenge file for " . $domain . "\r\n";
            file_put_contents(
                "{$domainChallengeDirectory}/{$credential['fileName']}",
                $credential['fileContent']
            );
        }
        echo "+ Verifying challenge for " . $domain . "\r\n";
        $challenge->verify();
    }
    
    echo "+ Getting certificate info (this can take a while)\r\n";
    $certificateInfo = $order->getCertificateFile();
    
    // Update for non-www domain
    echo "+ Writing certificate to nginx config for " . $domain . "\r\n";
    $this->httpServer->updateSite($domain, $certificateInfo);
    // Update for www domain
    echo "+ Writing certificate to nginx config for " . $wwwDomain . "\r\n";
    $this->httpServer->updateSite($wwwDomain, $certificateInfo);
    
    echo "+ Reloading web server configuration\r\n";
    $this->httpServer->reloadConfiguration();

    echo "Done!\r\n";
}

}

and site.blade.php:

server {
    listen 80;
    listen [::]:80;
    server_name {{ $domain }} www.{{ $domain }}; // Include both www and non-www versions of the domain

    location /.well-known/acme-challenge {
        default_type "text/plain";
        alias {{ $challengeDirectory }}/{{ $domain }};
    }

    @if ($certificateInfo)
        # Redirect to HTTPS version
        location / {
            return 301 https://$host$request_uri;
        }
    @else
        # Reset connection
        location / {
            return 444;
        }
    @endif
}

@if ($certificateInfo)
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name {{ $domain }}; // Non-www version of the domain
    root {{ config("ssl-manager.root_site") }};

    ssl_certificate     {{ $certificateInfo['nonWwwCertificateFullChained'] }};
    ssl_certificate_key {{ $certificateInfo['nonWwwPrivateKey'] }};

    # Improve HTTPS performance with session resumption
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 5m;

    # Enable server-side protection against BEAST attacks
    ssl_prefer_server_ciphers on;
    ssl_ciphers .....

    # Disable SSLv3
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

    # Diffie-Hellman parameter for DHE ciphersuites
    # $ sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096
    ssl_dhparam /etc/ssl/certs/dhparam.pem;

    # Enable HSTS (https://developer.mozilla.org/en-US/docs/Security/HTTP_Strict_Transport_Security)
    add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";

    index index.html index.htm index.php;

    # Enable OCSP stapling (http://blog.mozilla.org/security/2013/07/29/ocsp-stapling-in-firefox)
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate {{ $certificateInfo['nonWwwCertificateFullChained'] }};

    resolver 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 5s;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    access_log off;
    error_log  /var/log/nginx/{{ $domain }}-error.log error;

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/run/php/php8.1-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
        include fastcgi_params;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name www.{{ $domain }}; // www version of the domain
    root {{ config("ssl-manager.root_site") }};

    ssl_certificate     {{ $certificateInfo['wwwCertificateFullChained'] }};
    ssl_certificate_key {{ $certificateInfo['wwwPrivateKey'] }};

    # Improve HTTPS performance with session resumption
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 5m;

    # Enable server-side protection against BEAST attacks
    ssl_prefer_server_ciphers on;
    ssl_ciphers ...

    # Disable SSLv3
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

    # Diffie-Hellman parameter for DHE ciphersuites
    # $ sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096
    ssl_dhparam /etc/ssl/certs/dhparam.pem;

    # Enable HSTS (https://developer.mozilla.org/en-US/docs/Security/HTTP_Strict_Transport_Security)
    add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";

    index index.html index.htm index.php;

    # Enable OCSP stapling (http://blog.mozilla.org/security/2013/07/29/ocsp-stapling-in-firefox)
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate {{ $certificateInfo['wwwCertificateFullChained'] }};

    resolver 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 5s;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    access_log off;
    error_log  /var/log/nginx/www.{{ $domain }}-error.log error;

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/run/php/php8.1-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
        include fastcgi_params;
    }
}
@endif
0 likes
2 replies
LaryAI's avatar
Level 58

To add Let's Encrypt SSL certificates for both the main domain and the www subdomain in a Laravel package, you can follow these steps:

  1. Include both domains in the order: When creating the order with the ACME client, include both the main domain and the www subdomain in the domain list. This is already correctly done in the provided SslService.php code:
$order = $client->getOrder(
    [
        CommonConstant::CHALLENGE_TYPE_HTTP => [$domain, $wwwDomain],
    ],
    CommonConstant::KEY_PAIR_TYPE_RSA,
    $renew
);
  1. Single challenge directory: You do not need to create two separate challenge directories for the main domain and the www subdomain. The ACME server will send challenges for both domains, and you can serve them from the same directory. The provided site.blade.php configuration already handles this correctly by aliasing the challenge directory for both domains:
location /.well-known/acme-challenge {
    default_type "text/plain";
    alias {{ $challengeDirectory }}/{{ $domain }};
}
  1. SSL ciphers: The ssl_ciphers directive does not need to be unique per block or domain. You can use the same strong cipher suite for both the main domain and the www subdomain. However, ensure that the cipher suite you choose is compatible with the browsers and clients you expect to connect to your server.

  2. Nginx server blocks: In the site.blade.php configuration, you have separate server blocks for the main domain and the www subdomain. This is correct, and you should specify the appropriate SSL certificate files for each server block:

# Non-www version of the domain
ssl_certificate     {{ $certificateInfo['nonWwwCertificateFullChained'] }};
ssl_certificate_key {{ $certificateInfo['nonWwwPrivateKey'] }};

# www version of the domain
ssl_certificate     {{ $certificateInfo['wwwCertificateFullChained'] }};
ssl_certificate_key {{ $certificateInfo['wwwPrivateKey'] }};
  1. Reloading Nginx: After updating the Nginx configuration with the new certificates, ensure that you reload Nginx to apply the changes. This is already done in the SslService.php code:
$this->httpServer->reloadConfiguration();
  1. Testing: After implementing the changes, test both the main domain and the www subdomain to ensure that the SSL certificates are correctly installed and that the redirection from HTTP to HTTPS is working as expected.

By following these steps, you should be able to successfully add Let's Encrypt SSL certificates for both the main domain and the www subdomain in your Laravel package.

1 like
rhand's avatar
Level 6

There will be more issues I see now. The package uses certificateFullChained:

grep -rnw ~/code/le-ssl-laravel-package/  -e "certificateFullChained"
/Users/user/code/le-ssl-laravel-package//vendor/stonemax/acme2/src/helpers/CommonHelper.php:185:        $certificateFullChained = '';
/Users/user/code/le-ssl-laravel-package//vendor/stonemax/acme2/src/helpers/CommonHelper.php:194:                $certificateFullChained .= trim($match)."\n";
/Users/user/code/le-ssl-laravel-package//vendor/stonemax/acme2/src/helpers/CommonHelper.php:199:                'certificateFullChained' => trim($certificateFullChained),
/Users/user/code/le-ssl-laravel-package//vendor/stonemax/acme2/src/services/OrderService.php:404:        file_put_contents($this->_certificateFullChainedPath, $certificateMap['certificateFullChained']);
/Users/user/code/le-ssl-laravel-package//vendor/stonemax/acme2/src/services/OrderService.php:419:            'certificateFullChained' => realpath($this->_certificateFullChainedPath),
/Users/user/code/le-ssl-laravel-package//src/views/site.blade.php:31:    ssl_certificate     {{ $certificateInfo['certificateFullChained'] }};
/Users/user/code/le-ssl-laravel-package//src/views/site.blade.php:60:    ssl_trusted_certificate {{ $certificateInfo['certificateFullChained'] }};

as well as privateKey, but the new setup will use nonWwwCertificateFullChained, wwwCertificateFullChained, wwwPrivateKey and nonWwwPrivateKey

Need to find a way to either update files lik le-ssl-laravel-package/vendor/stonemax/acme2/src/helpers/CommonHelper.php and le-ssl-laravel-package/vendor/stonemax/acme2/src/services/OrderService.php or extend them. I prefer extending them as I do not want to really fork and adjust the Stonemax ACME2 package.

Also updated src/Core/HttpService.php to accept an array of domains https://github.com/smart48/le-ssl-laravel-package/pull/30/files#diff-933b34f58941812098c38c1e75c9db487c399791c6708097bc052ac66a648b01

Please or to participate in this conversation.