Skip to content

Instantly share code, notes, and snippets.

@gilbitron
Created June 16, 2021 10:55
Show Gist options
  • Save gilbitron/36d48eb751875bebdf43da0a91c9faec to your computer and use it in GitHub Desktop.
Save gilbitron/36d48eb751875bebdf43da0a91c9faec to your computer and use it in GitHub Desktop.
Enabling HTTPS (SSL) for Laravel Sail using Caddy
<?php
# app/Http/Controllers/CaddyController.php
namespace App\Http\Controllers;
use App\Store;
use Illuminate\Http\Request;
class CaddyController extends Controller
{
public function check(Request $request)
{
$authorizedDomains = [
'laravel.test',
'www.laravel.test',
// Add subdomains here
];
if (in_array($request->query('domain'), $authorizedDomains)) {
return response('Domain Authorized');
}
// Abort if there's no 200 response returned above
abort(503);
}
}
# docker/Caddyfile
{
on_demand_tls {
ask http://laravel.test/caddy-check
}
local_certs
}
:443 {
tls internal {
on_demand
}
reverse_proxy laravel.test {
header_up Host {host}
header_up X-Real-IP {remote}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Port {server_port}
header_up X-Forwarded-Proto {scheme}
health_timeout 5s
}
}
services:
caddy:
image: caddy:latest
restart: unless-stopped
ports:
- '80:80'
- '443:443'
volumes:
- './docker/Caddyfile:/etc/caddy/Caddyfile'
- sailcaddy:/data
- sailcaddy:/config
networks:
- sail
# Remove "ports" from laravel.test service
volumes:
sailcaddy:
driver: local
<?php
# routes/web.php
Route::get('/caddy-check', 'CaddyController@check');
@Nilpo
Copy link

Nilpo commented Oct 28, 2022

I've been trying to get this solution working for days. The setup is simple enough, but Caddy can't verify the domains because it creates a redirect loop. Caddy sends a request to http://laravel.test/caddy-check, which it then redirects to https://laravel.test/caddy-check causing the request to fail with the error following http redirects is not allowed. Since there doesn't seem to be a way to exclude that single endpoint, the whole solution is a wash.

@clytras
Copy link

clytras commented Oct 29, 2022

@Nilpo that sounds like you don't handle the caddy-check route properly or it isn't configured properly.

I have this inside routes:

Route::get('caddy-check', [CaddyController::class, 'check']);

the CaddyController looks like this:

class CaddyController extends Controller
{
    public function check(Request $request)
    {
        $domain = $request->query('domain');
        $domains = collect(config('app.domains'));
        $domainsRegExp = regex_escape(collect($domains)->join('|'), '.');

        if (preg_match("/$domainsRegExp/", $domain)) {
            return response('Domain Authorized');
        }

        // Abort if there's no 200 response returned above
        abort(503);
    }
}

and inside Caddyfile I have the on demand TLS like this (note that I'm only uing this with /~~local/ route prefix).

{
	# debug

	on_demand_tls {
		ask http://{$HOST_DOMAIN}/~~local/caddy-check
	}

	local_certs
}

You should also check the logs and of course see what Caddy is doing by attaching the caddy container output using sail logs -f caddy.

@Nilpo
Copy link

Nilpo commented Oct 29, 2022

@Nilpo that sounds like you don't handle the caddy-check route properly or it isn't configured properly.

I have this inside routes:

Route::get('caddy-check', [CaddyController::class, 'check']);

the CaddyController looks like this:

class CaddyController extends Controller
{
    public function check(Request $request)
    {
        $domain = $request->query('domain');
        $domains = collect(config('app.domains'));
        $domainsRegExp = regex_escape(collect($domains)->join('|'), '.');

        if (preg_match("/$domainsRegExp/", $domain)) {
            return response('Domain Authorized');
        }

        // Abort if there's no 200 response returned above
        abort(503);
    }
}

and inside Caddyfile I have the on demand TLS like this (note that I'm only uing this with /~~local/ route prefix).

{
	# debug

	on_demand_tls {
		ask http://{$HOST_DOMAIN}/~~local/caddy-check
	}

	local_certs
}

You should also check the logs and of course see what Caddy is doing by attaching the caddy container output using sail logs -f caddy.

I'm using the exact code above. I posted the exact error from the logs. The configuration as posted creates a redirect loop.

It's not the handler, it never gets executed because Caddy won't process the redirect.

A call to http://laravel.test/caddy-check is handled by Caddy which transforms it to https but then refuses to follow it to complete the domain verification. So the request fails and Caddy sends the browser an SSL error.

@Maxime-Missichini
Copy link

Maxime-Missichini commented Dec 2, 2022

@Nilpo Hi, it may be a late reply but I managed to solve the same error, here is what I did :
Inside CaddyController:
$authorizedDomains = [ 'localhost', // Add subdomains here ];
This way I can access via https://localhost
Then the error was because of the Caddyfile, as you said when this error occurs we don't even reach the controller.
Here is my Caddyfile:


{
    on_demand_tls {
        ask http://laravel.test/caddy-check
    }
    local_certs
}

:443 {
    tls internal {
        on_demand
    }

    reverse_proxy laravel.test {
        header_up Host {host}
        header_up X-Real-IP {remote}
        header_up X-Forwarded-For {remote}
        header_up X-Forwarded-Port {server_port}
        header_up X-Forwarded-Proto {scheme}

        health_timeout 5s
    }
}

Notice that laravel.test is the name of the reverse proxy service !
My on_demand_tls was set to : ask http://localhost/caddy-check, which caused the failure ...
Don't forget to delete and rebuild your container after modifications on the Caddyfile, otherwise it will keep the old one.

@Nilpo
Copy link

Nilpo commented Dec 3, 2022

Hi @Maxime-Missichini Thanks for the reply. This still causes the same redirect loop. I've even tried modifying the authorized domains as follows, which accounts for every possibility.

$authorizedDomains = [
    env('APP_SERVICE'),
    'www.' . env('APP_SERVICE'),
    'localhost',
];

But as I mentioned in my last comment, this has no effect because the Caddy handler never gets called. Caddy kills the request with an SSL error and never hands the request off to Laravel for routing.

For the record, APP_SERVICE and the Sail service name are both the same (laravel.test, localhost, or other) and the test domain is resolvable. The failure is with Caddy.

You can view my fork for more details.

The problems is ultimately that the ask URL cannot contain an https protocol, but Caddy refuses to complete the request if it doesn't.

I'm using Laravel Sail in Laravel 9 based on the PHP 8.1 image and Ubuntu 22.04

I'll create an empty project that reproduces this error and create a GitHub repo.

EDIT:

Here's the sample repo.

https://github.com/Nilpo/caddy-laravelsail

@franz-deleon
Copy link

franz-deleon commented Dec 4, 2022

@Nilpo
try changing your APP_PORT env variable (or whatever that new variable is called now) to a different port like
APP_PORT=8080

referencing the docker-compose.yml

 services:
    laravel.test:
       ports:
            - '${APP_PORT:-80}:80' 

@Nilpo
Copy link

Nilpo commented Dec 4, 2022

Well, here's a weird one. I wonder if this is a Mac issue.

If you clone the sample repo I made (or start with a clean Laravel project), the first build doesn't work. But if you immediately force a rebuild with no changes, it works. (I'm starting with a clean Docker install. No locally downloaded images or containers. The strange thing is that the rebuild shouldn't actually change anything. The volumes are all the same too since there's no file changes.

git clone https://github.com/Nilpo/caddy-laravelsail
cd caddy-laravelsail && composer install
./vendor/bin/sail up -d
    # opening https://caddy-laravelsail.test fails
./vendor/bin/sail up --build -d
    # opening https://caddy-laravelsail.test succeeds

@ethanclevenger91
Copy link

If the domains are going to be hard-coded into that controller anyway, why not put them in the Caddyfile?

# docker/Caddyfile

laravel.test, www.laravel.test {
    tls internal

    reverse_proxy laravel.test {
        header_up Host {host}
        header_up X-Real-IP {remote}
        header_up X-Forwarded-For {remote}
        header_up X-Forwarded-Port {server_port}
        header_up X-Forwarded-Proto {scheme}

        health_timeout 5s
    }
}

@mreduar
Copy link

mreduar commented Mar 23, 2023

Is there any way to do it without creating code in the application? In theory development things should not have to be in the production project. Also, not everyone in the team necessarily has to use docker or sail. It causes me noise to create a route and a controller

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