ACMED.TOML(5)

πŸ“¦ version 0.25.0, in alpine 3.24

πŸ’– NAME πŸ’–

acmed.toml β€” the ACMEd configuration file, bestie. This is manual page ACMED.TOML(5) from the File Formats Manual, and it’s literally the config file for acmed(8).


✨ DESCRIPTION ✨

So acmed.toml is the configuration file for acmed(8), and it’s written in TOML format. The allowed elements are all described below β€” every single one, no skipping, periodt.

πŸŽ€ account

Array of tables, each one representing an account on one or several endpoint. Iconic.

  • contacts array β€” Array of tables describing the account holder’s contact information. Each table must have one and only one key-value pair. Possible keys and their associated values are:
    • mailto string β€” A mailto URI as defined by RFC 6068. This URI can’t contain hfields, and it can’t contain more than one addr-spec in the to component.
  • env table β€” Table of environment variables that will be accessible from hooks.
  • external_account table β€” Table containing the information required to bind the account to an external one. Possible fields and values are:
    • identifier string β€” ASCII string identifying the key.
    • key string β€” Private key encoded in base64url without padding.
    • signature_algorithm string β€” Name of the signature algorithm used to sign the external account binding message sent to the endpoint as defined in RFC 7518. Possible values are:
      • HS256 ⟨default⟩
      • HS384
      • HS512
  • hooks array β€” Names of hooks that will be called during operations on the account storage file. The hooks are guaranteed to be called sequentially in the declaration order.
  • key_type string β€” Name of the asymmetric cryptography algorithm used to generate the key pair. Possible values are:
    • ed25519
    • ed448
    • ecdsa_p256 ⟨default⟩
    • ecdsa_p384
    • ecdsa_p521
    • rsa2048
    • rsa4096
  • name string β€” The name the account is registered under. Must be unique.
  • signature_algorithm string β€” Name of the signature algorithm used to sign the messages sent to the endpoint as defined in RFC 7518. The default value is derived from the key type. Possible values are:
    • ES256
    • ES384
    • ES512
    • RS256

🌈 certificate

Array of tables, each one representing a certificate that will be requested to a CA.

Heads up, bestie: by default, certificates are identified by the first identifier in the list of identifiers. That means if you reorder the identifiers so a different identifier is first, a new certificate with a new name will be issued. Not me making the rules, I’m just telling you the rules.
  • account string β€” Name of the account to use.
  • csr_digest string β€” Name of the certificate’s signing request digest algorithm. Possible values are:
    • sha256 ⟨default⟩
    • sha384
    • sha512
  • directory string β€” Path to the directory where certificates and their associated private keys are stored.
  • endpoint string β€” Name of the endpoint to use.
  • env table β€” Table of environment variables that will be accessible from hooks.
  • file_name_format string β€” Template used to build the file’s name. The template syntax is MiniJinja. See the STANDARDS section for a link to the MiniJinja specifications. If not specified, the value defined in the endpoint element, and then the global element, is used. Default is {{ name }}_{{ key_type }}.{{ file_type }}.{{ ext }}. Possible variables are:
    • ext string β€” File extension. See cert_file_ext and pk_file_ext.
    • file_type string β€” Contains pk for the private key file and crt for the certificate file.
    • key_type string β€” The certificate’s private key type.
    • name string β€” The certificate’s name.
  • hooks array β€” Names of hooks that will be called when requesting a new certificate. The hooks are guaranteed to be called sequentially in the declaration order.
  • identifiers array β€” Array of tables listing the identifiers that should be included in the certificate along with the challenge to use for each one. The dns and ip fields are mutually exclusive.
    • challenge string β€” The name of the challenge to use to prove the identifier’s ownership. Possible values are:
      • dns-01
      • http-01
      • tls-alpn-01
    • dns string β€” The domain name.
    • env table β€” Table of environment variables that will be accessible from hooks.
    • ip string β€” The IP address.
  • key_type string β€” Name of the asymmetric cryptography algorithm used to generate the certificate’s key pair. Possible values are:
    • ed25519
    • ed448
    • ecdsa_p256
    • ecdsa_p384
    • ecdsa_p521
    • rsa2048 ⟨default⟩
    • rsa4096
  • kp_reuse boolean β€” Set whether or not the private key should be reused when renewing the certificate. Default is false.
  • name β€” Name of the certificate. Must be unique unless the key type is different. Will be used in logs and in the associated file’s name. The *, : and / characters will be replaced by an underscore. Default is the first identifier.
  • random_early_renew string β€” Period of time before the usual certificate renewal in which the certificate will renew at a random time. This is useful for when you want to even out your certificate orders when you’re dealing with very large numbers of certificates. The format is described in the TIME PERIODS section. Default is the value defined in the associated endpoint.
  • renew_delay string β€” Period of time between the certificate renewal and its expiration date. The format is described in the TIME PERIODS section. Default is the value defined in the associated endpoint.
  • subject_attributes table β€” Table where the certificate’s subject attributes are specified. Possible keys, with their RFC 4519 and X.500 equivalents, are:
    • country_name β€” RFC 4519: c; X.500: countryName
    • generation_qualifier β€” shown equivalent: generationQualifier
    • given_name β€” shown equivalent: givenName
    • initials β€” shown equivalent: initials
    • locality_name β€” RFC 4519: l; X.500: localityName
    • name β€” shown equivalent: name
    • organization_name β€” RFC 4519: o; X.500: organizationName
    • organizational_unit_name β€” RFC 4519: ou; X.500: organizationalUnitName
    • pkcs9_email_address β€” no equivalent shown
    • postal_address β€” shown equivalent: postalAddress
    • postal_code β€” shown equivalent: postalCode
    • state_or_province_name β€” RFC 4519: st; X.500: stateOrProvinceName
    • street β€” RFC 4519: street; X.500: streetAddress
    • surname β€” RFC 4519: sn; X.500: surname
    • title β€” shown equivalent: title

