A pepper is essentially a secret encryption key (it's a long secret string that's added to the password and the salt to ensure more entropy). With cloud key management services (e.g. both AWS and GCP have a KMS), I think it's more beneficial to just encrypt the hash before putting it in your database. Process looks like this:
Upon password creation:
1. Generate hash as hash of password + salt.
2. Encrypt the hash with a public key from KMS (you can store the public key in your server code).
3. In your database store the encrypted hash, the salt, plus some "key ID" that identifies which KMS public key you used (this is so you can rotate keys later).
Upon user login to verify the password:
1. Retrieve the user's encrypted password hash, salt and KMS key ID from the database.
2. Make a call to KMS to decrypt the hash (KMS internally stores the corresponding private key but never lets you access it).
3. Then hash the password the user entered + salt and compare it to the decrypted hash to see if there is a match.
Benefits of this are:
1. If an attacker steals your database, they can't decrypt any of the passwords or the password hashes.
2. KMS never exposes the private key of the async key pair, so you know this won't get exposed either. The only way to decrypt something is to make an API call to KMS.
3. Thus, the only valid attack really is if the attacker is able to gain the same access privileges as your server. But even then they still need to call KMS one-at-a-time to decrypt hashes, and all of those KMS calls are logged in an audit trail, so it should be much easier to see if you have anomalous calls to KMS. There is a huge benefit here in that it is impossible to do bulk decryption without a giant audit trail.
We do something similar for storing all DB entries (since our data is sensitive, as we're a financial services company). Even if someone gets access to our DB, all they'll get is garbage :)
Yep. Not sure the details of AWS, but in GCP access to KMS APIs and specific keys is controlled by IAM, and you can set "conditions" on IAM policies to restrict access by things like IP of the request: https://cloud.google.com/iam/docs/conditions-overview
Upon password creation:
1. Generate hash as hash of password + salt.
2. Encrypt the hash with a public key from KMS (you can store the public key in your server code).
3. In your database store the encrypted hash, the salt, plus some "key ID" that identifies which KMS public key you used (this is so you can rotate keys later).
Upon user login to verify the password:
1. Retrieve the user's encrypted password hash, salt and KMS key ID from the database.
2. Make a call to KMS to decrypt the hash (KMS internally stores the corresponding private key but never lets you access it).
3. Then hash the password the user entered + salt and compare it to the decrypted hash to see if there is a match.
Benefits of this are:
1. If an attacker steals your database, they can't decrypt any of the passwords or the password hashes.
2. KMS never exposes the private key of the async key pair, so you know this won't get exposed either. The only way to decrypt something is to make an API call to KMS.
3. Thus, the only valid attack really is if the attacker is able to gain the same access privileges as your server. But even then they still need to call KMS one-at-a-time to decrypt hashes, and all of those KMS calls are logged in an audit trail, so it should be much easier to see if you have anomalous calls to KMS. There is a huge benefit here in that it is impossible to do bulk decryption without a giant audit trail.