gamalan / caddy-tlsredis

Redis Storage using for Caddy TLS Data
Apache License 2.0
95 stars 31 forks source link

Allow ENV variable to override Caddyfile config #48

Closed hmhackmaster closed 1 year ago

hmhackmaster commented 1 year ago

I am working on adding some automation to my Caddy config. For my primary ('production') Caddy instance I specify many config options (like host) in the Caddyfile but would like to override these config options via environment variables when I validate the config or test locally.

I see in the GetConfigValue() function that the order specifies the configured setting, then Env, then default (which explains the behaviour that I am seeing). I also noticed the ReplaceEnvConfigCaddy() function but I don't understand what it does.

While I am not a developer, it seems that, in general, ENVs override configs. Is this a change that you would be willing to adopt, or does it already exist and I am just doing it wrong? Thanks!

francislavoie commented 1 year ago

What does your config look like? What style env var syntax are you using?

hmhackmaster commented 1 year ago

Hey @francislavoie!

My Caddyfile looks mostly like this:

{
    acme_dns cloudflare {env.CADDY_CF_API_TOKEN}

    storage redis {
        host "redis01.internal.example"
        db 0
        key_prefix "caddytls"
        value_prefix "caddy-storage-redis"
        timeout 5
        tls_enabled "true"
        tls_insecure "false"
    }
}