πŸ›°οΈ endpoint

Array of tables where each element defines a Certificate Authority (CA) which may be used to request certificates. Main character energy, honestly.

  • file_name_format string β€” Template used to build the file’s name. For detailed documentation, see the file_name_format directive located in the certificate element.
  • name string β€” The name the endpoint is registered under. Must be unique.
  • rate_limits array β€” Array containing the names of the HTTPS rate limits to apply.
  • random_early_renew string β€” Period of time before the usual certificate renewal in which the certificate will renew at a random time. This is useful for when you want to even out your certificate orders when you’re dealing with very large numbers of certificates. The format is described in the TIME PERIODS section. Default is the value defined in the global section.
  • renew_delay string β€” Period of time between the certificate renewal and its expiration date. The format is described in the TIME PERIODS section. Default is the value defined in the global section.
  • root_certificates (array) β€” a list of paths to root certs you want dropped into the trust store, bestie. Just paths, that's it πŸ’…
  • tos_agreed (boolean) β€” the "yes I pinky promise I read the Terms Of Service" switch. Set it to agree (or don't, and see what happens πŸ‘€).
  • url (string) β€” the directory URL of the endpoint. Like, the actual address it talks to ✨

🌍 global

The big table of global config options. Set it once, vibe everywhere 🫢

  • accounts_directory (string) β€” where the accounts' private and public keys live. Pick a directory, any directory πŸŽ€
  • cert_file_group (group_name or group_id, string) β€” the group that'll own newly-created certificate files. See chown(2) for the deets πŸ”₯
  • cert_file_mode (integer) β€” the permissions to slap on newly-created certificate files. See chmod(2) for the deets πŸ”₯
  • cert_file_user (username or user_id, string) β€” the user who'll own newly-created certificate files. See chown(2) again πŸ’œ
  • cert_file_ext (string) β€” the file extension for certificate files. Yes, you get to pick it πŸŽ€
  • certificates_directory (string) β€” where certificates and their associated private keys are stored. Keep 'em together, it's giving organized πŸ¦‹
  • env (table) β€” a table of environment variables that hooks can see. Hook fuel β›½
  • file_name_format (string) β€” the template used to build the file's name. For the full breakdown, go look at the file_name_format directive inside the certificate element β€” it's all documented there πŸ“–
  • pk_file_group (group_name or group_id, string) β€” the group that'll own newly-created private-key files. See chown(2) 😌
  • pk_file_mode (integer) β€” permissions for newly-created private-key files. See chmod(2) πŸ”
  • pk_file_user (username or user_id, string) β€” the user who'll own newly-created private-key files. See chown(2) πŸ’–
  • pk_file_ext (string) β€” file extension for private-key files. Yes, separately from certs. Iconic 🌸
  • random_early_renew (string) β€” a window before the usual cert renewal where the cert renews at a random time. Super useful when you want to spread out your cert orders because you're dealing with, like, a truly unhinged number of certificates. Format lives in the β€œTIME PERIODS” section. By default this is disabled β€” or more accurately, the time frame is 0 πŸ•
  • renew_delay (string) β€” how long between the renewal and the expiration date. Format's in the β€œTIME PERIODS” section. Default: 30d ⏳
  • root_certificates (array) β€” array of paths to root certs that get added to the trust store. Same energy as the endpoint-level one, just globally πŸ’…

πŸ‘― group

An array of tables that lets you group several hooks into one. A group is treated as a brand-new hook. Found family, but for hooks 🫢

  • hooks (array) β€” the names of the hooks being grouped. They're guaranteed to be called sequentially in the declaration order. No cutting the line 🚦
  • name (string) β€” the name the group registers under. This name counts as a hook name. Must be unique. No duplicates, periodt πŸ’€

πŸͺ hook

An array of tables where each element defines a command that gets launched at a defined point. See β€œWRITING A HOOK” for more πŸ’–

  • allow_failure (boolean) β€” whether an error return from this hook is allowed or not. If not allowed, a failure here fails the whole certificate request process. Default: false. So yes, it's a big deal 😭
  • args (array) β€” array of strings for the command's arguments. Every single one πŸŽ€
  • cmd (string) β€” the name of the command that gets launched. The main character ✨
  • name (string) β€” the name the hook registers under. Must be unique. Again. No repeats πŸ’…
  • stderr (string) β€” path to the file where the command's standard error output is written. Catch those Ls on disk πŸ“
  • stdin (string) β€” path to the file that gets written into the command's standard input. Mutually exclusive with stdin_str β€” pick one, not both 🚫
  • stdin_str (string) β€” a string written straight into the command's standard input. Mutually exclusive with stdin β€” again, one or the other πŸ™…β€β™€οΈ
  • stdout (string) β€” path to the file where the command's standard output is written. Collect those Ws πŸ’«
  • type (array) β€” array of strings. The possible types are exactly:
    • challenge-dns-01
    • challenge-dns-01-clean
    • challenge-http-01
    • challenge-http-01-clean
    • challenge-tls-alpn-01
    • challenge-tls-alpn-01-clean
    • file-post-create
    • file-post-edit
    • file-pre-create
    • file-pre-edit
    • post-operation

πŸ“Ž include

An array of paths to configuration files you want to include. The path can be relative or absolute β€” if it's relative, it's relative to the config file that included it 🧚

Heads up, bestie: if global options overlap, the one from the last included file wins. So if file A includes files B and C, and all three define the same global option, the final value comes from file C. Last one in the group chat gets the final say πŸ’€

Unix-style globbing is supported. Yep, globbing. Go wild 🌈


🚦 rate-limit

An array of tables where each element defines an HTTPS rate limit. Boundaries, but make it certificates 😌

  • name (string) β€” the name the rate limit registers under. Must be unique. You know the drill by now πŸ’…
  • number (integer) β€” how many requests are authorized within the time period. The cap 🧒
  • period (string) β€” the period of time during which that max number of requests is authorized. Format's described in the β€œTIME PERIODS” section, go check it ⏰

✨ WRITING A HOOK ✨

ok so bestie, listen πŸ’– when you're grabbing a certificate from a CA over ACME, there are literally three steps that are a nightmare to automatize, and we need to talk about them:

  1. Solving challenges to prove you own every identifier you're including β€” this one needs you to poke at other services' configs, so it totally depends on how your infra is set up. no universal answer, sorry 😭
  2. Restarting every service that uses the cert β€” same deal, it's all about your setup.
  3. Archiving β€” sure, there are a few default methods, but sometimes admins want (or are required) to do it a different way. and honestly? valid.

So, to give you full automation of all three steps without imposing arbitrary restrictions or methods, acmed(8) uses hooks. fundamentally, a hook is a command line template that gets called at a specific moment in the process πŸŽ€ that's it, that's the whole vibe. and because it's just a template, you can point it at any executable script or program chilling on the machine and customize the whole process. iconic.

For a given certificate, hooks are guaranteed to be called sequentially, in the order you declared them ✨ so yes, you can absolutely have one hook that depends on another. but β€” and this is the important part β€” several certificates may be renewed at the same time 🫢 therefore hooks shall not use globing or any other action that could disrupt hooks called by a different certificate. don't be that person.

A hook has a type, and the type influences both when it gets called and which template variables you get. you can declare several types on one hook β€” in that case, the hook is invoked whenever one of its types calls for it. when it's actually called, the hook only has access to the template variables for the current type. and if a hook uses a template variable that doesn't exist for the type it's being invoked for? the variable is just empty. no crash, no drama, just empty πŸ’…

When you're writing a hook, the values of args, stdin, stdin_str, stdout and stderr are treated as template strings, whereas cmd is not. the template syntax is MiniJinja. check the "STANDARDS" section for a link to the MiniJinja specs, iykyk πŸ“Ž

the available types and their template variables are down below bestie πŸ‘‡

πŸ¦‹ challenge-dns-01

Invoked when the ownership of an identifier must be proved using the dns-01 challenge. the available template variables are:

  • challenge (string) β€” the name of the challenge type ⟨dns-01⟩. mostly used in hooks with multiple types.
  • env (array) β€” array containing all the environment variables.
  • identifier (string) β€” the identifier name whose ownership is currently being validated.
  • is_clean_hook (bool) β€” False
  • proof (string) β€” the content of the proof that must be written to a TXT entry of the DNS zone for the _acme-challenge subdomain.

🧚 challenge-dns-01-clean

Invoked once an identifier's ownership has been proven using the dns-01 challenge. this hook is intended to remove the proof, since it's no longer required πŸ—‘οΈ the template variables are strictly identical to those given in the corresponding challenge-dns-01 hook, excepted is_clean_hook, which is set to true.

🌸 challenge-http-01

Invoked when the ownership of an identifier must be proved using the http-01 challenge. the available template variables are:

  • challenge (string) β€” the name of the challenge type ⟨http-01⟩. mostly used in hooks with multiple types.
  • env (array) β€” array containing all the environment variables.
  • file_name (string) β€” name of the file containing the proof. this is not a full path and does not include the .well-known/acme-challenge/ prefix.
  • identifier (string) β€” the identifier name whose ownership is currently being validated.
  • is_clean_hook (bool) β€” False
  • proof (string) β€” the content of the proof that must be written to file_name.

πŸŽ€ challenge-http-01-clean

Invoked once an identifier's ownership has been proven using the http-01 challenge. this hook is intended to remove the proof since it's no longer required. the template variables are strictly identical to those given in the corresponding challenge-http-01 hook, excepted is_clean_hook, which is set to true.

πŸ”₯ challenge-tls-alpn-01

Invoked when the ownership of an identifier must be proved using the tls-alpn-01 challenge. the available template variables are:

  • challenge (string) β€” the name of the challenge type ⟨tls-alpn-01⟩. mostly used in hooks with multiple types.
  • env (array) β€” array containing all the environment variables.
  • identifier (string) β€” the identifier name whose ownership is currently being validated.
  • identifier_tls_alpn (string) β€” the identifier name whose ownership is currently being validated, in a form suitable for the TLS ALPN challenge.
  • is_clean_hook (bool) β€” False
  • proof (string) β€” plain-text representation of the acmeIdentifier extension that should be used in the self-signed certificate presented when a TLS connection is initiated with the ALPN extension value. acmed(8) will not generate the certificate itself, since it can be done using tacd(8).
  • raw_proof (string) β€” the SHA-256 digest of the key authorization, encoded using Base64 URL scheme without padding. this is intended for the use of ACMEd with a TLS-ALPN responder other than tacd(8).

🫢 challenge-tls-alpn-01-clean

Invoked once an identifier's ownership has been proven using the tls-alpn-01 challenge. this hook is intended to remove the proof since it's no longer required. the template variables are strictly identical to those given in the corresponding challenge-tls-alpn-01 hook, excepted is_clean_hook, which is set to true.

⭐ file-post-create

Invoked after a non-existent file is created. the available template variables are the same as those available for the file-pre-create type.

⭐ file-post-edit

Invoked after an existent file is modified. the available template variables are the same as those available for the file-pre-create type.

πŸ“ file-pre-create

Invoked before a non-existent file is created. the available template variables are:

  • env (array) β€” array containing all the environment variables.
  • file_directory (string) β€” name of the directory where the impacted file is located.
  • file_name (string) β€” name of the impacted file.
  • file_path (string) β€” full path to the impacted file.

πŸ“ file-pre-edit

Invoked before an existent file is modified. the available template variables are the same as those available for the file-pre-create type.

πŸ‘‘ post-operation

Invoked at the end of the certificate request process. the available template variables are:

  • env (array) β€” array containing all the environment variables.
  • identifiers (string) β€” array containing the identifiers included in the requested certificate.
  • is_success (boolean) β€” True if the certificate request is successful.
  • key_type (string) β€” name of the asymmetric cryptography algorithm used to generate the certificate's key pair.
  • status (string) β€” human-readable status. if the certificate request failed, it contains the error description.
  • certificate_path (string) β€” path to the file containing the certificate.
  • private_key_path (string) β€” path to the file containing the private key.

🌈 DEFAULT HOOKS 🌈

Because so many people have the exact same needs, ACMEd ships with a set of hooks that should serve most situations πŸ’– no need to reinvent the wheel, bestie.

okay but heads up: hook names are unique. so the following names, and any other name starting with those, are reserved and you should not use them πŸ™ˆ pick something else, there's plenty of room.

  • git β€” this hook uses git(1) to archive private keys, public keys and certificates. you can customize the commit username and email by using the GIT_USERNAME and GIT_EMAIL environment variables respectively. cute, right? πŸŽ€
  • http-01-echo β€” this hook is designed to solve the http-01 challenge. for this purpose, it will write the proof into {{ env.HTTP_ROOT }}/{{ identifier }}/.well-known/acme-challenge/{{ file_name }}.

the web server must be configured so the file http://{{ identifier }}/.well-known/acme-challenge/{{ file_name }} can be accessed from the CA. yes, that has to actually be reachable, don't skip this part 😭

if HTTP_ROOT is not specified, it will be set to /var/www.

  • tls-alpn-01-tacd-tcp β€” this hook is designed to solve the tls-alpn-01 challenge using tacd(8). it requires pkill(1) to support the -F option ⚠️

tacd(8) will listen on the host defined by the TACD_HOST environment variable (default is the identifier to be validated) and on the port defined by the TACD_PORT environment variable (default is 5001).

tacd(8) will store its pid into {{ TACD_PID_ROOT }}/tacd_{{ identifier }}.pid. if TACD_PID_ROOT is not specified, it will be set to /run.

  • tls-alpn-01-tacd-unix β€” this hook is designed to solve the tls-alpn-01 challenge using tacd(8). it requires pkill(1) to support the -F option ⚠️

tacd(8) will listen on the unix socket {{ env.TACD_SOCK_ROOT }}/tacd_{{ identifier }}.sock. if TACD_SOCK_ROOT is not specified, it will be set to /run.

tacd(8) will store its pid into {{ TACD_PID_ROOT }}/tacd_{{ identifier }}.pid. if TACD_PID_ROOT is not specified, it will be set to /run.

⏰ TIME PERIODS ⏰

okay so ACMEd has its own time period format, and it’s vaguely inspired by ISO 8601 but it’s doing its own thing. Periods are formatted as PM[PM...], where M is a case-sensitive character that stands for a length, and P is an integer multiplier for the length right after it. The authorized lengths are:

  • s β€” second
  • m β€” minute
  • h β€” hour
  • d β€” day
  • w β€” week

Those PM couples can be specified multiple times and in any order, no cap.

For example, 1d42s and 40s20h4h2s both represents a period of one day and forty-two seconds. Math girlie slay ✨


🧚 TEMPLATE FILTERS 🧚

MiniJinja already gives you a ton of filters by default, and ACMEd adds these on top:

πŸ” rev_labels

Reverts the labels of a domain name (eg: mx1.example.org becomes org.example.mx1).


πŸ“ FILES πŸ“

  • /var/lib/acmed/accounts β€” default accounts private and public keys directory.
  • /etc/acmed/acmed.toml β€” default acmed(8) configuration file.
  • /var/lib/acmed/certs β€” default certificates and associated private keys directory.

🌸 EXAMPLES 🌸

The following example defines a typical endpoint, account and certificate for a domain, several subdomains and an IP address. It’s giving full setup energy πŸ’…

[[endpoint]]
name = "example name"
url = "https://acme.example.org/directory"
tos_agreed = true

[[account]]
name = "my test account"
contacts = [
  { mailto = "certs@exemple.net" }
]

[[certificate]]
endpoint = "example name"
account = "my test account"
identifiers = [
    { dns = "exemple.net", challenge = "http-01"},
    { dns = "1.exemple.net", challenge = "dns-01"},
    { dns = "2.exemple.net", challenge = "tls-alpn-01", env.TACD_PORT="5010"},
    { dns = "3.exemple.net", challenge = "tls-alpn-01", env.TACD_PORT="5011"},
    { ip = "203.0.113.1", challenge = "http-01"},
]
hooks = ["git", "http-01-echo", "tls-alpn-01-tacd-tcp", "some-dns-01-hook"]
env.HTTP_ROOT = "/srv/http"

It is possible to use echo(1) to solve the http-01 challenge and rm(1) to clean it. mkdir(1) and chmod(1) are used to prevent issues related to file access. Bestie, it’s giving very careful file permissions 🫢

[[hook]]
name = "http-01-echo-mkdir"
type = ["challenge-http-01"]
cmd = "mkdir"
args = [
    "-m", "0755",
    "-p", "{{ env.HTTP_ROOT | default('/var/www') }}/{{ identifier }}/.well-known/acme-challenge"
]

[[hook]]
name = "http-01-echo-echo"
type = ["challenge-http-01"]
cmd = "echo"
args = ["{{ proof }}"]
stdout = "{{ env.HTTP_ROOT | default('/var/www') }}/{{ identifier }}/.well-known/acme-challenge/{{ file_name }}"

[[hook]]
name = "http-01-echo-chmod"
type = ["challenge-http-01-clean"]
cmd = "chmod"
args = [
    "a+r",
    "{{ env.HTTP_ROOT | default('/var/www') }}/{{ identifier }}/.well-known/acme-challenge/{{ file_name }}"
]

[[hook]]
name = "http-01-echo-clean"
type = ["challenge-http-01-clean"]
cmd = "rm"
args = [
    "-f",
    "{{ env.HTTP_ROOT | default('/var/www') }}/{{ identifier }}/.well-known/acme-challenge/{{ file_name }}"
]

The hooks from the previous example can be grouped in order to reduce the number of hooks to define in the certificate. Group-chat organization, honestly iconic πŸŽ€

[[group]]
name = "http-01-echo-var-www"
hooks = [
    "http-01-echo-mkdir",
    "http-01-echo-echo",
    "http-01-echo-chmod",
    "http-01-echo-clean"
]

[[certificate]]
# Some fields omitted
hooks = ["http-01-echo"]
env.HTTP_ROOT = "/srv/http"

It is also possible to use sendmail(8) in a hook in order to notify someone when the certificate request process is done. Notification queen behavior πŸ’Œ

[[hook]]
name = "email-report"
type = ["post-operation"]
cmd = "sendmail"
args = [
    "-f", "noreply.certs@example.net",
    "contact@example.net"
]
stdin_str = """Subject: Certificate renewal {{ 'succeeded' if is_success else 'failed' }} for {{ identifiers.0 }}

The following certificate has {{ '' if is_success else '*not* ' }}been renewed.
identifiers: {% for ident in identifiers %}{% if not loop.first %}, {% endif %}{{ ident }}{% endfor %}
key type: {{ key_type }}
status: {{ status }}"""

πŸ‘€ SEE ALSO πŸ‘€

acmed(8), tacd(8)


πŸ“š STANDARDS πŸ“š


πŸ‘‘ AUTHORS πŸ‘‘

Rodolphe Bréard ⟨rodolphe@breard.tf⟩


okay bye bestie β€” this page is ACMED.TOML(5) from Debian, dated Mar 17, 2025, footer tea delivered, periodt πŸ’…βœ¨

πŸ“¦ where this page comes from
  • alpine 3.24: acmed-doc 0.25.0-r0 Β· MIT
  • πŸ’– girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms