Repository: distribution/distribution
Pull request #3169 (2020-05-26 20:15:22)

Added flag for user configurable cipher suites
Configuration of list of cipher suites allows a user to disable use
of weak ciphers or continue to support them for legacy usage if they
so choose.

List of available cipher suites at:
https://golang.org/pkg/crypto/tls/#pkg-constants

Default cipher suites have been updated to:
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_AES_128_GCM_SHA256
- TLS_CHACHA20_POLY1305_SHA256
- TLS_AES_256_GCM_SHA384

MinimumTLS has also been updated to include TLS 1.3 as an option
and now defaults to TLS 1.2 since 1.0 and 1.1 have been deprecated.

Also updated install-dev-tools to use recommended golangci-lint
install to fix CI pipeline.

Signed-off-by: David Luu <david@davidluu.info>

gabrielcalderon: Any plans to merge this? This is way overdue. 
milosgajdos: @d-luu can you rebase, please? Thanks.
d-luu: let me verify something before merging
d-luu: @milosgajdos 
I was checking to see if by using the registry docker image we can set the cipher suite via the indexed environment variable `REGISTRY_HTTL_TLS_CIPHERSUITES_0=TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256` rather than `REGISTRY_HTTL_TLS_CIPHERSUITES=' - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256` but it doesn't look to be the case. The former doesn't work, the latter does.

I recall awhile back being able to pass ClientCAs via the list index `_0` env var and I don't see anything that stands out at the moment that would cause it to be treated differently: https://github.com/distribution/distribution/blob/main/configuration/parser.go

milosgajdos: Thanks for the awesome work on this, @d-luu

