Closed jroper closed 5 years ago
@jroper That's unsafe though because it ends up in memory. The better approach would be if we can interact with pinentry
from within the CI and store the password in the agent prior to running the build. I spent some time looking at this a few days ago and it seemed promising.
But basically, you should never ever have a password of any sort in the JVM. Ever.
Why not? What makes the JVM any different from any other process? All processes can have their memory read by other processes of the same user, all processes can have remote execution vulnerabilities, what's so special about the JVM that makes it so dangerous? How would you deploy to Maven central without having the password in the JVM? How would you establish a TLS connection from a JVM to something else without having the key for that connection in the JVM? How would you accept a user login in a JVM based authentication service without having their password in memory?
GPG is built in a server-client architecture, the actual process that manages key material is not the gpg
command line utility itself, but rather a separate "agent" process that runs in the background. The reasons for this setup are:
1) It increases security by isolating any sensitive data in a separate process 2) It abstracts away the key store, as there may not be such a thing as a (password protected) private key file. Maybe an HSM is used or the key is supplied by some other mechanism. Requiring a private key file is an assumption that sbt-pgp made, based on its use of the bouncycastle library.
Considering this design, the recommended way to provide secret keys and passphrases, is to configure the agent and/or the password reader (pinentry) separately.
I understand that this might not always be the easiest solution, but simply reading the passphrase from credentials may interfere with setups that do not use a passphrase. According to the man page,
--passphrase-fd n
Read the passphrase from file descriptor n. Only the first line will be read from file
descriptor n. If you use 0 for n, the passphrase will be read from STDIN. This can only
be used if only one passphrase is supplied.
Note that since Version 2.0 this passphrase is only used if the option --batch has also
been given. Since Version 2.1 the --pinentry-mode also needs to be set to loopback.
additional flags need to be passed to gpg for this to function.
It would be preferable if we came up with a way to supply passwords without affecting default setups.
@jodersky In my experience, there are two archetypes here: local signing, and CI signing. In the CI model, credentials are encrypted and injected as environment variables (usually into some sort of docker container which executes the build) at build-time. In the local model, credentials are entered by the physical keyboard into a secure prompt. Currently, sbt-gpg is very well-optimized for the local signing case (❤️❤️❤️) and quite awkward for the CI case.
Fortunately, I don't think sbt-gpg itself needs to be optimized for the CI case. People are quite accustomed to cargo-culting their Travis/Circle-CI/whatever build configurations. A simple section in the readme discussing how to get a private key decryption password from an environment variable into pinentry
seems like all that would be needed here, ideally with something that is copy-pasteable into a YAML build script configuration.
Another option is to encourage CI signing models to AES-encrypt the keychain itself and remove the GPG password protection from the private key. This is equivalent security and works out-of-the-box with sbt-gpg. WDYT?
@jroper I never replied to your comment because I completely missed it. Sorry. Here's a more full reply:
Why not? What makes the JVM any different from any other process?
Several things. For starters, it's functionally impossible to have a protected section of memory in the JVM which is isolated from other components of the same JVM. This is a huge problem for a single-JVM platform like SBT. Not only can random plugins sniff your decryption password, but so can the code you are compiling (e.g. tests are run in-process)! This is a completely unacceptable and completely unnecessary security risk.
All processes can have their memory read by other processes of the same user, all processes can have remote execution vulnerabilities, what's so special about the JVM that makes it so dangerous?
For starters, this isn't actually true on modern operating systems with default security settings. OS-level process isolation has been the default on major OSes for several years now. Try it! Use pinentry
to unlock your key and then, from a separate process, try to extract that key. You cannot unless you either a) enter an administrators password, or b) are using an insecure OS (either because you changed the defaults or your OS has bad defaults to begin with). Contrast this with how trivial it is to convince a java
process to dump its heap.
But even beyond that… There is no way to obfuscate memory storage on the JVM. It's literally outside your control. Keys are always going to be trivially easy to extract. While it's still not particularly hard to extract key material from the core dump of a native process, it can be made an order of magnitude harder than is possible with the JVM.
The JVM is simply not a secure platform in any way. You should never have secrets of any sort in memory of a JVM process unless the process itself is being protected by some other means (e.g. an isolated hardened VM), and even then you should think twice.
How would you deploy to Maven central without having the password in the JVM?
Sonatype credentials are an order of magnitude less sensitive than private keys. For one thing, they're revokable. For another, they only enable publication to Maven Central, and even then only the groupIds to which you have access. Especially since most people don't use key splitting, a GPG private key decryption password is unbelievably powerful and also nearly impossible to revoke when leaked. (yes, I'm aware of revocation keys; they don't tend to work well in practice)
How would you establish a TLS connection from a JVM to something else without having the key for that connection in the JVM?
You have the key in the JVM. So the danger then is that another process on your local machine can dump that key and MITM the connection. This is a real danger and a lot of people work very hard to explicitly prevent it. For most cases though it doesn't matter, since you're only talking about a client side private key. Again, think about the scope of the leakage. If a client private session key leaks, it compromises… that session for that client. That's it. It would be much much more problematic if the server private key was in the JVM.
This is one of two reasons why you should never do SSL termination in the JVM when writing an http server. Ever. Terminate SSL in an external proxy such as nginx. (the second major reason is performance, btw)
How would you accept a user login in a JVM based authentication service without having their password in memory?
There are various ways to solve this problem, such as client-side encrypting the password prior to form submission, but generally it's considered an acceptable risk to hold a user password in memory for a short time. It really depends on your threat model. Again, how much risk are you exposing yourself to if the key leaks? User service authentication is much less of a risk than corporate private keys of all types. In a lot of cases, this is acceptable. If it's not acceptable, then you can either locally encrypt (like Keybase and 1Password do) or use an external process which pre-processes the data before it gets to the JVM (e.g. a tiny nginx plugin running in your SSL-terminating proxy is a very sound and very easy solution).
The JVM is simply not a secure platform in any way. You should never have secrets of any sort in memory of a JVM process unless the process itself is being protected by some other means (e.g. an isolated hardened VM), and even then you should think twice.
You should always be running inside a machine with least privileges, regardless, as a defense in depth strategy. I'm personally a fan of the DJB Way where every program runs in its own address space, but that's a little extreme considering JVM overhead. Rust is probably the best system level code base here.
I do wish that Isolates had taken off, but that's neither here nor there now.
This is one of two reasons why you should never do SSL termination in the JVM when writing an http server. Ever. Terminate SSL in an external proxy such as nginx. (the second major reason is performance, btw)
It's a bit more complex than that. External proxies like nginx do not take any special measures to protect the security key against exposure, and since external proxies are written in C, they are exposed to the openssl library and unsafe memory access, leading to things like heartbleed, which Java is thankfully not prone to. There's a number of security researchers who have argued that Java is more secure than C in this case (again, this was pre-Rust).
The attacks you mention require you already have access to JVM internals, or can cause the JVM to dump heap -- if an attacker can pivot and has account access to the machine, they can do the exact same thing to nginx / C program, so that's not a distinguishing factor. You mention that it can be made harder in C... but obfuscation isn't not something to rely on, attacks can happen offline, and many servers simply don't obfuscate in any case.
There is also a mitigating factor, which is that sites can use short-lived certificates. This is a strategy that Lemur and Vault PKI use, generating short TTL certs based off intermediate certificates, so revocation should not be a factor.
Ideally, web servers should not have access to private keys at all, and the private keys should be using PKCS#11 to talk to an HSM module like YubikeyHSM or CloudHSM -- Java manages this with plain keystores:
char[] pin = ...;
KeyStore ks = KeyStore.getInstance("PKCS11");
ks.load(null, pin);
(the second major reason is performance, btw)
This is much less of an issue now that AES intrinsics are enabled and the underlying engine issues have been worked on. Performance is around 36% slower in 1.9+181 according to Istio.
Thanks for everyone's feedback and further explanations. Since this plugin is intended to work with GnuPG, and that itself encourages passwords via a separate pinentry process, I'm going to leave it at that.
Regarding @djspiewak's comment
Another option is to encourage CI signing models to AES-encrypt the keychain itself and remove the GPG password protection from the private key.
this is exactly how the plugin is intended to be used. GPG keys are usually not the only sensitive material required in a CI run, so stripping them of a passphrase and using OpenSSL or the like to encrypt a "secrets bundle" with one single key is preferred.
It would be nice if the sbt-gpg could use the password from the credentials file. It's been a while since I've played with this stuff directly, but the way to do this is probably to use the
--passphrase-fd 0
argument, which tells gpg to read the passphrase from standard in. Then when you run the command, you write the passphrase to the processes OutputStream and then close it.