Let's start with the constraints:
- You need some kind of shared secret to send to a service to authenticate, so you need to be able to access the plaintext.
- This needs to be done in an automated fashion (unattended).
- You'd like to protect the credentials as much as possible.
First, we have to acknowledge that it is impossible to completely protect them, and it may or may not be worth doing so. If someone can execute code on your server, they can do almost anything the legitimate service can -- including reading the keys and using the keys to make requests to remote services. Depending on how much inconvenience you're willing to suffer, however, you can make them harder to use.
First, let's start with the "best practices" that have little to no impact on the ability to use these.
- Run each service on your server as a separate user. Ideally, add a MAC system like SELinux or AppArmor to further contain a compromise.
- Store keys/secrets in files that are only readable by the proper service and lock down the permissions as much as possible. (0400 on Linux.)
- Ensure that the keys provide as little access as necessary on the remote end. Uploading files to an S3 bucket? Create an Access Token with only that permission. (This way, if compromised, it limits the damage an attacker can do.)
If you need to go beyond this point, you can do a few things:
- Store the secrets in a hardware module. (Particularly if asymmetric crypto is an option.) This won't prevent an attacker from using your credentials, but will prevent them from exfiltrating them.
- Use a hardware module that requires human interaction. When the service starts, you'll need to enter a PIN, and then the service keeps the decrypted key in memory only. (Of course, a good attacker can probably get the key out of memory, but it raises the bar.)
Beyond this point, we get into the "silly for most applications": air gapping, interactive tokens requiring PINs for each signature, etc. The unattended nature will quickly go away.