use an encrypted client certificate to connect to a docker daemon
| ... | ... |
@@ -20,6 +20,7 @@ import ( |
| 20 | 20 |
dopts "github.com/docker/docker/opts" |
| 21 | 21 |
"github.com/docker/go-connections/sockets" |
| 22 | 22 |
"github.com/docker/go-connections/tlsconfig" |
| 23 |
+ "github.com/docker/notary/passphrase" |
|
| 23 | 24 |
"github.com/pkg/errors" |
| 24 | 25 |
"github.com/spf13/cobra" |
| 25 | 26 |
"golang.org/x/net/context" |
| ... | ... |
@@ -153,9 +154,30 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
|
| 153 | 153 |
|
| 154 | 154 |
var err error |
| 155 | 155 |
cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile) |
| 156 |
+ if tlsconfig.IsErrEncryptedKey(err) {
|
|
| 157 |
+ var ( |
|
| 158 |
+ passwd string |
|
| 159 |
+ giveup bool |
|
| 160 |
+ ) |
|
| 161 |
+ passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil) |
|
| 162 |
+ |
|
| 163 |
+ for attempts := 0; tlsconfig.IsErrEncryptedKey(err); attempts++ {
|
|
| 164 |
+ // some code and comments borrowed from notary/trustmanager/keystore.go |
|
| 165 |
+ passwd, giveup, err = passRetriever("private", "encrypted TLS private", false, attempts)
|
|
| 166 |
+ // Check if the passphrase retriever got an error or if it is telling us to give up |
|
| 167 |
+ if giveup || err != nil {
|
|
| 168 |
+ return errors.Wrap(err, "private key is encrypted, but could not get passphrase") |
|
| 169 |
+ } |
|
| 170 |
+ |
|
| 171 |
+ opts.Common.TLSOptions.Passphrase = passwd |
|
| 172 |
+ cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile) |
|
| 173 |
+ } |
|
| 174 |
+ } |
|
| 175 |
+ |
|
| 156 | 176 |
if err != nil {
|
| 157 | 177 |
return err |
| 158 | 178 |
} |
| 179 |
+ |
|
| 159 | 180 |
cli.defaultVersion = cli.client.ClientVersion() |
| 160 | 181 |
|
| 161 | 182 |
if opts.Common.TrustKey == "" {
|
| ... | ... |
@@ -33,7 +33,7 @@ func TestNewEnvClient(t *testing.T) {
|
| 33 | 33 |
envs: map[string]string{
|
| 34 | 34 |
"DOCKER_CERT_PATH": "invalid/path", |
| 35 | 35 |
}, |
| 36 |
- expectedError: "Could not load X509 key pair: open invalid/path/cert.pem: no such file or directory. Make sure the key is not encrypted", |
|
| 36 |
+ expectedError: "Could not load X509 key pair: open invalid/path/cert.pem: no such file or directory", |
|
| 37 | 37 |
}, |
| 38 | 38 |
{
|
| 39 | 39 |
envs: map[string]string{
|
| ... | ... |
@@ -17,7 +17,7 @@ github.com/vdemeester/shakers 24d7f1d6a71aa5d9cbe7390e4afb66b7eef9e1b3 |
| 17 | 17 |
golang.org/x/net c427ad74c6d7a814201695e9ffde0c5d400a7674 |
| 18 | 18 |
golang.org/x/sys 8f0908ab3b2457e2e15403d3697c9ef5cb4b57a9 |
| 19 | 19 |
github.com/docker/go-units 9e638d38cf6977a37a8ea0078f3ee75a7cdb2dd1 |
| 20 |
-github.com/docker/go-connections a2afab9802043837035592f1c24827fb70766de9 |
|
| 20 |
+github.com/docker/go-connections e15c02316c12de00874640cd76311849de2aeed5 |
|
| 21 | 21 |
golang.org/x/text f72d8390a633d5dfb0cc84043294db9f6c935756 |
| 22 | 22 |
|
| 23 | 23 |
github.com/RackSec/srslog 456df3a81436d29ba874f3590eeeee25d666f8a5 |
| ... | ... |
@@ -8,11 +8,13 @@ package tlsconfig |
| 8 | 8 |
import ( |
| 9 | 9 |
"crypto/tls" |
| 10 | 10 |
"crypto/x509" |
| 11 |
+ "encoding/pem" |
|
| 11 | 12 |
"fmt" |
| 12 | 13 |
"io/ioutil" |
| 13 | 14 |
"os" |
| 14 | 15 |
|
| 15 | 16 |
"github.com/Sirupsen/logrus" |
| 17 |
+ "github.com/pkg/errors" |
|
| 16 | 18 |
) |
| 17 | 19 |
|
| 18 | 20 |
// Options represents the information needed to create client and server TLS configurations. |
| ... | ... |
@@ -34,6 +36,9 @@ type Options struct {
|
| 34 | 34 |
// the system pool will be used. |
| 35 | 35 |
ExclusiveRootPools bool |
| 36 | 36 |
MinVersion uint16 |
| 37 |
+ // If Passphrase is set, it will be used to decrypt a TLS private key |
|
| 38 |
+ // if the key is encrypted |
|
| 39 |
+ Passphrase string |
|
| 37 | 40 |
} |
| 38 | 41 |
|
| 39 | 42 |
// Extra (server-side) accepted CBC cipher suites - will phase out in the future |
| ... | ... |
@@ -127,6 +132,67 @@ func adjustMinVersion(options Options, config *tls.Config) error {
|
| 127 | 127 |
return nil |
| 128 | 128 |
} |
| 129 | 129 |
|
| 130 |
+// IsErrEncryptedKey returns true if the 'err' is an error of incorrect |
|
| 131 |
+// password when tryin to decrypt a TLS private key |
|
| 132 |
+func IsErrEncryptedKey(err error) bool {
|
|
| 133 |
+ return errors.Cause(err) == x509.IncorrectPasswordError |
|
| 134 |
+} |
|
| 135 |
+ |
|
| 136 |
+// getPrivateKey returns the private key in 'keyBytes', in PEM-encoded format. |
|
| 137 |
+// If the private key is encrypted, 'passphrase' is used to decrypted the |
|
| 138 |
+// private key. |
|
| 139 |
+func getPrivateKey(keyBytes []byte, passphrase string) ([]byte, error) {
|
|
| 140 |
+ // this section makes some small changes to code from notary/tuf/utils/x509.go |
|
| 141 |
+ pemBlock, _ := pem.Decode(keyBytes) |
|
| 142 |
+ if pemBlock == nil {
|
|
| 143 |
+ return nil, fmt.Errorf("no valid private key found")
|
|
| 144 |
+ } |
|
| 145 |
+ |
|
| 146 |
+ var err error |
|
| 147 |
+ if x509.IsEncryptedPEMBlock(pemBlock) {
|
|
| 148 |
+ keyBytes, err = x509.DecryptPEMBlock(pemBlock, []byte(passphrase)) |
|
| 149 |
+ if err != nil {
|
|
| 150 |
+ return nil, errors.Wrap(err, "private key is encrypted, but could not decrypt it") |
|
| 151 |
+ } |
|
| 152 |
+ keyBytes = pem.EncodeToMemory(&pem.Block{Type: pemBlock.Type, Bytes: keyBytes})
|
|
| 153 |
+ } |
|
| 154 |
+ |
|
| 155 |
+ return keyBytes, nil |
|
| 156 |
+} |
|
| 157 |
+ |
|
| 158 |
+// getCert returns a Certificate from the CertFile and KeyFile in 'options', |
|
| 159 |
+// if the key is encrypted, the Passphrase in 'options' will be used to |
|
| 160 |
+// decrypt it. |
|
| 161 |
+func getCert(options Options) ([]tls.Certificate, error) {
|
|
| 162 |
+ if options.CertFile == "" && options.KeyFile == "" {
|
|
| 163 |
+ return nil, nil |
|
| 164 |
+ } |
|
| 165 |
+ |
|
| 166 |
+ errMessage := "Could not load X509 key pair" |
|
| 167 |
+ |
|
| 168 |
+ cert, err := ioutil.ReadFile(options.CertFile) |
|
| 169 |
+ if err != nil {
|
|
| 170 |
+ return nil, errors.Wrap(err, errMessage) |
|
| 171 |
+ } |
|
| 172 |
+ |
|
| 173 |
+ prKeyBytes, err := ioutil.ReadFile(options.KeyFile) |
|
| 174 |
+ if err != nil {
|
|
| 175 |
+ return nil, errors.Wrap(err, errMessage) |
|
| 176 |
+ } |
|
| 177 |
+ |
|
| 178 |
+ prKeyBytes, err = getPrivateKey(prKeyBytes, options.Passphrase) |
|
| 179 |
+ if err != nil {
|
|
| 180 |
+ return nil, errors.Wrap(err, errMessage) |
|
| 181 |
+ } |
|
| 182 |
+ |
|
| 183 |
+ tlsCert, err := tls.X509KeyPair(cert, prKeyBytes) |
|
| 184 |
+ if err != nil {
|
|
| 185 |
+ return nil, errors.Wrap(err, errMessage) |
|
| 186 |
+ } |
|
| 187 |
+ |
|
| 188 |
+ return []tls.Certificate{tlsCert}, nil
|
|
| 189 |
+} |
|
| 190 |
+ |
|
| 130 | 191 |
// Client returns a TLS configuration meant to be used by a client. |
| 131 | 192 |
func Client(options Options) (*tls.Config, error) {
|
| 132 | 193 |
tlsConfig := ClientDefault() |
| ... | ... |
@@ -139,13 +205,11 @@ func Client(options Options) (*tls.Config, error) {
|
| 139 | 139 |
tlsConfig.RootCAs = CAs |
| 140 | 140 |
} |
| 141 | 141 |
|
| 142 |
- if options.CertFile != "" || options.KeyFile != "" {
|
|
| 143 |
- tlsCert, err := tls.LoadX509KeyPair(options.CertFile, options.KeyFile) |
|
| 144 |
- if err != nil {
|
|
| 145 |
- return nil, fmt.Errorf("Could not load X509 key pair: %v. Make sure the key is not encrypted", err)
|
|
| 146 |
- } |
|
| 147 |
- tlsConfig.Certificates = []tls.Certificate{tlsCert}
|
|
| 142 |
+ tlsCerts, err := getCert(options) |
|
| 143 |
+ if err != nil {
|
|
| 144 |
+ return nil, err |
|
| 148 | 145 |
} |
| 146 |
+ tlsConfig.Certificates = tlsCerts |
|
| 149 | 147 |
|
| 150 | 148 |
if err := adjustMinVersion(options, tlsConfig); err != nil {
|
| 151 | 149 |
return nil, err |