Appending a secret (pepper) to Argon2 password hashes

Argon2 actually allows a pepper in the algorithm itself, called the secret. This would be the ideal way to use a pepper, unfortunately most language bindings I've looked at don't expose this parameter (of the bindings listed on GitHub only 2 of them specifically mention supporting keyed hashing).

If the secret parameter can't be used, argon2(hmac(pepper, password)) is a reasonable alternative. argon2 can be applied outside hmac for simplicity, as the way it's generally used it'll give you an output that includes an encoding of the salt and options used. Using hmac outside argon2 should be comparable for security, but then you'd need to keep track of the Argon2 parameters yourself.

As for the spec seeming to indicate that a pepper isn't necessary, I assume you're referring to this:

A trivial solution for password hashing is a keyed hash function such as HMAC. If the protocol designer prefers hashing without secret keys to avoid all the problems with key generation, storage, and update, then he has few alternatives: the generic mode PBKDF2, the Blowfish-based bcrypt, and scrypt.

The way I read it, what that's saying is that a keyed hash is actually the best way to hash passwords, so long as you protect the key really well (such as using an HSM). The problem is, protecting the key is difficult (HSMs cost money, supporting a pepper with an HSM takes development time), so slow hashes like scrypt, bcrypt, and Argon2 are used instead.

What it's not saying is that using a keyed hash in addition to Argon2 is a bad idea. Even if you can't protect the key with an HSM, it can still be worthwhile if it's better protected than the password hashes. The worst passwords will always be cracked no matter how high you set the cost (and your users' patience gives you a low upper limit); a pepper can prevent that.

Edit: Strike this entire comment. My approach isn't wrong, per se, but argon2 already offers a secret parameter. Just use this.

The simplest approach to this—and the one on the strongest cryptographic footing—is (simplistically) HMAC(key, argon2(password)). This also still allows the expensive argon2 calculation to be performed client-side for server-relief situations.