diff --git a/config/config.go b/config/config.go index 697fa3c6631..1e12785d323 100644 --- a/config/config.go +++ b/config/config.go @@ -60,6 +60,8 @@ type Configuration struct { BlacklistedAcctMap map[string]bool // Is publisher/account ID required to be submitted in the OpenRTB2 request AccountRequired bool `mapstructure:"account_required"` + // Local private file containing SSL certificates + PemCertsFile string `mapstructure:"certificates_file"` } const MIN_COOKIE_SIZE_BYTES = 500 @@ -704,6 +706,7 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("blacklisted_apps", []string{""}) v.SetDefault("blacklisted_accts", []string{""}) v.SetDefault("account_required", false) + v.SetDefault("certificates_file", "") // Set environment variable support: v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) diff --git a/config/config_test.go b/config/config_test.go index 17c8c5e3f6b..14eda7496a7 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -32,6 +32,7 @@ func TestDefaults(t *testing.T) { cmpBools(t, "account_required", cfg.AccountRequired, false) cmpInts(t, "metrics.influxdb.collection_rate_seconds", cfg.Metrics.Influxdb.MetricSendInterval, 20) cmpBools(t, "account_adapter_details", cfg.Metrics.Disabled.AccountAdapterDetails, false) + cmpStrings(t, "certificates_file", cfg.PemCertsFile, "") } var fullConfig = []byte(` @@ -105,6 +106,7 @@ adapters: usersync_url: https://tag.adkernel.com/syncr?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&r= blacklisted_apps: ["spamAppID","sketchy-app-id"] account_required: true +certificates_file: /etc/ssl/cert.pem `) var adapterExtraInfoConfig = []byte(` @@ -266,6 +268,7 @@ func TestFullConfig(t *testing.T) { cmpStrings(t, "adapters.rhythmone.usersync_url", cfg.Adapters[string(openrtb_ext.BidderRhythmone)].UserSyncURL, "https://sync.1rx.io/usersync2/rmphb?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&redir=http%3A%2F%2Fprebid-server.prebid.org%2F%2Fsetuid%3Fbidder%3Drhythmone%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5BRX_UUID%5D") cmpBools(t, "account_required", cfg.AccountRequired, true) cmpBools(t, "account_adapter_details", cfg.Metrics.Disabled.AccountAdapterDetails, true) + cmpStrings(t, "certificates_file", cfg.PemCertsFile, "/etc/ssl/cert.pem") } func TestUnmarshalAdapterExtraInfo(t *testing.T) { diff --git a/router/router.go b/router/router.go index 15478f7b38a..854b7fcdb94 100644 --- a/router/router.go +++ b/router/router.go @@ -175,12 +175,22 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r r = &Router{ Router: httprouter.New(), } + + // For bid processing, we need both the hardcoded certificates and the certificates found in container's + // local file system + certPool := ssl.GetRootCAPool() + var readCertErr error + certPool, readCertErr = ssl.AppendPEMFileToRootCAPool(certPool, cfg.PemCertsFile) + if readCertErr != nil { + glog.Infof("Could not read certificates file: %s \n", readCertErr.Error()) + } + theClient := &http.Client{ Transport: &http.Transport{ MaxIdleConns: cfg.Client.MaxIdleConns, MaxIdleConnsPerHost: cfg.Client.MaxIdleConnsPerHost, IdleConnTimeout: time.Duration(cfg.Client.IdleConnTimeout) * time.Second, - TLSClientConfig: &tls.Config{RootCAs: ssl.GetRootCAPool()}, + TLSClientConfig: &tls.Config{RootCAs: certPool}, }, } // Hack because of how legacy handles districtm diff --git a/ssl/mockcertificates/mock-certs.pem b/ssl/mockcertificates/mock-certs.pem new file mode 100644 index 00000000000..e0bf7db58ff --- /dev/null +++ b/ssl/mockcertificates/mock-certs.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBhTCCASugAwIBAgIQIRi6zePL6mKjOipn+dNuaTAKBggqhkjOPQQDAjASMRAw +DgYDVQQKEwdBY21lIENvMB4XDTE3MTAyMDE5NDMwNloXDTE4MTAyMDE5NDMwNlow +EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD0d +7VNhbWvZLWPuj/RtHFjvtJBEwOkhbN/BnnE8rnZR8+sbwnc/KhCk3FhnpHZnQz7B +5aETbbIgmuvewdjvSBSjYzBhMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggr +BgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1UdEQQiMCCCDmxvY2FsaG9zdDo1 +NDUzgg4xMjcuMC4wLjE6NTQ1MzAKBggqhkjOPQQDAgNIADBFAiEA2zpJEPQyz6/l +Wf86aX6PepsntZv2GYlA5UpabfT2EZICICpJ5h/iI+i341gBmLiAFQOyTDT+/wQc +6MF9+Yw1Yy0t +-----END CERTIFICATE----- diff --git a/ssl/ssl.go b/ssl/ssl.go index bafd91613c7..d05c90154b9 100644 --- a/ssl/ssl.go +++ b/ssl/ssl.go @@ -2,6 +2,8 @@ package ssl import ( "crypto/x509" + "fmt" + "io/ioutil" ) // from https://medium.com/@kelseyhightower/optimizing-docker-images-for-static-binaries-b5696e26eb07 @@ -16,6 +18,26 @@ func GetRootCAPool() *x509.CertPool { return pool } +// Appends certificates to the `x509.CertPool` from a `.pem` private local file. On many Linux +// systems, /etc/ssl/cert.pem will contain the system wide set but in our case, we'll pull +// the certificate file path from the `Configuration` struct +func AppendPEMFileToRootCAPool(certPool *x509.CertPool, pemFileName string) (*x509.CertPool, error) { + if certPool == nil { + certPool = x509.NewCertPool() + } + if pemFileName != "" { + //read file and place it's contents in `pemCerts` + pemCerts, err := ioutil.ReadFile(pemFileName) + if err != nil { + return certPool, fmt.Errorf("Failed to read file %s: %v", pemFileName, err) + } + + //`pemCerts` has been obtained, append to certPool + certPool.AppendCertsFromPEM(pemCerts) + } + return certPool, nil +} + var pemCerts = []byte(` -----BEGIN CERTIFICATE----- MIIDzzCCAregAwIBAgIDAWweMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYDVQQGEwJB diff --git a/ssl/ssl_test.go b/ssl/ssl_test.go new file mode 100644 index 00000000000..c4c29d149ef --- /dev/null +++ b/ssl/ssl_test.go @@ -0,0 +1,54 @@ +package ssl + +import ( + "crypto/x509" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCertsFromFilePoolExists(t *testing.T) { + // Load hardcoded certificates found in ssl.go + certPool := GetRootCAPool() + + // Assert loaded certificates by looking at the length of the subjects array of strings + subjects := certPool.Subjects() + hardCodedSubNum := len(subjects) + assert.True(t, hardCodedSubNum > 0) + + // Load certificates from file + certificatesFile := "mockcertificates/mock-certs.pem" + certPool, err := AppendPEMFileToRootCAPool(certPool, certificatesFile) + + // Assert loaded certificates by looking at the length of the subjects array of strings + assert.NoError(t, err, "Error thrown by AppendPEMFileToRootCAPool while loading file %s: %v", certificatesFile, err) + subjects = certPool.Subjects() + subNumIncludingFile := len(subjects) + assert.True(t, subNumIncludingFile > hardCodedSubNum, "subNumIncludingFile should be greater than hardCodedSubNum") +} + +func TestCertsFromFilePoolDontExist(t *testing.T) { + // Empty certpool + var certPool *x509.CertPool = nil + + // Load certificates from file + certificatesFile := "mockcertificates/mock-certs.pem" + certPool, err := AppendPEMFileToRootCAPool(certPool, certificatesFile) + + // Assert loaded certificates by looking at the length of the subjects array of strings + assert.NoError(t, err, "Error thrown by AppendPEMFileToRootCAPool while loading file %s: %v", certificatesFile, err) + subjects := certPool.Subjects() + assert.Equal(t, len(subjects), 1, "We only loaded one vertificate from the file, len(subjects) should equal 1") +} + +func TestAppendPEMFileToRootCAPoolFail(t *testing.T) { + // Empty certpool + var certPool *x509.CertPool + + // In this test we are going to pass a file that does not exist as value of second argument + fakeCertificatesFile := "mockcertificates/NO-FILE.pem" + certPool, err := AppendPEMFileToRootCAPool(certPool, fakeCertificatesFile) + + // Assert AppendPEMFileToRootCAPool correctly throws an error when trying to load an nonexisting file + assert.Errorf(t, err, "AppendPEMFileToRootCAPool should throw an error by while loading fake file %s \n", fakeCertificatesFile) +}