Husk is written in Rust, so you need a recent rust toolchain to compile the source. If your distribution doesn't provide such a toolchain (or it is too old), you can get an up-to-date version from here.
For debian/trixie the following packages are necessary:
# apt install clang nettle-dev pkg-config libssl-dev capnproto libsqlite3-dev
Husk is a published crate, so you can use cargo install to install it.
Please note, that the name of the crate is husk-milter.
$ cargo install --locked husk-milter
This will install the binary husk into your home directory in
~/.cargo/bin.
If you want to enjoy the newest version of Husk, clone its repository and compile:
$ git clone https://gitlab.com/husk-project/husk-milter
$ cd husk-milter
$ cargo build --locked --release
This will create the binary husk in ./target/release/. Copy it to
a convenient place.
The compilation also generates a set of files containing code for
shell completion. The environment variable ASSET_OUT_DIR can be
used to specify the location where these file are generated.
$ ASSET_OUT_DIR=/tmp cargo build --locked --release
A subdirectory /tmp/shell_completions will be created, containing
the generated files.
The configuration file of Husk uses the toml format.
Husk uses OpenPGP certificates for encryption. It needs a storage location
for these certificates. As Husk uses Sequoia PGP,
the name for this parameter is sequoia_home.
sequoia_home = "/var/lib/husk/"
Husk will use the default cryptographic policy of Sequoia PGP. If you want to make modifications, you can specify a file by:
policy = "/path/to/file"
The default location is /etc/husk/crypto-policy. For details, see below.
Husk consists of
Milter, updater and monitor have their sections in the config file: [milter],
[updater] and [monitor].
The milter is a background process (daemon) that listens on a TCP port for
incoming requests. This port is configured under the name connection.
[milter]
connection = "localhost:3000"
This example tells Husk to listen on 127.0.0.1 (localhost), port 3000.
There is a corresponding configuration for the MTA, so that the MTA
knows where to find Husk (see below).
Husk queries online sources for missing certificates. For WKD and DANE,
the URL of the online source is derived from the email. Keyservers
have to be configured separately.
These keyservers are also used by the updater.
[milter]
...
keyservers = [ "hkps://keys.openpgp.org/", "hkps://mail-api.proton.me" ]
The milter creates a file which contains information to identify the milter
process - a pid file.
The location of the pid file for the milter can be configured by:
[milter]
...
pid_file = "/path/to/file"
If no pid_file is configured, /var/lib/husk/husk.pid is used.
The milter will can add a signature if it encrypts an email. For that
a signing_key has to be provided. Its storage location is configured
by:
[milter]
...
signing_key = "/var/lib/husk/signing_key.asc"
How the signing key can be set or created is described below. If no
signing key can be found, the milter will not add a signature. The
default location is /var/lib/husk/signing_key.asc.
When encrypting an email, the subject of the email is moved into the encrypted part and the subject is replaced. The replacement can be configured by:
[milter]
...
subject_replacement = "some string"
Default subject_replacement is ... (three dots).
The milter can search for missing certificates via the
Tor network. You will need a locally running
Tor process, as the milter will use it as a sock5 proxy.
[milter]
...
use_tor_via = "socks5://localhost:9050/"
Default is undefined, meaning the updates are not tunneled through Tor.
When the milter encrypts a message, it chooses the output format from the
capabilities the used certificate advertises. A certificate which signals
OpenPGPv6 capabilities will lead to an encrypted message in OpenPGPv6
format.
This will lead to undecryptable emails, when clients only capable of OpenPGPv4
are used (at the time of this writing for instance Thunderbird and GnuPG).
The milter can be forced to ignore OpenPGPv6 capabilities and produce
OpenPGPv4 format messages.
[milter]
...
rfc4880_profile = true
For the time being the default is true.
This is a problematic feature as it sabotages the upgrade path to OpenPGPv6.
Husk queries online sources for missing certificates. If a certificate isn't available, Husk will repeat the query when the next email for the associated recipient needs processing.
This can lead to many queries and as updates to online sources are rare, the outcome will likely not change. To prevent (possibly) useless queries, Husk keeps a history of failed attempts and will refrain from querying if the last failure is too recent.
The history can be configured by defining how long an entry in the history is considered 'too recent' and how many entries the history will store.
[milter]
...
throttle_max_age = 3600
Entries in the history older then throttle_max_age are no longer
considered 'too recent', in case the milter will repeat the query.
throttle_max_age is measured in seconds.
[milter]
...
throttle_max_size = 4096
This option limits the number of entries in the history. If the history overflows, failed queries might be repeated even if they are otherwise 'recent'.
The milter process can be probed for email addresses, to check how a mail for an certain email address would be treated (see below). To prevent unauthorized access, a secret has to be exchanged together with the email address.
[milter]
...
secret = "oochu9xieghaic2uu1iejooquos4uiP1aiBe8OhchiaYephu"
The secret has to be one word (no whitespaces) and should be random. If no secret is configured, probing will be rejected.
The updater periodically updates all certificates of the local certificate store from online sources.
The updates are spaced by a random time interval, specified by the minimum of seconds to wait and the maximum.
[updater]
...
wait_min = 5
wait_max = 3600
Like the milter, the updater can tunnel its requests through the Tor network. The configuration is similar:
[updater]
...
use_tor_via = "socks5://localhost:9050/"
Default is undefined, meaning the updates are not tunneled through Tor.
Like the milter, the updater also creates a pid file. Its location can be configured by:
[updater]
...
pid_file = "/path/to/file"
If no pid_file is configured, /var/lib/husk/updater.pid is used.
The updater maintains a state file so, that it can resume its work after a restart of the process. The location can be configured by:
[updater]
...
state_file = "/path/to/file"
The default location is /var/lib/husk/updater.state.
The monitor is a stopover which collects and aggregates statistical data from the milter and the updater. This data can then be read out to feed it into higher level monitoring solutions.
For communication, has two sockets - one for receiving data from the
milter and the updater, and a second for reading out the aggregated data.
[monitor]
...
input = "/var/lib/husk/monitor.socket"
output = "/var/lib/husk/monitor-readout.socket"
These are also the default values. Make sure that the monitor process has read/write permissions on those sockets. If not existing, the monitor process will try to create these entries which requires the corresponding permissions for the parent directory.
The monitor creates a pid file:
[monitor]
...
pid_file = "/var/lib/husk/monitor.pid"
This is also the default.
When the monitor is read out, there is an option to reset all counters to zero.
[monitor]
...
reset_on_read = true
This can be set to true or false. Default is true.
Husk uses log4rs for logging. The configuration of log4rs is merged
into the main configuration file under the node named logging. That
means that you take a log4rs configuration and prepend logging to
each section:
[logging.appenders.console]
kind = "console"
[logging.appenders.console.encoder]
pattern = "{d(%Y-%m-%d %H:%M:%S)(local)} {h({l})} [{t}] {m}{n}"
[logging.appenders.file]
kind = "file"
path = "/tmp/husk.log"
[logging.appenders.file.encoder]
pattern = "{d(%Y-%m-%d %H:%M:%S)(local)} {h({l})} [{t}] {m}{n}"
[logging.root]
level = "debug"
appenders = ["file"]
[logging.loggers."husk"]
level = "debug"
so that [appenders.console] becomes [logging.appenders.console]
and so on.
If you do not specify any logging, Husk will default to log to the console.
You can put the configuration file at any place and start Husk with
$ husk ... --config <LOCATION> ...
or use the default location /etc/husk/config.toml. In this case you
can skip --config.
This is a description on how to add Husk to the postfix MTA. If you want to
use Husk with a different MTA, please refer to its documentation on how to add
a milter.
For postfix, you have to tell the MTA where it can find the milter port.
The configuration entries are smtpd_milters and non_smtpd_milters.
smtpd_milters = inet:localhost:3000
non_smtpd_milters = $smtpd_milters
inet:localhost:3000 corresponds to the connection configuration parameter
in the Husk configuration.
The example above configures non_smtpd_milters by using the setting of
smtpd_milters (via the $ operator).
Please keep in mind that in case you configured multiple milters, the sequence is important, as Husk might change the content of the mail.
Husk uses the default cryptographic policy from Sequoia PGP. This default
policy rejects algorithms which are deprecated (like SHA1 for instance).
You can modify this default policy by providing a file containing the modifications you want to make. The format of this file is identical to the cryptographic policy used by Sequoia PGP.
An example to allow the usage of SHA1:
[hash_algorithms]
sha1.second_preimage_resistance = "always"
sha1.collision_resistance = "always"
A full documentation of the cryptographic policy can be found here.
The modifications are only used by Husk, other applications based on Sequoia PGP are not effected.
Husk has two aspects:
The CLI syntax follows the scheme NOUN (more NOUNs), VERB and then
parameters (if sensible). Like
$ husk introducer add --cert-file FILE --domains DOMAIN
or
$ husk daemon start --config config.toml
Husk includes a help system, which can be used by passing --help.
$ husk --help
or
$ husk introducer add --help
and so on. It shows you the available subcommands and parameters.
The daemon is the background process which processes mails and encrypts them if possible. It has to be started separately, the MTA expects it to be reachable and doesn't start it on it's own.
$ husk daemon start
or use
$ husk daemon start --config /path/to/the/configuration/file.toml
If you want to start the daemon as a background process add --background
to the command:
$ husk daemon start --background
Husk can (and should) run as an unprivileged process (keep in mind to use a port number above 1024, as ports below require a privileged process).
Stopping means terminating the process. Please note that your MTA will start producing error messages if a registered milter disappears.
$ husk daemon stop
$ husk daemon status
If the daemon is running, daemon status will also report the pid of the process.
In this example the binary is installed into /usr/local/bin/husk and
/var/lib/husk is used for certificate and key storage. The user account
husk is used for running the process.
[Unit]
Description=Husk encrypting milter
After=network.target
[Service]
Type=simple
WorkingDirectory=/var/lib/husk
ExecStart=/usr/local/bin/husk daemon start
User=husk
Group=husk
Restart=always
[Install]
WantedBy=multi-user.target
Create a file with the above content in /etc/systemd/system/husk.service and
use systemctl to enable and start the process.
For Husk to use a certificate for encryption, it has to be authenticated - meaning, the user id of the certificate (usually an email address) is indeed the user id of the key holder of said certificate.
This authentication can only be done by humans, but once it's done, its result can be shared.
This is what an introducer does - it authenticates certificates and publishes the result in form of a certification which becomes part of the certificate.
Husk allows to use introducers to benefit from their work. At the same time, introducers are limited to a list of domains for which certifications are accepted. This is a restriction Husk imposes. The list of domains is specified by the administrator, not the introducer.
To add an introducer you need its certificate in a file. When adding you have to specify a list of domains this introducer is allowed to introduce.
$ husk introducer add --cert-file FILE --domains example.com
You can specify a list of domains by
... ---domains domain1 domains2 domain3
FILE must contain a valid certificate, not expired and not revoked.
In case you already added the certificate to Husk, you can promote it by using its fingerprint.
$ husk introducer add --cert FINGERPRINT --domains example.com
$ husk introducer list
$ husk introducer remove --cert FINGERPRINT
If you remove an introducer, the certificate will stay in the certificate
store - only it's special powers will be removed. You can reactivate an
introducer by using husk introducer add --cert FINGERPRINT ..., if you
accidentally removed one.
Introducers are maintained separately from Husk. The following is an example
on how to create and use one, using the command line tool sq from Sequoia PGP.
For the sake of this example we have two sides: Husk as consumer and CA
as a producer. CA stands for certificate authority. CA is an ordinary
OpenPGP key, what makes it a certificate authority is how it is used.
In most cases, CA is not you, but the one who's work you want to use - see above.
Create a key:
$ sq key generate --own-key --userid 'My CA'
You can create a more complex key with more user ids and capabilities,
but for a CA this is enough.
This CA can now be used to certify other keys (or more precise: certificates).
Let B0F698D7419862EAC6F435B0D5FC683922578868 be the fingerprint of a
certificate.
$ sq pki vouch add --certifier-userid 'My CA' \
--cert B0F698D7419862EAC6F435B0D5FC683922578868 \
--all
This example certifies all user ids of B0F698D7419862EAC6F435B0D5FC683922578868.
You can be more selective by using --userid or --email instead of --all.
Now publish the certified key:
$ sq network keyserver publish --cert B0F698D7419862EAC6F435B0D5FC683922578868
Repeat the last two steps for all certificates you want to certify.
Now let Husk know and use your CA. First copy the certificate of CA
to where Husk is running.
$ sq cert export --cert-userid 'My CA' > ca.pgp
$ scp ca.pgp $HUSK_SERVER
On the $HUSK_SERVER:
$ husk introducer add --cert-file ca.pgp --domains ...
Choose the list of domains you want to trust CA to introduce carefully.
Let's assume the B0F698D7419862EAC6F435B0D5FC683922578868 is the certificate
of alice@example.com and you trust CA for that domain.
$ husk introducer add --cert-file ca.pgp --domains example.com
If now Husk encounters a mail addressed to alice@example.com (and her
certificate isn't yet locally available), Husk will query online sources
and check the results for a certification of CA (as CA is registered
as an introducer for *@example.com).
If such a certification can be found, the right certificate is identified and Husk will use it for encryption (and stores it locally).
Locals are certificates which are added to Husk by the administrator
and do not require to be introduced.
If you want to test Husk or want to use it only with a few dedicated
certificates, you can skip creating an introducer and use locals.
$ husk locals add --cert-file FILE
FILE must contain a valid certificate, not expired and not revoked.
Locals can be promoted to introducers by using
husk introducer add --cert FINGERPRINT ....
$ husk locals list
$ husk locals remove --cert FINGERPRINT
If you remove a local, its certificate will stay in the certificate store, but will be dormant.
Husk can sign/encrypt mails instead of only encrypting them. For that, Husk needs the appropriate key.
Signing is done when a signing key is configured, without that key Husk will only encrypt a message.
If you already have a key, you can import it into the keystore of Husk by:
$ husk signkey import --cert-file FILE
FILE must contain an OpenPGP key (public and private key material) which
is not expired or revoked and must be able to sign messages.
The new key will be used instantly, a restart of the daemon is not necessary.
You can tell Husk to create a signing key by:
$ hust signkey create
This will create a key which can only sign messages. With --userid you can
specify a custom user id:
$ hust signkey create --userid "My special signing key"
Without --userid Husk will use Husk signing key as user id.
$ husk signkey fingerprint
You might want to make the signing key public, so that users receiving a signed/encrypted message from Husk can identify the signer.
$ husk signkey print
will print the certificate (public key) to the console, so that you can share it with the world.
$ husk updater start
or use
$ husk updater start --config /path/to/the/configuration/file.toml
If you want to start the updater as a background process, add --background
to the command:
$ husk updater start --background
The Husk updater must run as the same user as the milter daemon.
$ husk updater stop
$ husk updater status
If the updater is running, updater status will also report the pid of the process.
In this example the binary is installed into /usr/local/bin/husk and
/var/lib/husk is used for certificate and key storage. The user account
husk is used for running the process.
[Unit]
Description=Husk certificate updater
After=network.target
[Service]
Type=simple
WorkingDirectory=/var/lib/husk
ExecStart=/usr/local/bin/husk updater start
User=husk
Group=husk
Restart=always
[Install]
WantedBy=multi-user.target
Create a file with the above content in /etc/systemd/system/husk-updater.service and
use systemctl to enable and start the process.
The monitor functions as a stopover to aggregate statistical data before it is read out for further processing. If the monitor process isn't running, statistical data is not collected.
$ husk monitor start
or use
$ husk monitor start --config /path/to/the/configuration/file.toml
If you want to start the monitor as a background process, add --background
to the command:
$ husk monitor start --background
The Husk monitor must run as the same user as the milter daemon.
$ husk monitor stop
$ husk monitor status
If the monitor is running, monitor status will also report the pid of the process.
In this example the binary is installed into /usr/local/bin/husk and
/var/lib/husk is used for certificate and key storage. The user account
husk is used for running the process.
[Unit]
Description=Husk certificate monitor process
After=network.target
[Service]
Type=simple
WorkingDirectory=/var/lib/husk
ExecStart=/usr/local/bin/husk monitor start
User=husk
Group=husk
Restart=always
[Install]
WantedBy=multi-user.target
Create a file with the above content in /etc/systemd/system/husk-monitor.service and
use systemctl to enable and start the process.
$ husk monitor readout
encryption_error:1
encrypted_rcpt:12
unencrypted_rcpt:7
unencryptable_mail:5
e2e_encrypted_mail:7
injected_mail:9
encrypted_mail:12
updater_cert_updated:1
updater_cert_keep:1
The meaning of the returned fields:
encryption_error: Number of attempts an encryption failed due to an
unsupported certificate.encrypted_rcpt: Number of recipients with a valid certificate.unencrypted_rcpt: Number of recipients without a valid certificate.e2e_encrypted_mail: Number of incoming emails which are already encrypted.injected_mail: Number of times a new email had to be send because the
list of recipients had to be split (for instance in encryptable and
unencryptable recipients).encrypted_mail: Number of time an email got encrypted.updater_cert_updated: Number of times the updater found an update
for a certificate online.updater_cert_keep: Number of certificates the updater found no update.Fields with a value of zero are skipped in the output.
You can get a json formatted output by:
$ husk monitor readout --json
The json output will contain all fields (even if their value is zero).
If the monitor is configured to reset_on_read, the counters are set to
zero after a readout.
Using the monitor subcommand, the running milter process can be queried
for email addresses.
$ husk monitor probe --email alice@example.com
alice@example.com has 1 local certificates
introduced by 7033BAF30F9E9D1338D0A5D35E3E9BC3663623E5 - <ca@example.com>, CA
or
$ husk monitor probe --email bob@example.com
bob@example.com has 0 local certificates
certificate queries for bob@example.com are currently throttled
introduced by 7033BAF30F9E9D1338D0A5D35E3E9BC3663623E5 - <ca@example.com>, CA
If there are no lines starting with introduced by, then the domain of
the requested email has no introducer. If the domain has several introducers,
all are listed.
The line certificate queries... indicates that
throttle_max_age ago (see above).