Husk

Documentation

Installation

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.

Install the dependencies

For debian/trixie the following packages are necessary:

# apt install clang nettle-dev pkg-config libssl-dev capnproto libsqlite3-dev

Install via 'cargo install'

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.

Install from repository

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.

Configuration

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

  • a milter (mail filter) which does the encryption,
  • an updater which periodically updates certificates with data from online sources and
  • A monitor process which collects statistical data from the above processes.

Milter, updater and monitor have their sections in the config file: [milter], [updater] and [monitor].

Milter

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.

Updater

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.

Monitor

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.

Logging

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.

Location

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.

Configure postfix

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.

Configure a cryptographic policy

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.

Usage

Husk has two aspects:

  • it can run as a background process
  • it can be used as a cli-tool for administrating the background process.

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 milter daemon

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.

Start the daemon

$ 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).

Stop the daemon

Stopping means terminating the process. Please note that your MTA will start producing error messages if a registered milter disappears.

$ husk daemon stop 

Status of the daemon

$ husk daemon status

If the daemon is running, daemon status will also report the pid of the process.

Using systemd

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.

Introducers

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.

Add an 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 

List introducers

$ husk introducer list 

Remove an introducer

$ 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.

Create an introducer

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

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.

Add 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 ....

List locals

$ husk locals list

Remove locals

$ husk locals remove --cert FINGERPRINT

If you remove a local, its certificate will stay in the certificate store, but will be dormant.

Signing key

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.

Setting the signing key

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.

Create a signing key

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.

Displaying the fingerprint of the current signing key

$ husk signkey fingerprint

Exporting the certificate of a signing key

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.

The updater

Start the updater

$ 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.

Stop the updater

$ husk updater stop 

Status of the updater

$ husk updater status

If the updater is running, updater status will also report the pid of the process.

Using systemd

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

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.

Start the monitor

$ 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.

Stop the updater

$ husk monitor stop 

Status of the updater

$ husk monitor status

If the monitor is running, monitor status will also report the pid of the process.

Using systemd

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.

Read out the data

$ 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.

Probe the milter

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

  • there are no (valid) certificates that can be found online.
  • the last attempt to get a certificate for that email address is less then throttle_max_age ago (see above).