Conditional basic auth

1. Caddy version (caddy version):

2.4.6

2. How I run Caddy:

a. System environment:

Linux, x86_64, Docker, docker-compose

b. Command:

docker-compose start caddy

c. Service/unit/compose file:

version: '2.2'

services:
  caddy:
    ports:
      - "0.0.0.0:80:80"
      - "0.0.0.0:443:443"
    volumes:
    - /var/run/docker.sock:/var/run/docker.sock
    - ./config:/config
    - ./data:/data
    - ./caddy:/etc/caddy
    - ./logs:/logs
    - ./srv:/srv
    working_dir: /etc/caddy
    build:
      context: .
      dockerfile: Dockerfile.docker
    networks:
      - proxy
    command: caddy

d. My complete Caddyfile or JSON config:


(auth) {
        basicauth {
                import /etc/caddy/htpasswd/{args.0}
        }
}

:80 {
        respond "This is not the website you are looking for."
}

s1.t.atman.ro {
        reverse_proxy s1 80
        import auth s1
}

the /etc/caddy/htpasswd/s1 file:

test JDJhJDEyJGFUWDBQM1BYMkZ1NUR3Ti55YktPVU9GamQ1b0FBTE81cTFBTVFpeGZ2dXdUYy5GdS4uNnky

3. The problem I’m having:

I would like to use basicauth if the authentication file exist and skip basicauth otherwise.

4. Error messages and/or full log output:

5. What I already tried:

  • empty basicauth {} will still ask for a user and password
  • I tried to use basicauth oneliners but I didn’t managed to get it right

6. Links to relevant resources:

Thank you!

Would you be able to update the Caddy config over (admin) API?

1 Like

No, I would like to be able to generate the httpasswd files (for some of the sites) through ansible and the actual sites configurations to be loaded through docker-proxy module (https://github.com/lucaslorentz/caddy-docker-proxy/plugin).

If the password file for a domain does exist, enable basicauth and use that. If it does not exist, just configure the site withouth basicauth.

Now that I am thinking, maybe a solution would be to be able to define the fallback for the basic auth if it’s empty?

2 Likes

I am VERY curious about caddy-docker-proxy and it would be interesting to see it in how you use it.

If you can upload a minimal sample here on on GitHub/BitBucket/GitLab/Gitea, I was thinking we could write a small custom service that monitors configuration and updates the basicauth settings via the (admin) API

This way, you can continue using the stock basicauth module (in addition to sharing some real worlds use of caddy-docker-proxy), as otherwise it feels like the stock basicauth module will need to updated to behave this way (and it seems risky to do so - imagine the file getting deleted and anyone being able to access an otherwise protected endpoint)

1 Like

Sure, my current config:

s1 is a vanilla nginx container with the following labels:

caddy: s1.t.example.com
caddy.0_import: "rev_proxy {{upstreams 80}}"
caddy.1_import: "auth doauth s1"

The relevant Caddyfile:

(rev_proxy) {
        reverse_proxy {
                to {args.0}
                header_down -Server
        }
}
(auth) {
        @doauth_or_not {
                #expression {args.0}.startsWith("do")
                expression "\"{args.0}\"" == \""doauth\""
        }
        basicauth @doauth_or_not {
                import /etc/caddy/htpasswd/{args.1}
        }
}

When the s1 container goes up, the @doauth_or_not is true so the basicauth is enabled.
If I change the last label as caddy.1_import: "auth noauth s1", the basic auth will be disabled.

The json config for enabled auth case:

{"admin":{"listen":"tcp/localhost:2019"},"apps":{"http":{"servers":{"srv0":{"listen":[":443"],"routes":[{"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"authentication","providers":{"http_basic":{"accounts":[{"password":"JDJhJDE0JEU0RVZHSWhobGEuMFIycFlIeVV0SE9nek1yS2RmY2t2c1EvRml2d3dkNkhVcDlqV2xyVWk2","username":"test"}],"hash":{"algorithm":"bcrypt"},"hash_cache":{}}}}],"match":[{"expression":"\"doauth\" == \"doauth\""}]},{"handle":[{"handler":"reverse_proxy","headers":{"response":{"delete":["Server"]}},"upstreams":[{"dial":"10.99.0.36:80"}]}]}]}],"match":[{"host":["s1.t.example.com"]}],"terminal":true}]},"srv1":{"listen":[":80"],"routes":[{"handle":[{"body":"This is not the website you are looking for.","handler":"static_response"}]}]}}}},"logging":{"logs":{"default":{"encoder":{"format":"json","time_format":"iso8601"},"level":"DEBUG","writer":{"filename":"/logs/main.log","output":"file"}}}}}
2 Likes

There’s no way to make it conditional based on the contents of basicauth.

What you should probably do instead is put your basicauth { inside of your /etc/caddy/htpasswd/s1 file instead, and leave it empty if you want no auth.

1 Like

Well, that’s one way. I was thinking since we have docker-compose here, we write a small custom service that monitors configuration and updates the basicauth settings (stanza?) via the (admin) API

Sure, that would work too, @francislavoie.
Since I plan to generate those files with ansible, it’s easy to add the basicauth block in there.
But what happens in this case if the s1 file does not exists? Will the import statement error throw away the whole server block or it will just complain about the file not being present but still serve unauthenticated content from s1?

I think my problem is not limited to the actual basic auth thing. I did managed to kinda solve it by replying on a specific value sent as argument in in one of the labels of the upstream container. I am using that in an expression matcher whch is used by basicauth.
I think the actual issue is (for me at the moment) that attempting to import a non-existent file kicks out the whole server block from the configuration.

If you put a * in the import path, then Caddy won’t require the file to exist.

For example:

example.com {
	import basicauth*.txt
	respond "Hello"
}

Then just make files like basicauth1.txt and so on. If the file(s) don’t exist, the import will just do nothing.

3 Likes

Oh, that’s interesting and cool. I will use that tip!
Shame it is not documented on import (Caddyfile directive) — Caddy Documentation
Thank you, I think this one can be closed.

Just for the next one stublimg around here in search for answers, using expression matcher on basicauth is really neat.

(auth) {
	@check_auth {
		expression "\"{args.0}\"" == "\"auth_enabled\""
	}
	basicauth @check_auth {
		# if a file is missing, the whole vhost wont work, UNLESS you have a wildcard in the file name!
		# in that case, missing files wont result in error
		# the file should just contain the space separated user and password
		import /etc/caddy/htpasswd/{args.1}*
	}
}

Use it in a template like this:

server.example.com {
        ...
        import auth auth_enabled
        ...

or to skip it:

server2.example.com {
        ...
        import auth auth_nope
        ...
1 Like

This can be simplified:

@check_auth expression `"{args.0}" == "auth_enabled"`
3 Likes

Just reporting back: the wildcard is working perfect! I do get a warning in the logs but thats even better :slight_smile:

Also thank you for the simplified expression syntax, @francislavoie
If I may, this one

	@check_add {
		expression "\"{args.0}\"" == "\"headers_add\"" || "\"{args.0}\" == \"headers_overwrite\""
	}

could also be written as below?

	@check_add expression `"{args.0}" == "headers_add" || "{args.0}" == "headers_overwrite"`
1 Like

Yeah.

If a named matcher only has a single matcher, it can be a one-liner and skip the { } block.

There’s two different string delimiters in the Caddyfile, i.e. backticks and double quotes. Since double quotes have its own meaning in CEL expressions, you can instead use backticks to wrap the tokens in the Caddyfile, and avoid escaping your double quotes.

But actually, in Caddy v2.5.0, you’ll be able to omit the backticks altogether, because I made a patch to make the expression matcher a bit smarter about how it handles tokens, so it will preserve double quotes as-is instead of consuming them before adapting the config to JSON.

3 Likes

Nice, looking forward to it!

1 Like

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