I'm planning on setting this up locally over this weekend and having a proper look. 
codecov-io: # [Codecov](https://codecov.io/gh/distribution/distribution/pull/3169?src=pr&el=h1) Report
> Merging [#3169](https://codecov.io/gh/distribution/distribution/pull/3169?src=pr&el=desc) (7fff010) into [main](https://codecov.io/gh/distribution/distribution/commit/22c074842eaad74036164f95b2182a9d0fe484b5?el=desc) (22c0748) will **increase** coverage by `0.38%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/distribution/distribution/pull/3169/graphs/tree.svg?width=650&height=150&src=pr&token=q4gaG5NuY3)](https://codecov.io/gh/distribution/distribution/pull/3169?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##             main    #3169      +/-   ##
==========================================
+ Coverage   55.84%   56.22%   +0.38%     
==========================================
  Files         102      102              
  Lines        7222     7224       +2     
==========================================
+ Hits         4033     4062      +29     
+ Misses       2552     2520      -32     
- Partials      637      642       +5     
```


| [Impacted Files](https://codecov.io/gh/distribution/distribution/pull/3169?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [...bution/distribution/configuration/configuration.go](https://codecov.io/gh/distribution/distribution/pull/3169/diff?src=pr&el=tree#diff-c3JjL2dpdGh1Yi5jb20vZGlzdHJpYnV0aW9uL2Rpc3RyaWJ1dGlvbi9jb25maWd1cmF0aW9uL2NvbmZpZ3VyYXRpb24uZ28=) | `62.03% <0.00%> (ø)` | |
| [...com/distribution/distribution/registry/registry.go](https://codecov.io/gh/distribution/distribution/pull/3169/diff?src=pr&el=tree#diff-c3JjL2dpdGh1Yi5jb20vZGlzdHJpYnV0aW9uL2Rpc3RyaWJ1dGlvbi9yZWdpc3RyeS9yZWdpc3RyeS5nbw==) | `43.39% <0.00%> (+13.39%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/distribution/distribution/pull/3169?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/distribution/distribution/pull/3169?src=pr&el=footer). Last update [22c0748...7fff010](https://codecov.io/gh/distribution/distribution/pull/3169?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b228dbec..9dd01f91 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,7 +30,7 @@ jobs:
     - name: Set up Go
       uses: actions/setup-go@v2
       with:
-        go-version: 1.15.*
+        go-version: 1.14.*
 
     - name: Dependencies
       run: |
diff --git a/Dockerfile b/Dockerfile
index 49c7ea19..170fea8f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
-ARG GO_VERSION=1.15
+ARG GO_VERSION=1.13.8
 
-FROM golang:${GO_VERSION}-alpine3.12 AS build
+FROM golang:${GO_VERSION}-alpine3.11 AS build
 
 ENV DISTRIBUTION_DIR /go/src/github.com/distribution/distribution
 ENV BUILDTAGS include_oss include_gcs
@@ -18,7 +18,7 @@ WORKDIR $DISTRIBUTION_DIR
 COPY . $DISTRIBUTION_DIR
 RUN CGO_ENABLED=0 make PREFIX=/go clean binaries && file ./bin/registry | grep "statically linked"
 
-FROM alpine:3.12
+FROM alpine:3.11
 
 RUN set -ex \
     && apk add --no-cache ca-certificates
diff --git a/Makefile b/Makefile
index 5e01549f..677d63b7 100644
--- a/Makefile
+++ b/Makefile
@@ -50,7 +50,7 @@ version/version.go:
 
 check: ## run all linters (TODO: enable "unused", "varcheck", "ineffassign", "unconvert", "staticheck", "goimports", "structcheck")
 	@echo "$(WHALE) $@"
-	@GO111MODULE=off golangci-lint run
+	@golangci-lint run
 
 test: ## run tests, except integration test with test.short
 	@echo "$(WHALE) $@"
diff --git a/README.md b/README.md
index fafd5bdb..e16a4037 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,6 @@ Project, and VMware Harbor Registry.
 [![Build Status](https://github.com/distribution/distribution/workflows/CI/badge.svg?branch=main&event=push)](https://github.com/distribution/distribution/actions?query=workflow%3ACI)
 [![GoDoc](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/distribution/distribution)
 [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE)
-[![codecov](https://codecov.io/gh/distribution/distribution/branch/main/graph/badge.svg)](https://codecov.io/gh/distribution/distribution)
 
 This repository contains the following components:
 
diff --git a/configuration/configuration.go b/configuration/configuration.go
index 34884dd2..dd315485 100644
--- a/configuration/configuration.go
+++ b/configuration/configuration.go
@@ -111,6 +111,9 @@ type Configuration struct {
 			// Specifies the lowest TLS version allowed
 			MinimumTLS string `yaml:"minimumtls,omitempty"`
 
+			// Specifies a list of cipher suites allowed
+			CipherSuites []string `yaml:"ciphersuites,omitempty"`
+
 			// LetsEncrypt is used to configuration setting up TLS through
 			// Let's Encrypt instead of manually specifying certificate and
 			// key. If a TLS certificate is specified, the Let's Encrypt
diff --git a/contrib/compose/README.md b/contrib/compose/README.md
index 4eb49ad2..01617860 100644
--- a/contrib/compose/README.md
+++ b/contrib/compose/README.md
@@ -60,7 +60,7 @@ to the 1.0 registry. Requests from newer clients will route to the 2.0 registry.
 		$ docker-compose build
 		registryv1 uses an image, skipping
 		Building registryv2...
-		Step 0 : FROM golang:1.15
+		Step 0 : FROM golang:1.4
 		
 		...
 		
diff --git a/docs/configuration.md b/docs/configuration.md
index c386f203..6027ad96 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -795,7 +795,10 @@ http:
     clientcas:
       - /path/to/ca.pem
       - /path/to/another/ca.pem
-    minimumtls: tls1.0
+    minimumtls: tls1.2
+    ciphersuites:
+      - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
+      - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
     letsencrypt:
       cachefile: /path/to/cache-file
       email: emailused@letsencrypt.com
@@ -831,10 +834,49 @@ and proxy connections to the registry server.
 
 | Parameter | Required | Description                                           |
 |-----------|----------|-------------------------------------------------------|
-| `certificate` | yes  | Absolute path to the x509 certificate file.           |
-| `key`         | yes  | Absolute path to the x509 private key file.           |
-| `clientcas`   | no   | An array of absolute paths to x509 CA files.          |
-| `minimumtls`  | no   | Minimum TLS version allowed (tls1.0, tls1.1, tls1.2). Defaults to tls1.0 |
+| `certificate`  | yes  | Absolute path to the x509 certificate file.           |
+| `key`          | yes  | Absolute path to the x509 private key file.           |
+| `clientcas`    | no   | An array of absolute paths to x509 CA files.          |
+| `minimumtls`   | no   | Minimum TLS version allowed (tls1.0, tls1.1, tls1.2, tls1.3). Defaults to tls1.2 |
+| `ciphersuites` | no   | Cipher suites allowed. Please see below for allowed values and default. |
+
+Available cipher suites:
+- TLS_RSA_WITH_RC4_128_SHA
+- TLS_RSA_WITH_3DES_EDE_CBC_SHA
+- TLS_RSA_WITH_AES_128_CBC_SHA
+- TLS_RSA_WITH_AES_256_CBC_SHA
+- TLS_RSA_WITH_AES_128_CBC_SHA256
+- TLS_RSA_WITH_AES_128_GCM_SHA256
+- TLS_RSA_WITH_AES_256_GCM_SHA384
+- TLS_ECDHE_ECDSA_WITH_RC4_128_SHA
+- TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
+- TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
+- TLS_ECDHE_RSA_WITH_RC4_128_SHA
+- TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA
+- TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
+- TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
+- TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
+- TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
+- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
+- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
+- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
+- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
+- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
+- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
+- TLS_AES_128_GCM_SHA256
+- TLS_AES_256_GCM_SHA384
+- TLS_CHACHA20_POLY1305_SHA256
+
+Default cipher suites:
+- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
+- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
+- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
+- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
+- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
+- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
+- TLS_AES_128_GCM_SHA256
+- TLS_CHACHA20_POLY1305_SHA256
+- TLS_AES_256_GCM_SHA384
 
 ### `letsencrypt`
 
diff --git a/go.mod b/go.mod
index 4461aa1a..61d7ec16 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/distribution/distribution/v3
 
-go 1.15
+go 1.12
 
 require (
 	github.com/Azure/azure-sdk-for-go v16.2.1+incompatible
@@ -9,7 +9,7 @@ require (
 	github.com/aws/aws-sdk-go v1.34.9
 	github.com/bitly/go-simplejson v0.5.0 // indirect
 	github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
-	github.com/bshuster-repo/logrus-logstash-hook v1.0.0
+	github.com/bshuster-repo/logrus-logstash-hook v0.4.1
 	github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd
 	github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b // indirect
 	github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 // indirect
diff --git a/go.sum b/go.sum
index 4e9ff1d1..284c0cf8 100644
--- a/go.sum
+++ b/go.sum
@@ -18,8 +18,8 @@ github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkN
 github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
-github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
-github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
+github.com/bshuster-repo/logrus-logstash-hook v0.4.1 h1:pgAtgj+A31JBVtEHu2uHuEx0n+2ukqUJnS2vVe5pQNA=
+github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
 github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng=
 github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
 github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ=
diff --git a/registry/registry.go b/registry/registry.go
index 8f891b17..66fa6d11 100644
--- a/registry/registry.go
+++ b/registry/registry.go
@@ -9,6 +9,7 @@ import (
 	"net/http"
 	"os"
 	"os/signal"
+	"strings"
 	"syscall"
 	"time"
 
@@ -18,7 +19,6 @@ import (
 	"github.com/bugsnag/bugsnag-go"
 	"github.com/docker/go-metrics"
 	gorhandlers "github.com/gorilla/handlers"
-	"github.com/sirupsen/logrus"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 	"github.com/yvasiyarov/gorelic"
@@ -34,6 +34,60 @@ import (
 	"github.com/distribution/distribution/v3/version"
 )
 
+// a map of TLS cipher suite names to constants in https://golang.org/pkg/crypto/tls/#pkg-constants
+var cipherSuites = map[string]uint16{
+	// TLS 1.0 - 1.2 cipher suites
+	"TLS_RSA_WITH_RC4_128_SHA":                      tls.TLS_RSA_WITH_RC4_128_SHA,
+	"TLS_RSA_WITH_3DES_EDE_CBC_SHA":                 tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
+	"TLS_RSA_WITH_AES_128_CBC_SHA":                  tls.TLS_RSA_WITH_AES_128_CBC_SHA,
+	"TLS_RSA_WITH_AES_256_CBC_SHA":                  tls.TLS_RSA_WITH_AES_256_CBC_SHA,
+	"TLS_RSA_WITH_AES_128_CBC_SHA256":               tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
+	"TLS_RSA_WITH_AES_128_GCM_SHA256":               tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
+	"TLS_RSA_WITH_AES_256_GCM_SHA384":               tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
+	"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA":              tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
+	"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA":          tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
+	"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA":          tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
+	"TLS_ECDHE_RSA_WITH_RC4_128_SHA":                tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
+	"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA":           tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
+	"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA":            tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
+	"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA":            tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
+	"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256":       tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
+	"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256":         tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
+	"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256":         tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+	"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256":       tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+	"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384":         tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+	"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384":       tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+	"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256":   tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
+	"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
+	// TLS 1.3 cipher suites
+	"TLS_AES_128_GCM_SHA256":       tls.TLS_AES_128_GCM_SHA256,
+	"TLS_AES_256_GCM_SHA384":       tls.TLS_AES_256_GCM_SHA384,
+	"TLS_CHACHA20_POLY1305_SHA256": tls.TLS_CHACHA20_POLY1305_SHA256,
+}
+
+// a list of default ciphersuites to utilize
+var defaultCipherSuites = []uint16{
+	tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+	tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+	tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
+	tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
+	tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+	tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+	tls.TLS_AES_128_GCM_SHA256,
+	tls.TLS_CHACHA20_POLY1305_SHA256,
+	tls.TLS_AES_256_GCM_SHA384,
+}
+
+// maps tls version strings to constants
+var defaultTLSVersionStr = "tls1.2"
+var tlsVersions = map[string]uint16{
+	// user specified values
+	"tls1.0": tls.VersionTLS10,
+	"tls1.1": tls.VersionTLS11,
+	"tls1.2": tls.VersionTLS12,
+	"tls1.3": tls.VersionTLS13,
+}
+
 // this channel gets notified when process receives signal. It is global to ease unit testing
 var quit = make(chan os.Signal, 1)
 
@@ -128,6 +182,35 @@ func NewRegistry(ctx context.Context, config *configuration.Configuration) (*Reg
 	}, nil
 }
 
+// takes a list of cipher suites and converts it to a list of respective tls constants
+// if an empty list is provided, then the defaults will be used
+func getCipherSuites(names []string) ([]uint16, error) {
+	if len(names) == 0 {
+		return defaultCipherSuites, nil
+	}
+	cipherSuiteConsts := make([]uint16, len(names))
+	for i, name := range names {
+		cipherSuiteConst, ok := cipherSuites[name]
+		if !ok {
+			return nil, fmt.Errorf("unknown TLS cipher suite '%s' specified for http.tls.cipherSuites", name)
+		}
+		cipherSuiteConsts[i] = cipherSuiteConst
+	}
+	return cipherSuiteConsts, nil
+}
+
+// takes a list of cipher suite ids and converts it to a list of respective names
+func getCipherSuiteNames(ids []uint16) []string {
+	if len(ids) == 0 {
+		return nil
+	}
+	names := make([]string, len(ids))
+	for i, id := range ids {
+		names[i] = tls.CipherSuiteName(id)
+	}
+	return names
+}
+
 // ListenAndServe runs the registry's HTTP server.
 func (registry *Registry) ListenAndServe() error {
 	config := registry.config
@@ -138,35 +221,27 @@ func (registry *Registry) ListenAndServe() error {
 	}
 
 	if config.HTTP.TLS.Certificate != "" || config.HTTP.TLS.LetsEncrypt.CacheFile != "" {
-		var tlsMinVersion uint16
 		if config.HTTP.TLS.MinimumTLS == "" {
-			tlsMinVersion = tls.VersionTLS10
-		} else {
-			switch config.HTTP.TLS.MinimumTLS {
-			case "tls1.0":
-				tlsMinVersion = tls.VersionTLS10
-			case "tls1.1":
-				tlsMinVersion = tls.VersionTLS11
-			case "tls1.2":
-				tlsMinVersion = tls.VersionTLS12
-			default:
-				return fmt.Errorf("unknown minimum TLS level '%s' specified for http.tls.minimumtls", config.HTTP.TLS.MinimumTLS)
-			}
-			dcontext.GetLogger(registry.app).Infof("restricting TLS to %s or higher", config.HTTP.TLS.MinimumTLS)
+			config.HTTP.TLS.MinimumTLS = defaultTLSVersionStr
+		}
+		tlsMinVersion, ok := tlsVersions[config.HTTP.TLS.MinimumTLS]
+		if !ok {
+			return fmt.Errorf("unknown minimum TLS level '%s' specified for http.tls.minimumtls", config.HTTP.TLS.MinimumTLS)
 		}
+		dcontext.GetLogger(registry.app).Infof("restricting TLS version to %s or higher", config.HTTP.TLS.MinimumTLS)
+
+		tlsCipherSuites, err := getCipherSuites(config.HTTP.TLS.CipherSuites)
+		if err != nil {
+			return err
+		}
+		dcontext.GetLogger(registry.app).Infof("restricting TLS cipher suites to: %s", strings.Join(getCipherSuiteNames(tlsCipherSuites), ","))
+
 		tlsConf := &tls.Config{
 			ClientAuth:               tls.NoClientCert,
 			NextProtos:               nextProtos(config),
 			MinVersion:               tlsMinVersion,
 			PreferServerCipherSuites: true,
-			CipherSuites: []uint16{
-				tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
-				tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
-				tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
-				tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
-				tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
-				tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
-			},
+			CipherSuites:             tlsCipherSuites,
 		}
 
 		if config.HTTP.TLS.LetsEncrypt.CacheFile != "" {
@@ -286,7 +361,7 @@ func configureLogging(ctx context.Context, config *configuration.Configuration)
 		})
 	case "logstash":
 		log.SetFormatter(&logstash.LogstashFormatter{
-			Formatter: &logrus.JSONFormatter{TimestampFormat: time.RFC3339Nano},
+			TimestampFormat: time.RFC3339Nano,
 		})
 	default:
 		// just let the library use default on empty string.
diff --git a/vendor/github.com/bshuster-repo/logrus-logstash-hook/.travis.yml b/vendor/github.com/bshuster-repo/logrus-logstash-hook/.travis.yml
index 0df15979..60c00ef6 100644
--- a/vendor/github.com/bshuster-repo/logrus-logstash-hook/.travis.yml
+++ b/vendor/github.com/bshuster-repo/logrus-logstash-hook/.travis.yml
@@ -1,10 +1,13 @@
 language: go
 sudo: false
 
-go:
-  - "1.11.x"
-  - "1.12.x"
-  - "tip"
+matrix:
+  include:
+    - go: 1.3
+    - go: 1.4
+    - go: 1.5
+    - go: 1.6
+    - go: tip
 
 install:
   - # Skip
@@ -12,5 +15,5 @@ install:
 script:
   - go get -t -v ./...
   - diff -u <(echo -n) <(gofmt -d .)
-  - go vet .
+  - go tool vet .
   - go test -v -race ./...
diff --git a/vendor/github.com/bshuster-repo/logrus-logstash-hook/CHANGELOG.md b/vendor/github.com/bshuster-repo/logrus-logstash-hook/CHANGELOG.md
index 293d799a..31c8b5f3 100644
--- a/vendor/github.com/bshuster-repo/logrus-logstash-hook/CHANGELOG.md
+++ b/vendor/github.com/bshuster-repo/logrus-logstash-hook/CHANGELOG.md
@@ -1,11 +1,5 @@
 # Changelog
 
-## 1.0
-
- * Remove the old API: `NewConnWith`, `WithPrefix` and etc and move to a simple `New` function.
- * Prefix is no longer supported in this package.
- * Change the Hook structure to have only two members: `logrus.Formatter` and `io.Writer`.
-
 ## 0.4
 
  * Update the name of the package from `logrus_logstash` to `logrustash`
diff --git a/vendor/github.com/bshuster-repo/logrus-logstash-hook/README.md b/vendor/github.com/bshuster-repo/logrus-logstash-hook/README.md
index f5cf1638..9cc4378c 100644
--- a/vendor/github.com/bshuster-repo/logrus-logstash-hook/README.md
+++ b/vendor/github.com/bshuster-repo/logrus-logstash-hook/README.md
@@ -1,35 +1,30 @@
-# Logstash hook for logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:" />
-[![Build Status](https://travis-ci.org/bshuster-repo/logrus-logstash-hook.svg?branch=master)](https://travis-ci.org/bshuster-repo/logrus-logstash-hook)
-[![Go Report Status](https://goreportcard.com/badge/github.com/bshuster-repo/logrus-logstash-hook)](https://goreportcard.com/report/github.com/bshuster-repo/logrus-logstash-hook)
+# Logstash hook for logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:" /> [![Build Status](https://travis-ci.org/bshuster-repo/logrus-logstash-hook.svg?branch=master)](https://travis-ci.org/bshuster-repo/logrus-logstash-hook)
+Use this hook to send the logs to [Logstash](https://www.elastic.co/products/logstash) over both UDP and TCP.
 
-Use this hook to send the logs to [Logstash](https://www.elastic.co/products/logstash).
-
-# Usage
+## Usage
 
 ```go
 package main
 
 import (
-        "github.com/bshuster-repo/logrus-logstash-hook"
         "github.com/sirupsen/logrus"
-        "net"
+        "github.com/bshuster-repo/logrus-logstash-hook"
 )
 
 func main() {
         log := logrus.New()
-        conn, err := net.Dial("tcp", "logstash.mycompany.net:8911")
+        hook, err := logrustash.NewHook("tcp", "172.17.0.2:9999", "myappName")
+
         if err != nil {
                 log.Fatal(err)
         }
-        hook := logrustash.New(conn, logrustash.DefaultFormatter(logrus.Fields{"type": "myappName"}))
-
         log.Hooks.Add(hook)
         ctx := log.WithFields(logrus.Fields{
-                "method": "main",
+          "method": "main",
         })
+        ...
         ctx.Info("Hello World!")
 }
-
 ```
 
 This is how it will look like:
@@ -46,8 +41,61 @@ This is how it will look like:
           "type" => "myappName"
 }
 ```
+## Hook Fields
+Fields can be added to the hook, which will always be in the log context.
+This can be done when creating the hook:
+
+```go
+
+hook, err := logrustash.NewHookWithFields("tcp", "172.17.0.2:9999", "myappName", logrus.Fields{
+        "hostname":    os.Hostname(),
+        "serviceName": "myServiceName",
+})
+```
+
+Or afterwards:
+
+```go
+
+hook.WithFields(logrus.Fields{
+        "hostname":    os.Hostname(),
+        "serviceName": "myServiceName",
+})
+```
+This allows you to set up the hook so logging is available immediately, and add important fields as they become available.
+
+Single fields can be added/updated using 'WithField':
+
+```go
+
+hook.WithField("status", "running")
+```
+
+
+
+## Field prefix
+
+The hook allows you to send logging to logstash and also retain the default std output in text format.
+However to keep this console output readable some fields might need to be omitted from the default non-hooked log output.
+Each hook can be configured with a prefix used to identify fields which are only to be logged to the logstash connection.
+For example if you don't want to see the hostname and serviceName on each log line in the console output you can add a prefix:
+
+```go
+
+
+hook, err := logrustash.NewHookWithFields("tcp", "172.17.0.2:9999", "myappName", logrus.Fields{
+        "_hostname":    os.Hostname(),
+        "_serviceName": "myServiceName",
+})
+...
+hook.WithPrefix("_")
+```
+
+There are also constructors available which allow you to specify the prefix from the start.
+The std-out will not have the '\_hostname' and '\_servicename' fields, and the logstash output will, but the prefix will be dropped from the name.
+
 
-# Maintainers
+# Authors
 
 Name         | Github    | Twitter    |
 ------------ | --------- | ---------- |
diff --git a/vendor/github.com/bshuster-repo/logrus-logstash-hook/hook.go b/vendor/github.com/bshuster-repo/logrus-logstash-hook/hook.go
deleted file mode 100644
index cd229a75..00000000
--- a/vendor/github.com/bshuster-repo/logrus-logstash-hook/hook.go
+++ /dev/null
@@ -1,126 +0,0 @@
-package logrustash
-
-import (
-	"io"
-	"sync"
-
-	"github.com/sirupsen/logrus"
-)
-
-// Hook represents a Logstash hook.
-// It has two fields: writer to write the entry to Logstash and
-// formatter to format the entry to a Logstash format before sending.
-//
-// To initialize it use the `New` function.
-//
-type Hook struct {
-	writer    io.Writer
-	formatter logrus.Formatter
-}
-
-// New returns a new logrus.Hook for Logstash.
-//
-// To create a new hook that sends logs to `tcp://logstash.corp.io:9999`:
-//
-// conn, _ := net.Dial("tcp", "logstash.corp.io:9999")
-// hook := logrustash.New(conn, logrustash.DefaultFormatter())
-func New(w io.Writer, f logrus.Formatter) logrus.Hook {
-	return Hook{
-		writer:    w,
-		formatter: f,
-	}
-}
-
-// Fire takes, formats and sends the entry to Logstash.
-// Hook's formatter is used to format the entry into Logstash format
-// and Hook's writer is used to write the formatted entry to the Logstash instance.
-func (h Hook) Fire(e *logrus.Entry) error {
-	dataBytes, err := h.formatter.Format(e)
-	if err != nil {
-		return err
-	}
-	_, err = h.writer.Write(dataBytes)
-	return err
-}
-
-// Levels returns all logrus levels.
-func (h Hook) Levels() []logrus.Level {
-	return logrus.AllLevels
-}
-
-// Using a pool to re-use of old entries when formatting Logstash messages.
-// It is used in the Fire function.
-var entryPool = sync.Pool{
-	New: func() interface{} {
-		return &logrus.Entry{}
-	},
-}
-
-// copyEntry copies the entry `e` to a new entry and then adds all the fields in `fields` that are missing in the new entry data.
-// It uses `entryPool` to re-use allocated entries.
-func copyEntry(e *logrus.Entry, fields logrus.Fields) *logrus.Entry {
-	ne := entryPool.Get().(*logrus.Entry)
-	ne.Message = e.Message
-	ne.Level = e.Level
-	ne.Time = e.Time
-	ne.Data = logrus.Fields{}
-	for k, v := range fields {
-		ne.Data[k] = v
-	}
-	for k, v := range e.Data {
-		ne.Data[k] = v
-	}
-	return ne
-}
-
-// releaseEntry puts the given entry back to `entryPool`. It must be called if copyEntry is called.
-func releaseEntry(e *logrus.Entry) {
-	entryPool.Put(e)
-}
-
-// LogstashFormatter represents a Logstash format.
-// It has logrus.Formatter which formats the entry and logrus.Fields which
-// are added to the JSON message if not given in the entry data.
-//
-// Note: use the `DefaultFormatter` function to set a default Logstash formatter.
-type LogstashFormatter struct {
-	logrus.Formatter
-	logrus.Fields
-}
-
-var (
-	logstashFields   = logrus.Fields{"@version": "1", "type": "log"}
-	logstashFieldMap = logrus.FieldMap{
-		logrus.FieldKeyTime: "@timestamp",
-		logrus.FieldKeyMsg:  "message",
-	}
-)
-
-// DefaultFormatter returns a default Logstash formatter:
-// A JSON format with "@version" set to "1" (unless set differently in `fields`,
-// "type" to "log" (unless set differently in `fields`),
-// "@timestamp" to the log time and "message" to the log message.
-//
-// Note: to set a different configuration use the `LogstashFormatter` structure.
-func DefaultFormatter(fields logrus.Fields) logrus.Formatter {
-	for k, v := range logstashFields {
-		if _, ok := fields[k]; !ok {
-			fields[k] = v
-		}
-	}
-
-	return LogstashFormatter{
-		Formatter: &logrus.JSONFormatter{FieldMap: logstashFieldMap},
-		Fields:    fields,
-	}
-}
-
-// Format formats an entry to a Logstash format according to the given Formatter and Fields.
-//
-// Note: the given entry is copied and not changed during the formatting process.
-func (f LogstashFormatter) Format(e *logrus.Entry) ([]byte, error) {
-	ne := copyEntry(e, f.Fields)
-	dataBytes, err := f.Formatter.Format(ne)
-	releaseEntry(ne)
-	return dataBytes, err
-}
diff --git a/vendor/github.com/bshuster-repo/logrus-logstash-hook/logstash.go b/vendor/github.com/bshuster-repo/logrus-logstash-hook/logstash.go
new file mode 100644
index 00000000..1f2e5a0c
--- /dev/null
+++ b/vendor/github.com/bshuster-repo/logrus-logstash-hook/logstash.go
@@ -0,0 +1,133 @@
+package logrustash
+
+import (
+	"net"
+	"strings"
+
+	"github.com/sirupsen/logrus"
+)
+
+// Hook represents a connection to a Logstash instance
+type Hook struct {
+	conn             net.Conn
+	appName          string
+	alwaysSentFields logrus.Fields
+	hookOnlyPrefix   string
+	TimeFormat       string
+}
+
+// NewHook creates a new hook to a Logstash instance, which listens on
+// `protocol`://`address`.
+func NewHook(protocol, address, appName string) (*Hook, error) {
+	return NewHookWithFields(protocol, address, appName, make(logrus.Fields))
+}
+
+// NewHookWithConn creates a new hook to a Logstash instance, using the supplied connection
+func NewHookWithConn(conn net.Conn, appName string) (*Hook, error) {
+	return NewHookWithFieldsAndConn(conn, appName, make(logrus.Fields))
+}
+
+// NewHookWithFields creates a new hook to a Logstash instance, which listens on
+// `protocol`://`address`. alwaysSentFields will be sent with every log entry.
+func NewHookWithFields(protocol, address, appName string, alwaysSentFields logrus.Fields) (*Hook, error) {
+	return NewHookWithFieldsAndPrefix(protocol, address, appName, alwaysSentFields, "")
+}
+
+// NewHookWithFieldsAndPrefix creates a new hook to a Logstash instance, which listens on
+// `protocol`://`address`. alwaysSentFields will be sent with every log entry. prefix is used to select fields to filter
+func NewHookWithFieldsAndPrefix(protocol, address, appName string, alwaysSentFields logrus.Fields, prefix string) (*Hook, error) {
+	conn, err := net.Dial(protocol, address)
+	if err != nil {
+		return nil, err
+	}
+	return NewHookWithFieldsAndConnAndPrefix(conn, appName, alwaysSentFields, prefix)
+}
+
+// NewHookWithFieldsAndConn creates a new hook to a Logstash instance using the supplied connection
+func NewHookWithFieldsAndConn(conn net.Conn, appName string, alwaysSentFields logrus.Fields) (*Hook, error) {
+	return NewHookWithFieldsAndConnAndPrefix(conn, appName, alwaysSentFields, "")
+}
+
+//NewHookWithFieldsAndConnAndPrefix creates a new hook to a Logstash instance using the suppolied connection and prefix
+func NewHookWithFieldsAndConnAndPrefix(conn net.Conn, appName string, alwaysSentFields logrus.Fields, prefix string) (*Hook, error) {
+	return &Hook{conn: conn, appName: appName, alwaysSentFields: alwaysSentFields, hookOnlyPrefix: prefix}, nil
+}
+
+//NewFilterHook makes a new hook which does not forward to logstash, but simply enforces the prefix rules
+func NewFilterHook() *Hook {
+	return NewFilterHookWithPrefix("")
+}
+
+//NewFilterHookWithPrefix make a new hook which does not forward to logstash, but simply enforces the specified prefix
+func NewFilterHookWithPrefix(prefix string) *Hook {
+	return &Hook{conn: nil, appName: "", alwaysSentFields: make(logrus.Fields), hookOnlyPrefix: prefix}
+}
+
+func (h *Hook) filterHookOnly(entry *logrus.Entry) {
+	if h.hookOnlyPrefix != "" {
+		for key := range entry.Data {
+			if strings.HasPrefix(key, h.hookOnlyPrefix) {
+				delete(entry.Data, key)
+			}
+		}
+	}
+
+}
+
+//WithPrefix sets a prefix filter to use in all subsequent logging
+func (h *Hook) WithPrefix(prefix string) {
+	h.hookOnlyPrefix = prefix
+}
+
+func (h *Hook) WithField(key string, value interface{}) {
+	h.alwaysSentFields[key] = value
+}
+
+func (h *Hook) WithFields(fields logrus.Fields) {
+	//Add all the new fields to the 'alwaysSentFields', possibly overwriting exising fields
+	for key, value := range fields {
+		h.alwaysSentFields[key] = value
+	}
+}
+
+func (h *Hook) Fire(entry *logrus.Entry) error {
+	//make sure we always clear the hookonly fields from the entry
+	defer h.filterHookOnly(entry)
+
+	// Add in the alwaysSentFields. We don't override fields that are already set.
+	for k, v := range h.alwaysSentFields {
+		if _, inMap := entry.Data[k]; !inMap {
+			entry.Data[k] = v
+		}
+	}
+
+	//For a filteringHook, stop here
+	if h.conn == nil {
+		return nil
+	}
+
+	formatter := LogstashFormatter{Type: h.appName}
+	if h.TimeFormat != "" {
+		formatter.TimestampFormat = h.TimeFormat
+	}
+
+	dataBytes, err := formatter.FormatWithPrefix(entry, h.hookOnlyPrefix)
+	if err != nil {
+		return err
+	}
+	if _, err = h.conn.Write(dataBytes); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (h *Hook) Levels() []logrus.Level {
+	return []logrus.Level{
+		logrus.PanicLevel,
+		logrus.FatalLevel,
+		logrus.ErrorLevel,
+		logrus.WarnLevel,
+		logrus.InfoLevel,
+		logrus.DebugLevel,
+	}
+}
diff --git a/vendor/github.com/bshuster-repo/logrus-logstash-hook/logstash_formatter.go b/vendor/github.com/bshuster-repo/logrus-logstash-hook/logstash_formatter.go
new file mode 100644
index 00000000..64bc5c38
--- /dev/null
+++ b/vendor/github.com/bshuster-repo/logrus-logstash-hook/logstash_formatter.go
@@ -0,0 +1,81 @@
+package logrustash
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/sirupsen/logrus"
+)
+
+// Formatter generates json in logstash format.
+// Logstash site: http://logstash.net/
+type LogstashFormatter struct {
+	Type string // if not empty use for logstash type field.
+
+	// TimestampFormat sets the format used for timestamps.
+	TimestampFormat string
+}
+
+func (f *LogstashFormatter) Format(entry *logrus.Entry) ([]byte, error) {
+	return f.FormatWithPrefix(entry, "")
+}
+
+func (f *LogstashFormatter) FormatWithPrefix(entry *logrus.Entry, prefix string) ([]byte, error) {
+	fields := make(logrus.Fields)
+	for k, v := range entry.Data {
+		//remvove the prefix when sending the fields to logstash
+		if prefix != "" && strings.HasPrefix(k, prefix) {
+			k = strings.TrimPrefix(k, prefix)
+		}
+
+		switch v := v.(type) {
+		case error:
+			// Otherwise errors are ignored by `encoding/json`
+			// https://github.com/Sirupsen/logrus/issues/377
+			fields[k] = v.Error()
+		default:
+			fields[k] = v
+		}
+	}
+
+	fields["@version"] = "1"
+
+	timeStampFormat := f.TimestampFormat
+
+	if timeStampFormat == "" {
+		timeStampFormat = time.RFC3339
+	}
+
+	fields["@timestamp"] = entry.Time.Format(timeStampFormat)
+
+	// set message field
+	v, ok := entry.Data["message"]
+	if ok {
+		fields["fields.message"] = v
+	}
+	fields["message"] = entry.Message
+
+	// set level field
+	v, ok = entry.Data["level"]
+	if ok {
+		fields["fields.level"] = v
+	}
+	fields["level"] = entry.Level.String()
+
+	// set type field
+	if f.Type != "" {
+		v, ok = entry.Data["type"]
+		if ok {
+			fields["fields.type"] = v
+		}
+		fields["type"] = f.Type
+	}
+
+	serialized, err := json.Marshal(fields)
+	if err != nil {
+		return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
+	}
+	return append(serialized, '\n'), nil
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 5a4eef2d..fd1138fd 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -1,20 +1,16 @@
 # cloud.google.com/go v0.34.0
 cloud.google.com/go/compute/metadata
 # github.com/Azure/azure-sdk-for-go v16.2.1+incompatible
-## explicit
 github.com/Azure/azure-sdk-for-go/storage
 github.com/Azure/azure-sdk-for-go/version
 # github.com/Azure/go-autorest v10.8.1+incompatible
-## explicit
 github.com/Azure/go-autorest/autorest
 github.com/Azure/go-autorest/autorest/adal
 github.com/Azure/go-autorest/autorest/azure
 github.com/Azure/go-autorest/autorest/date
 # github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d
-## explicit
 github.com/Shopify/logrus-bugsnag
 # github.com/aws/aws-sdk-go v1.34.9
-## explicit
 github.com/aws/aws-sdk-go/aws
 github.com/aws/aws-sdk-go/aws/arn
 github.com/aws/aws-sdk-go/aws/awserr
@@ -61,84 +57,56 @@ github.com/aws/aws-sdk-go/service/sts
 github.com/aws/aws-sdk-go/service/sts/stsiface
 # github.com/beorn7/perks v1.0.1
 github.com/beorn7/perks/quantile
-# github.com/bitly/go-simplejson v0.5.0
-## explicit
-# github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869
-## explicit
-# github.com/bshuster-repo/logrus-logstash-hook v1.0.0
-## explicit
+# github.com/bshuster-repo/logrus-logstash-hook v0.4.1
 github.com/bshuster-repo/logrus-logstash-hook
 # github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd
-## explicit
 github.com/bugsnag/bugsnag-go
 github.com/bugsnag/bugsnag-go/errors
 # github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b
-## explicit
 github.com/bugsnag/osext
 # github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0
-## explicit
 github.com/bugsnag/panicwrap
 # github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba
-## explicit
 github.com/denverdino/aliyungo/cdn/auth
 github.com/denverdino/aliyungo/common
 github.com/denverdino/aliyungo/oss
 github.com/denverdino/aliyungo/util
 # github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c
-## explicit
 github.com/dgrijalva/jwt-go
-# github.com/dnaeon/go-vcr v1.0.1
-## explicit
 # github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c
-## explicit
 github.com/docker/go-events
 # github.com/docker/go-metrics v0.0.1
-## explicit
 github.com/docker/go-metrics
 # github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1
-## explicit
 github.com/docker/libtrust
 # github.com/felixge/httpsnoop v1.0.1
 github.com/felixge/httpsnoop
 # github.com/golang/protobuf v1.3.2
 github.com/golang/protobuf/proto
 # github.com/gomodule/redigo v1.8.2
-## explicit
 github.com/gomodule/redigo/redis
 # github.com/gorilla/handlers v1.5.1
-## explicit
 github.com/gorilla/handlers
 # github.com/gorilla/mux v1.8.0
-## explicit
 github.com/gorilla/mux
 # github.com/inconshreveable/mousetrap v1.0.0
-## explicit
 github.com/inconshreveable/mousetrap
 # github.com/jmespath/go-jmespath v0.3.0
 github.com/jmespath/go-jmespath
 # github.com/konsorten/go-windows-terminal-sequences v1.0.3
 github.com/konsorten/go-windows-terminal-sequences
-# github.com/kr/pretty v0.1.0
-## explicit
 # github.com/marstr/guid v1.1.0
-## explicit
 github.com/marstr/guid
 # github.com/matttproud/golang_protobuf_extensions v1.0.1
 github.com/matttproud/golang_protobuf_extensions/pbutil
 # github.com/mitchellh/mapstructure v1.1.2
-## explicit
 github.com/mitchellh/mapstructure
-# github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f
-## explicit
 # github.com/ncw/swift v1.0.47
-## explicit
 github.com/ncw/swift
 github.com/ncw/swift/swifttest
 # github.com/opencontainers/go-digest v1.0.0
-## explicit
 github.com/opencontainers/go-digest
 # github.com/opencontainers/image-spec v1.0.1
-## explicit
 github.com/opencontainers/image-spec/specs-go
 github.com/opencontainers/image-spec/specs-go/v1
 # github.com/prometheus/client_golang v1.1.0
@@ -155,28 +123,20 @@ github.com/prometheus/common/model
 github.com/prometheus/procfs
 github.com/prometheus/procfs/internal/fs
 # github.com/satori/go.uuid v1.2.0
-## explicit
 github.com/satori/go.uuid
 # github.com/sirupsen/logrus v1.6.0
-## explicit
 github.com/sirupsen/logrus
 # github.com/spf13/cobra v0.0.3
-## explicit
 github.com/spf13/cobra
 # github.com/spf13/pflag v1.0.3
-## explicit
 github.com/spf13/pflag
 # github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43
-## explicit
 github.com/yvasiyarov/go-metrics
 # github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50
-## explicit
 github.com/yvasiyarov/gorelic
 # github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f
-## explicit
 github.com/yvasiyarov/newrelic_platform_go
 # golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d
-## explicit
 golang.org/x/crypto/acme
 golang.org/x/crypto/acme/autocert
 golang.org/x/crypto/bcrypt
@@ -191,7 +151,6 @@ golang.org/x/net/idna
 golang.org/x/net/internal/timeseries
 golang.org/x/net/trace
 # golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
-## explicit
 golang.org/x/oauth2
 golang.org/x/oauth2/google
 golang.org/x/oauth2/internal
@@ -206,7 +165,6 @@ golang.org/x/text/transform
 golang.org/x/text/unicode/bidi
 golang.org/x/text/unicode/norm
 # google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff
-## explicit
 google.golang.org/api/gensupport
 google.golang.org/api/googleapi
 google.golang.org/api/googleapi/internal/uritemplates
@@ -223,13 +181,11 @@ google.golang.org/appengine/internal/remote_api
 google.golang.org/appengine/internal/urlfetch
 google.golang.org/appengine/urlfetch
 # google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8
-## explicit
 google.golang.org/cloud
 google.golang.org/cloud/internal
 google.golang.org/cloud/internal/opts
 google.golang.org/cloud/storage
 # google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a
-## explicit
 google.golang.org/grpc
 google.golang.org/grpc/codes
 google.golang.org/grpc/credentials
@@ -240,8 +196,6 @@ google.golang.org/grpc/naming
 google.golang.org/grpc/peer
 google.golang.org/grpc/transport
 # gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789
-## explicit
 gopkg.in/check.v1
 # gopkg.in/yaml.v2 v2.2.2
-## explicit
 gopkg.in/yaml.v2


Test patch:
diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go
index 977d569a..0d6136e1 100644
--- a/configuration/configuration_test.go
+++ b/configuration/configuration_test.go
@@ -80,11 +80,12 @@ var configStruct = Configuration{
 		RelativeURLs bool          `yaml:"relativeurls,omitempty"`
 		DrainTimeout time.Duration `yaml:"draintimeout,omitempty"`
 		TLS          struct {
-			Certificate string   `yaml:"certificate,omitempty"`
-			Key         string   `yaml:"key,omitempty"`
-			ClientCAs   []string `yaml:"clientcas,omitempty"`
-			MinimumTLS  string   `yaml:"minimumtls,omitempty"`
-			LetsEncrypt struct {
+			Certificate  string   `yaml:"certificate,omitempty"`
+			Key          string   `yaml:"key,omitempty"`
+			ClientCAs    []string `yaml:"clientcas,omitempty"`
+			MinimumTLS   string   `yaml:"minimumtls,omitempty"`
+			CipherSuites []string `yaml:"ciphersuites,omitempty"`
+			LetsEncrypt  struct {
 				CacheFile string   `yaml:"cachefile,omitempty"`
 				Email     string   `yaml:"email,omitempty"`
 				Hosts     []string `yaml:"hosts,omitempty"`
@@ -103,11 +104,12 @@ var configStruct = Configuration{
 		} `yaml:"http2,omitempty"`
 	}{
 		TLS: struct {
-			Certificate string   `yaml:"certificate,omitempty"`
-			Key         string   `yaml:"key,omitempty"`
-			ClientCAs   []string `yaml:"clientcas,omitempty"`
-			MinimumTLS  string   `yaml:"minimumtls,omitempty"`
-			LetsEncrypt struct {
+			Certificate  string   `yaml:"certificate,omitempty"`
+			Key          string   `yaml:"key,omitempty"`
+			ClientCAs    []string `yaml:"clientcas,omitempty"`
+			MinimumTLS   string   `yaml:"minimumtls,omitempty"`
+			CipherSuites []string `yaml:"ciphersuites,omitempty"`
+			LetsEncrypt  struct {
 				CacheFile string   `yaml:"cachefile,omitempty"`
 				Email     string   `yaml:"email,omitempty"`
 				Hosts     []string `yaml:"hosts,omitempty"`
diff --git a/registry/registry_test.go b/registry/registry_test.go
index 0e99c77a..9ff0ddfb 100644
--- a/registry/registry_test.go
+++ b/registry/registry_test.go
@@ -3,12 +3,24 @@ package registry
 import (
 	"bufio"
 	"context"
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/tls"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/pem"
 	"fmt"
 	"io/ioutil"
+	"math/big"
 	"net"
 	"net/http"
 	"os"
+	"path"
 	"reflect"
+	"strings"
 	"testing"
 	"time"
 
@@ -38,18 +50,30 @@ func TestNextProtos(t *testing.T) {
 	}
 }
 
-func setupRegistry() (*Registry, error) {
+type registryTLSConfig struct {
+	cipherSuites    []string
+	certificatePath string
+	privateKeyPath  string
+	certificate     *tls.Certificate
+}
+
+func setupRegistry(tlsCfg *registryTLSConfig, addr string) (*Registry, error) {
 	config := &configuration.Configuration{}
 	// TODO: this needs to change to something ephemeral as the test will fail if there is any server
 	// already listening on port 5000
-	config.HTTP.Addr = ":5000"
+	config.HTTP.Addr = addr
 	config.HTTP.DrainTimeout = time.Duration(10) * time.Second
+	if tlsCfg != nil {
+		config.HTTP.TLS.CipherSuites = tlsCfg.cipherSuites
+		config.HTTP.TLS.Certificate = tlsCfg.certificatePath
+		config.HTTP.TLS.Key = tlsCfg.privateKeyPath
+	}
 	config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}}
 	return NewRegistry(context.Background(), config)
 }
 
 func TestGracefulShutdown(t *testing.T) {
-	registry, err := setupRegistry()
+	registry, err := setupRegistry(nil, ":5000")
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -98,3 +122,227 @@ func TestGracefulShutdown(t *testing.T) {
 		t.Error("Body is not {}; ", string(body))
 	}
 }
+
+func TestGetCipherSuite(t *testing.T) {
+	resp, err := getCipherSuites([]string{"TLS_RSA_WITH_AES_128_CBC_SHA"})
+	if err != nil || len(resp) != 1 || resp[0] != tls.TLS_RSA_WITH_AES_128_CBC_SHA {
+		t.Errorf("expected cipher suite %q, got %q",
+			"TLS_RSA_WITH_AES_128_CBC_SHA",
+			strings.Join(getCipherSuiteNames(resp), ","),
+		)
+	}
+
+	resp, err = getCipherSuites([]string{"TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_AES_128_GCM_SHA256"})
+	if err != nil || len(resp) != 2 ||
+		resp[0] != tls.TLS_RSA_WITH_AES_128_CBC_SHA || resp[1] != tls.TLS_AES_128_GCM_SHA256 {
+		t.Errorf("expected cipher suites %q, got %q",
+			"TLS_RSA_WITH_AES_128_CBC_SHA,TLS_AES_128_GCM_SHA256",
+			strings.Join(getCipherSuiteNames(resp), ","),
+		)
+	}
+
+	_, err = getCipherSuites([]string{"TLS_RSA_WITH_AES_128_CBC_SHA", "bad_input"})
+	if err == nil {
+		t.Error("did not return expected error about unknown cipher suite")
+	}
+}
+
+func buildRegistryTLSConfig(name, keyType string, cipherSuites []string) (*registryTLSConfig, error) {
+	var priv interface{}
+	var pub crypto.PublicKey
+	var err error
+	switch keyType {
+	case "rsa":
+		priv, err = rsa.GenerateKey(rand.Reader, 2048)
+		if err != nil {
+			return nil, fmt.Errorf("failed to create rsa private key: %v", err)
+		}
+		rsaKey := priv.(*rsa.PrivateKey)
+		pub = rsaKey.Public()
+	case "ecdsa":
+		priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
+		if err != nil {
+			return nil, fmt.Errorf("failed to create ecdsa private key: %v", err)
+		}
+		ecdsaKey := priv.(*ecdsa.PrivateKey)
+		pub = ecdsaKey.Public()
+	default:
+		return nil, fmt.Errorf("unsupported key type: %v", keyType)
+	}
+
+	notBefore := time.Now()
+	notAfter := notBefore.Add(time.Minute)
+	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create serial number: %v", err)
+	}
+	cert := x509.Certificate{
+		SerialNumber: serialNumber,
+		Subject: pkix.Name{
+			Organization: []string{"registry_test"},
+		},
+		NotBefore:             notBefore,
+		NotAfter:              notAfter,
+		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+		BasicConstraintsValid: true,
+		IPAddresses:           []net.IP{net.ParseIP("127.0.0.1")},
+		DNSNames:              []string{"localhost"},
+		IsCA:                  true,
+	}
+	derBytes, err := x509.CreateCertificate(rand.Reader, &cert, &cert, pub, priv)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create certificate: %v", err)
+	}
+	if _, err := os.Stat(os.TempDir()); os.IsNotExist(err) {
+		os.Mkdir(os.TempDir(), 1777)
+	}
+
+	certPath := path.Join(os.TempDir(), name+".pem")
+	certOut, err := os.Create(certPath)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create pem: %v", err)
+	}
+	if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
+		return nil, fmt.Errorf("failed to write data to %s: %v", certPath, err)
+	}
+	if err := certOut.Close(); err != nil {
+		return nil, fmt.Errorf("error closing %s: %v", certPath, err)
+	}
+
+	keyPath := path.Join(os.TempDir(), name+".key")
+	keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
+	if err != nil {
+		return nil, fmt.Errorf("failed to open %s for writing: %v", keyPath, err)
+	}
+	privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
+	if err != nil {
+		return nil, fmt.Errorf("unable to marshal private key: %v", err)
+	}
+	if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
+		return nil, fmt.Errorf("failed to write data to key.pem: %v", err)
+	}
+	if err := keyOut.Close(); err != nil {
+		return nil, fmt.Errorf("error closing %s: %v", keyPath, err)
+	}
+
+	tlsCert := tls.Certificate{
+		Certificate: [][]byte{derBytes},
+		PrivateKey:  priv,
+	}
+
+	tlsTestCfg := registryTLSConfig{
+		cipherSuites:    cipherSuites,
+		certificatePath: certPath,
+		privateKeyPath:  keyPath,
+		certificate:     &tlsCert,
+	}
+
+	return &tlsTestCfg, nil
+}
+
+func TestRegistrySupportedCipherSuite(t *testing.T) {
+	name := "registry_test_server_supported_cipher"
+	cipherSuites := []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"}
+	serverTLS, err := buildRegistryTLSConfig(name, "rsa", cipherSuites)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	registry, err := setupRegistry(serverTLS, ":5001")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// run registry server
+	var errchan chan error
+	go func() {
+		errchan <- registry.ListenAndServe()
+	}()
+	select {
+	case err = <-errchan:
+		t.Fatalf("Error listening: %v", err)
+	default:
+	}
+
+	// Wait for some unknown random time for server to start listening
+	time.Sleep(3 * time.Second)
+
+	// send tls request with server supported cipher suite
+	clientCipherSuites, err := getCipherSuites(cipherSuites)
+	if err != nil {
+		t.Fatal(err)
+	}
+	clientTLS := tls.Config{
+		InsecureSkipVerify: true,
+		CipherSuites:       clientCipherSuites,
+	}
+	dialer := net.Dialer{
+		Timeout: time.Second * 5,
+	}
+	conn, err := tls.DialWithDialer(&dialer, "tcp", "127.0.0.1:5001", &clientTLS)
+	if err != nil {
+		t.Fatal(err)
+	}
+	fmt.Fprintf(conn, "GET /v2/ HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n")
+
+	resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if resp.Status != "200 OK" {
+		t.Error("response status is not 200 OK: ", resp.Status)
+	}
+	if body, err := ioutil.ReadAll(resp.Body); err != nil || string(body) != "{}" {
+		t.Error("Body is not {}; ", string(body))
+	}
+
+	// send stop signal
+	quit <- os.Interrupt
+	time.Sleep(100 * time.Millisecond)
+}
+
+func TestRegistryUnsupportedCipherSuite(t *testing.T) {
+	name := "registry_test_server_unsupported_cipher"
+	serverTLS, err := buildRegistryTLSConfig(name, "rsa", []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA358"})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	registry, err := setupRegistry(serverTLS, ":5002")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// run registry server
+	var errchan chan error
+	go func() {
+		errchan <- registry.ListenAndServe()
+	}()
+	select {
+	case err = <-errchan:
+		t.Fatalf("Error listening: %v", err)
+	default:
+	}
+
+	// Wait for some unknown random time for server to start listening
+	time.Sleep(3 * time.Second)
+
+	// send tls request with server unsupported cipher suite
+	clientTLS := tls.Config{
+		InsecureSkipVerify: true,
+		CipherSuites:       []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
+	}
+	dialer := net.Dialer{
+		Timeout: time.Second * 5,
+	}
+	_, err = tls.DialWithDialer(&dialer, "tcp", "127.0.0.1:5002", &clientTLS)
+	if err == nil {
+		t.Error("expected TLS connection to timeout")
+	}
+
+	// send stop signal
+	quit <- os.Interrupt
+	time.Sleep(100 * time.Millisecond)
+}