Caddyfile: how do I do this?

1. Output of caddy version:

v2.6.1 h1:EDqo59TyYWhXQnfde93Mmv4FJfYe00dO60zMiEt+pzo=

2. How I run Caddy:

Official docker image: caddy:2.6.1-alpine

a. System environment:

Docker Desktop for Mac
4.3.1 (72247)

b. Command:

Paste command here.

c. Service/unit/compose file:

version: '3.4'

services:
  caddy:
    build:
      context: .
      dockerfile: Dockerfile
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - caddy_data:/data
      - caddy_config:/config
    env_file:
      - '.env'
    networks:
      default:
        aliases:
          - 'api.mycompany.com'

volumes:
  caddy_data:
    # external: true
  caddy_config:

networks:
  default:
    external:
      name: development

d. My complete Caddy config:

# global options
{
	auto_https off
	debug

	log {
		output stdout
		format console
		level DEBUG
	}

	servers {
		metrics
	}
}

# reusable snippets
(oauth2_protected) {
	oauth2_token_introspection {
		token_location bearer_token
		introspection_endpoint {$INTROSPECTION_SERVICE_URI}/v1/oauth2/introspect
		introspection_authentication_strategy client_credentials
		introspection_client_id {$INTROSPECTION_CLIENT_ID}
		introspection_client_secret {$INTROSPECTION_CLIENT_SECRET}
		introspection_timeout 1000
	}
}

http://{$API_HOST}:8080 {
	@protected_paths {
		path /v1/hello
		path /v1/goodbye
	}
	route {
		import oauth2_protected
		reverse_proxy @protected_paths {$UPSTREAM_A_URI}
	}

	@public_paths {
		path /v1/foo
		path /v1/bar
	}
	route {
		reverse_proxy @public_paths {$UPSTREAM_B_URI}
	}
}

3. The problem I’m having:

I’m attempting to use Caddy as an API Gateway that sits in front of (e.g. reverse_proxy’s) a bunch of different backends. Some of the paths should be protected (I’m using an OAuth2 Token Introspection module here to make sure the request contains a valid OAuth2 token before reverse proxying to the backend, but this is sort of irrelevant for the issue I’m having) while others should be publicly available and don’t require this protection.

Since there are lots of different paths that get mapped to a specific backend, I assumed that I could put all of the paths that should get protected into a named matcher (e.g. @protected_paths) to avoid having to duplicate the oauth2_protected and reverse_proxy directives over and over again for each path. In other words I’m trying avoid duplication like this:

route /v1/hello {
	import oauth2_protected
	reverse_proxy  {$UPSTREAM_A_URI}
}

route /v1/goodbye {
	import oauth2_protected
	reverse_proxy {$UPSTREAM_A_URI}
}

And similarly set up named matchers for the public paths as well.

However, when set up this way, if I make a request to one of the public paths (say, /v1/foo) that should be served by upstream B (publicly available, not protected by OAuth), the OAuth2 module is being invoked on that path, so it seems to be getting matched to this route for some reason:

route {
	import oauth2_protected
	reverse_proxy @protected_paths {$UPSTREAM_A_URI}
}

This leads me to believe that I’m simply doing something wrong, and either named matchers can’t be used this way, routes can’t be used this way, or both?

How do I go about mapping a bunch of path handlers to a specific reverse_proxy backend?

4. Error messages and/or full log output:

N/A

5. What I already tried:

I tried changing the route directives to handle directives, as the docs mention that handle directives are “mutually exclusive” from other handle blocks, but it didn’t like the oauth2_token_introspection inside the handle directive:

caddy_1  | Error: adapting config using caddyfile: parsing caddyfile tokens for 'handle': directive 'oauth2_token_introspection' is not an ordered HTTP handler, so it cannot be used here

6. Links to relevant resources:

Please update to v2.6.2.

Did you mean to put the matcher on route instead of on reverse_proxy?

Both route directives will run with your current config.

The handle directive will only run the first matching one, whereas with route, they will all run – until it reaches a terminal handler which ends the middleware chain.

You can use the order global option to give oauth2_token_introspection an order, without needing to put it inside of a route. Try this:

order oauth2_token_introspection before basic_auth

Thanks for helping @francislavoie!

Did you mean to put the matcher on route instead of on reverse_proxy?

My goal is simply to have the reverse_proxy (and the oauth2_token_introspection module for protected paths) to run for ALL of the paths in the named matcher. Does it matter whether I put it on the route vs. the reverse_proxy?

Again, my goal here was simply to avoid a ton of duplicate directives (think hundreds of paths that all use the same reverse_proxy and oauth2_token_introspection module). Is how I’m trying to use named matchers in this way supported?

The handle directive will only run the first matching one, whereas with route , they will all run – until it reaches a terminal handler which ends the middleware chain.

Is this outlined somewhere in the docs? I really could not tell how route differs from handle other than the “mutually exclusive” comment in the docs, but it doesn’t really explain what that means exactly.

So luckily I’m the author of the oauth2_token_introspection module, so I can change it to work for the handle directive if that’s the directive I should be using here instead of route. Do you happen to know which interface I need to implement to avoid the directive 'oauth2_token_introspection' is not an ordered HTTP handler, so it cannot be used here error?

Yes. If you put the matcher only on reverse_proxy, then the entire route will run always.

This means if you have two route and you put oauth in the first one but not the second, then oauth will have run even for the second route where you didn’t have oauth in it.

You can shorten your matchers like this:

	@protected_paths path /v1/hello /v1/goodbye

Yes, the handle directive docs explain pretty clearly, I think, including how it differs from route.

There’s nothing to change, I don’t think.

No, it’s how it works for all plugins. You need to either use the order global option in your config like I said, to tell Caddy how to order it, or put it in route which forces the order to be literally the order in which you put it in your config. The order global option is what I recommend.

And if I instead put the matcher on the route, then will it only run that route if the requested path is in the matcher’s list of paths? In other words, for this config:

http://{$API_HOST}:8080 {
	@protected_paths {
		path /v1/hello
		path /v1/goodbye
	}
	route @protected_paths {
		import oauth2_protected
		reverse_proxy {$UPSTREAM_A_URI}
	}

	@public_paths {
		path /v1/foo
		path /v1/bar
	}
	route @public_paths {
		reverse_proxy {$UPSTREAM_B_URI}
	}
}

A request for /v1/foo (which is in @public_paths) will be handled by the second route and the first route will not be invoked (and thus also not invoke the oauth2_protected module)?

Is this true regardless of whether the matcher is on the route vs the reverse_proxy?

Correct.

No, it’s true because you put the matcher inside the route. Wrong level of nesting.

Thanks again for your help @francislavoie I’ll give this a whirl…

This topic was automatically closed after 30 days. New replies are no longer allowed.