- Shell 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Such a line cannot stay a | block: yq rewrites the whole value as one quoted string on a single line, and the entry becomes unreadable. Caddy ignores those spaces, so nothing else gives them away, and show stops colouring from that point because yq no longer prints the file. The check names the entry, and runs on every path into the file, so a value with one cannot be set either. While testing it: a failed mkdir of a log directory still reported the directory as created and carried on, because ensure_log_dirs runs from build, whose errors the callers handle themselves. |
||
| cctl | ||
| cctl.example.yaml | ||
| install | ||
| README.md | ||
cctl
One YAML file describes every site a Caddy server serves; cctl turns it
into the Caddyfile, validates it with caddy validate before replacing
anything, and, if asked to, keeps a dnsmasq hosts file in step with the
sites.
A site is written once, with the domains it is published on. cctl writes
the block for each domain, the redirects that point to it, and the DNS
lines that make its names resolve.
Requirements
- bash 4.4 or later
- yq by mikefarah, the Go one. The
Python
yq, a wrapper aroundjq, has a different syntax and does not work;cctlchecks which one it finds. - caddy
- OpenRC, for
run,manual,startandstop - dnsmasq, only if the YAML has a
dnssection
Installation
install -m 755 cctl /usr/local/bin/cctl
The install script in this repository does the same over the network,
and writes the tag and the commit count into the copy it installs, which
cctl -v then reports. A copy run from the repository says dev.
The configuration is read from /etc/cctl.yaml and the Caddyfile is
written to /etc/caddy/Caddyfile. Both can be changed with -c and -o,
or once and for all in the constants at the top of the script, together
with the account Caddy runs as (caddy) and the name of its service.
An example
global: |
{
email admin@example.com
}
(internal-ca) {
tls {
issuer acme https://ca.example.net/acme/acme/directory {
trusted_roots /etc/caddy/ca.crt
}
}
}
(sso) {
reverse_proxy /outpost.goauthentik.io/* http://192.0.2.50
forward_auth {args[0]} http://192.0.2.50 {
uri /outpost.goauthentik.io/auth/caddy
copy_headers X-Authentik-Username X-Authentik-Groups
}
}
(log) {
log {
output file /var/log/caddy/{args[0]}.log
}
}
dns:
file: /etc/dnsmasq.d/cctl
domains:
local:
tls: internal-ca
dns: 192.0.2.10
example.com:
proxy:
wiki:
enable: true
log: true
domains: [local, example.com]
sso: [example.com]
redir: [w]
to: 192.0.2.21:8080
sites:
static:
enable: true
domains: [example.com]
caddy: |
root * /srv/www/static
file_server
cctl config writes the global part as it is, followed by:
https://w {
import internal-ca
redir / https://wiki
}
https://wiki {
import internal-ca
import log wiki
reverse_proxy 192.0.2.21:8080
}
https://w.example.com {
redir / https://wiki.example.com
}
https://wiki.example.com {
import log wiki.example.com
route {
import sso *
reverse_proxy 192.0.2.21:8080
}
}
https://static.example.com {
root * /srv/www/static
file_server
}
and /etc/dnsmasq.d/cctl:
192.0.2.10 wiki
192.0.2.10 w
cctl.example.yaml in this repository is longer, and covers every kind of
site cctl can write.
The YAML file
Five top-level keys, all optional except where a site needs them.
global
Caddyfile text, written at the top of the Caddyfile unchanged: the global
options block, the snippets the sites import, and anything else that is
not a site. cctl imports three kinds of snippet by name, and they must
be defined here:
- the one named by a domain's
tls (sso), for sites withsso. It gets the path to protect as{args[0]}:*for the whole site.(log), for sites withlog: true. It gets the host of the block as{args[0]}, so each host can log to its own file.
dns
dns:
file: /etc/dnsmasq.d/cctl
After every change cctl rewrites this file with one line per name, for
the domains that have dns, and sends dnsmasq a SIGHUP, which makes it
reread its hosts files without stopping answering. The file belongs to
cctl: anything written in it by hand is lost on the next change. Names
of your own go in a file of their own, with an addn-hosts line of their
own in dnsmasq.conf.
Without this section cctl does not touch DNS; cctl hosts still prints
the lines.
domains
The key is the domain name: a site wiki on example.com is served as
wiki.example.com. local is the exception, for bare names with no domain
at all: wiki.
| Key | What it does |
|---|---|
tls |
snippet imported in every https block of the domain |
dns |
address the domain's names resolve to in the dns file |
proto |
http or https (default https) |
follow |
another domain: every site on it is also on this one |
A domain with no keys is written as just its name, like example.com:
above.
follow publishes every site of one domain on another as well, including
the sites added later. A site that should stay off lists the domain in
exclude. It goes one level deep: a domain cannot follow one that follows
another, so where a site's domains come from can always be read off the
site and one domain entry.
domains:
local:
tls: internal-ca
lan:
follow: local
tls: internal-ca
proxy and sites
Both hold sites, keyed by name. The two sections are only there to be
read: proxy for sites that are a reverse_proxy to one upstream,
sites for those with a body written by hand. A name is unique across
both.
| Key | Section | What it does |
|---|---|---|
enable |
both | true to generate the site; absent or false keeps it in the file only |
log |
both | true adds import log <host> to each block |
proto |
sites |
http or https, over the domain's |
domains |
both | the domains the site is on |
exclude |
both | domains it would get through follow, and should not |
sso |
both | domains where the site is behind the sso snippet; domain/path protects only that path |
redir |
both | other names that redirect to the site, on each of its domains |
to |
proxy |
the upstream of reverse_proxy |
caddy |
both | Caddyfile text for the body of the block |
raw |
sites |
complete blocks, addresses included, written as they are |
Keys are written back in this order by set, add and del.
The body of a block is the caddy text, then reverse_proxy <to>: in
that order, because inside the route that sso adds directives run as
written, and anything after reverse_proxy would never be reached.
A site with raw is written exactly as given, once, and only enable
applies to it. It is for the case none of the keys can express; nothing
in the example above needs it.
A site called *, quoted as "*" in YAML, stands for every name on a
domain: on example.com it is https://*.example.com. It cannot be on
local, and it has no line in the dns file, which cannot hold
wildcards.
Commands
| Command | What it does |
|---|---|
config |
generate, validate and install the Caddyfile |
edit |
edit the YAML; it is saved only once it is valid |
show [site] |
the blocks generated for a site; with no site, the YAML |
list [domain] |
sites and the domains they are on, disabled ones last |
hosts |
the DNS lines, printed |
dns |
rewrite the dns file now |
get <site> [key] |
a site, or one of its keys |
set <site> <key> <value> |
set a key; - reads the value from stdin |
add proxy|site <name> [key value ...] |
add a site |
del <site> [key] |
delete a site, or one of its keys |
get|set|add|del domain <name> ... |
the same, for a domain |
run |
config, then Caddy in the foreground |
manual |
Caddy in the foreground, without generating |
start, stop |
the Caddy service |
Flags: -c <file> for the YAML, -o <file> for the Caddyfile, -v for
the version, -n for a dry run: config prints the Caddyfile, set, add and del print the
diff of the YAML, and nothing is replaced.
cctl add proxy wiki domains local,example.com to 192.0.2.21:8080 enable true
cctl set wiki sso example.com
cctl set wiki enable false
cctl set static caddy - < static.caddy
cctl del wiki redir
cctl add domain lan follow local tls internal-ca
Lists are given comma-separated, enable and log as true or false.
The shell expands * unless it is quoted: cctl set '*' enable false.
How a change is applied
Every change, from edit or from set, add and del, is made to a
copy of the YAML. The copy is checked, turned into a Caddyfile and run
through caddy validate; only then does it replace the YAML and the
Caddyfile, and the dns file is rewritten. A change that would break the
configuration is refused, with every problem found and the entry it is
about, and nothing is written.
The checks are the rules the generator relies on: known keys and domains,
true or false where a boolean is expected, lists where a list is,
unique names including the ones in redir, a to for every proxy, at
most one sso path per domain, an exclude that actually removes a
domain.
As root, caddy validate runs as the account Caddy runs as, so a file
Caddy cannot read fails here and not when the service starts. The
directories of the log files are created first, owned by that account,
since it cannot create them under /var/log itself.
edit opens a copy in $EDITOR (nano if unset). If the copy does not
validate, the error is shown and the copy reopened with the error at the
top as # cctl: comments, which are dropped again before saving; Ctrl+C
gives up and leaves the YAML as it was.
set, add and del go through yq, and keep the file laid out as it
was: blank lines between entries, comments, flow lists like [a, b].
Writing the Caddyfile does not touch the running Caddy: the new
configuration takes effect on the next run or start. run and
manual stop the service, keep Caddy in the foreground with its log on
screen, and start the service again however Caddy exits, Ctrl+C
included.