import ./conf.d/*.caddyfile
import ./sites.d/*.caddyfile
import ./domains.d/*.caddyfile

With the 'prod' Caddy servers having ENVs like CADDY_CLUSTERING_REDIS_USERNAME and CADDY_CLUSTERING_REDIS_PASSWORD defined, along with several other ENVs that are explicitly used in the Caddyfile.

This works perfect and allows me to store my Caddyfile(s) in git without storing the credentials and use git workflows to push the config to the prod server (via caddy adapt and curl POST process).

What I am trying to do now is add a caddy validate process to a workflow that would run on every commit. Since I don't want to allow a potentially broken config to connect to my prod Redis instance I use for caddy-tlsredis, I am hoping to override the host/tls_enable/tls_insecure values with something else (likely localhost, for example) in the container that runs this particular validation workflow to allow caddy validate and caddy fmt commands to execute without error.

In writing this response, it occurs to me that I can just use a traditional ENV (something like host "{env.CADDY_TLSREDIS_HOST}") so that might be a viable solution but my non-developer-perspective & research assumed that ENVs like CADDY_CLUSTERING_REDIS_HOST would override the Caddyfile's host "redis01.internal.example (which overrides the predefined default DefaultRedisHost).

francislavoie commented 1 year ago

In writing this response, it occurs to me that I can just use a traditional ENV (something like host {env.CADDY_TLSREDIS_HOST})

Yes, that's what you should do. Caddyfile config is always going to be higher priority than config passed via environment, so if you want the value to come from the environment, you should use env replacement syntax in your config.

FYI, you can also use Caddyfile env syntax instead of the {env.*} syntax, which allows to set defaults, and works as text replacement before the Caddyfile is parsed, instead of being replaced at runtime: https://caddyserver.com/docs/caddyfile/concepts#environment-variables

hmhackmaster commented 1 year ago

Yes, that's what you should do. Caddyfile config is always going to be higher priority than config passed via environment, so if you want the value to come from the environment, you should use env replacement syntax in your config.

Ok, sounds good! I'll move forward with this strategy!

FYI, you can also use Caddyfile env syntax instead of the {env.*} syntax, which allows to set defaults, and works as text replacement before the Caddyfile is parsed, instead of being replaced at runtime: https://caddyserver.com/docs/caddyfile/concepts#environment-variables

I did see this when I was first working on setting this whole scheme up! I went with the {env.*} syntax because my (current) process actually uses two frontend edge servers (the second one doesn't take traffic yet) with each of the servers having separate ENVs (such a redis creds or a header that's added which identifies which frontend server serviced the request). This means I wanted to be able to take one Caddyfile, convert it to a single JSON and then push that identical JSON to both frontend servers, which are configured identically except for the ENVs (via EnvironmentFile=/etc/caddy/envfile.env included in systemd's caddy-api.service). In my initial read through of the docs to figure out how to work this, I assumed that 'parsing the Caddyfile' happened at the caddy adapt step (since after that, it's no longer a Caddyfile?) and since the actual frontend servers only get the JSON file and have just their ENV to work with, this seemed like the correct approach. I haven't encountered any issues with this setup (yet!) but I figured I would explain it all just in case I had any incorrect assumptions and I could do it better/more efficient!

francislavoie commented 1 year ago

Yeah that's correct, and {env.*} makes sense for that. But {env.*} can only be used in config locations where the module receiving the config actually applies the replacer on it (needs a couple lines of code). Pretty sure this Redis module does that for all of its config so it's fine :+1:

hmhackmaster commented 1 year ago

I have tested and confirmed that just renaming my existing ENVs from CADDY_CLUSTERING_REDIS_HOST=localhost to something like CADDY_TLSREDIS_HOST=localhost and referencing it in the config:

{
    storage redis {
        host {env.CADDY_TLSREDIS_HOST}
        username {env.CADDY_TLSREDIS_USERNAME}
        password {env.CADDY_TLSREDIS_PASSWORD}
        db 0
        key_prefix "caddytls"
        value_prefix "caddy-storage-redis"
        timeout 5
        tls_enabled {env.CADDY_TLSREDIS_TLS}
        aes_key {env.CADDY_TLSREDIS_AESKEY}
    }
}

Thanks so much @francislavoie!

hmhackmaster commented 1 year ago

Upon testing on my 'staging' Caddy server, it looks like tls_enabled {env.CADDY_TLSREDIS_TLS} doesn't actually work. No matter what I store in my .env file for CADDY_TLSREDIS_TLS=true, it looks like caddy-tlsredis defaults to false. Based on past experience(/pain) replacing true/false with variables in configs, I suspect it's a string vs bool (and a web search says "Environment variables can never be a boolean"...).

But I have had a good nap between then and now and I realized I overcomplicated it all in a pretty spectacular fashion... I'll bet it's pretty obvious to anyone reading this later, but I don't need to try and define a variable and then call the variable in the config. That's exactly what the pre-included ENVs are for.

So now my Caddyfile looks like this:

{
    acme_dns cloudflare {env.CADDY_CF_API_TOKEN}

    storage redis
}

import ./conf.d/*.caddyfile
import ./sites.d/*.caddyfile
import ./domains.d/*.caddyfile
`

and the ENV files in my workflow and on the test/prod systems just define all the ENVs as needed. Nice and simple and exactly the way it was intended to be:

CADDY_CLUSTERING_REDIS_HOST=
CADDY_CLUSTERING_REDIS_USERNAME=
CADDY_CLUSTERING_REDIS_PASSWORD=
CADDY_CLUSTERING_REDIS_DB=0
CADDY_CLUSTERING_REDIS_AESKEY=
CADDY_CLUSTERING_REDIS_KEYPREFIX=caddytls
CADDY_CLUSTERING_REDIS_VALUEPREFIX=caddy-storage-redis
CADDY_CLUSTERING_REDIS_TLS=true
CADDY_CLUSTERING_REDIS_TLS_INSECURE=false
francislavoie commented 1 year ago

it looks like tls_enabled {env.CADDY_TLSREDIS_TLS} doesn't actually work.

I think you'll need to use tls_enabled {$CADDY_TLSREDIS_TLS} in this case.

The Caddyfile parser is looking for a literal true or false value using strconv.ParseBool(), it doesn't store the string for later processing because the JSON config takes a boolean, so {env.*} is impossible. So the only option is to use Caddyfile-style env syntax to do string replacement before the Caddyfile is parsed.

FWIW, Caddy was designed with "all config comes from one place" (i.e. the config file) as a design principle. This module "breaks" that by accepting env vars as config like this. But whatever, it's a third-party plugin so anything goes :sweat_smile: