From 10d23a0393b12efc230435b3cc1125f2ff1cfb02 Mon Sep 17 00:00:00 2001
From: Stephen Nelson <stephen@sfnelson.org>
Date: Sat, 7 May 2016 23:28:12 +1200
Subject: [PATCH] Add support for TLS client authentication

Allow gitlab-ci to connect to a gitlab host using TLS client authentication
(mutual authentication). Adds configuration and support for using TLS client
certificates when using go's TLS transport layer and also sets git enviromental
variables for runners.
---
 common/build.go                              |  25 ++++
 common/config.go                             |  16 +-
 common/network.go                            |  22 ++-
 docs/configuration/advanced-configuration.md |   2 +
 network/client.go                            |  96 +++++++++---
 network/client_test.go                       | 148 ++++++++++++++++++-
 network/gitlab.go                            |  28 +++-
 network/gitlab_test.go                       |  20 ++-
 shells/abstract.go                           |  18 +--
 9 files changed, 323 insertions(+), 52 deletions(-)

diff --git a/common/build.go b/common/build.go
index 2f289c5c82c..02baaa4193d 100644
--- a/common/build.go
+++ b/common/build.go
@@ -382,11 +382,36 @@ func (b *Build) GetDefaultVariables() JobVariables {
 	}
 }
 
+func (b *Build) GetCITLSVariables() JobVariables {
+	variables := JobVariables{}
+	if b.TLSCAChain != "" {
+		variables = append(variables, JobVariable{"CI_SERVER_TLS_CA_FILE", b.TLSCAChain, true, true, true})
+	}
+	if b.TLSAuthCert != "" && b.TLSAuthKey != "" {
+		variables = append(variables, JobVariable{"CI_SERVER_TLS_CERT_FILE", b.TLSAuthCert, true, true, true})
+		variables = append(variables, JobVariable{"CI_SERVER_TLS_KEY_FILE", b.TLSAuthKey, true, true, true})
+	}
+	return variables
+}
+
+func (b *Build) GetGitTLSVariables() JobVariables {
+	variables := JobVariables{}
+	if b.TLSCAChain != "" {
+		variables = append(variables, JobVariable{"GIT_SSL_CAINFO", b.TLSCAChain, true, true, true})
+	}
+	if b.TLSAuthCert != "" && b.TLSAuthKey != "" {
+		variables = append(variables, JobVariable{"GIT_SSL_CERT", b.TLSAuthCert, true, true, true})
+		variables = append(variables, JobVariable{"GIT_SSL_KEY", b.TLSAuthKey, true, true, true})
+	}
+	return variables
+}
+
 func (b *Build) GetAllVariables() (variables JobVariables) {
 	if b.Runner != nil {
 		variables = append(variables, b.Runner.GetVariables()...)
 	}
 	variables = append(variables, b.GetDefaultVariables()...)
+	variables = append(variables, b.GetCITLSVariables()...)
 	variables = append(variables, b.Variables...)
 	return variables.Expand()
 }
diff --git a/common/config.go b/common/config.go
index c8034080236..5c576be6146 100644
--- a/common/config.go
+++ b/common/config.go
@@ -149,9 +149,11 @@ type KubernetesConfig struct {
 }
 
 type RunnerCredentials struct {
-	URL       string `toml:"url" json:"url" short:"u" long:"url" env:"CI_SERVER_URL" required:"true" description:"Runner URL"`
-	Token     string `toml:"token" json:"token" short:"t" long:"token" env:"CI_SERVER_TOKEN" required:"true" description:"Runner token"`
-	TLSCAFile string `toml:"tls-ca-file,omitempty" json:"tls-ca-file" long:"tls-ca-file" env:"CI_SERVER_TLS_CA_FILE" description:"File containing the certificates to verify the peer when using HTTPS"`
+	URL         string `toml:"url" json:"url" short:"u" long:"url" env:"CI_SERVER_URL" required:"true" description:"Runner URL"`
+	Token       string `toml:"token" json:"token" short:"t" long:"token" env:"CI_SERVER_TOKEN" required:"true" description:"Runner token"`
+	TLSCAFile   string `toml:"tls-ca-file,omitempty" json:"tls-ca-file" long:"tls-ca-file" env:"CI_SERVER_TLS_CA_FILE" description:"File containing the certificates to verify the peer when using HTTPS"`
+	TLSCertFile string `toml:"tls-cert-file,omitempty" json:"tls-cert-file" long:"tls-cert-file" env:"CI_SERVER_TLS_CERT_FILE" description:"File containing certificate for TLS client auth when using HTTPS"`
+	TLSKeyFile  string `toml:"tls-key-file,omitempty" json:"tls-key-file" long:"tls-key-file" env:"CI_SERVER_TLS_KEY_FILE" description:"File containing private key for TLS client auth when using HTTPS"`
 }
 
 type CacheConfig struct {
@@ -279,6 +281,14 @@ func (c *RunnerCredentials) GetTLSCAFile() string {
 	return c.TLSCAFile
 }
 
+func (c *RunnerCredentials) GetTLSCertFile() string {
+	return c.TLSCertFile
+}
+
+func (c *RunnerCredentials) GetTLSKeyFile() string {
+	return c.TLSKeyFile
+}
+
 func (c *RunnerCredentials) GetToken() string {
 	return c.Token
 }
diff --git a/common/network.go b/common/network.go
index 17172068f4d..d37f48a52ef 100644
--- a/common/network.go
+++ b/common/network.go
@@ -209,7 +209,9 @@ type JobResponse struct {
 	Credentials   []Credentials `json:"credentials"`
 	Dependencies  Dependencies  `json:"dependencies"`
 
-	TLSCAChain string `json:"-"`
+	TLSCAChain  string `json:"-"`
+	TLSAuthCert string `json:"-"`
+	TLSAuthKey  string `json:"-"`
 }
 
 func (j *JobResponse) RepoCleanURL() string {
@@ -224,10 +226,12 @@ type UpdateJobRequest struct {
 }
 
 type JobCredentials struct {
-	ID        int    `long:"id" env:"CI_JOB_ID" description:"The build ID to upload artifacts for"`
-	Token     string `long:"token" env:"CI_JOB_TOKEN" required:"true" description:"Build token"`
-	URL       string `long:"url" env:"CI_SERVER_URL" required:"true" description:"GitLab CI URL"`
-	TLSCAFile string `long:"tls-ca-file" env:"CI_SERVER_TLS_CA_FILE" description:"File containing the certificates to verify the peer when using HTTPS"`
+	ID          int    `long:"id" env:"CI_JOB_ID" description:"The build ID to upload artifacts for"`
+	Token       string `long:"token" env:"CI_JOB_TOKEN" required:"true" description:"Build token"`
+	URL         string `long:"url" env:"CI_SERVER_URL" required:"true" description:"GitLab CI URL"`
+	TLSCAFile   string `long:"tls-ca-file" env:"CI_SERVER_TLS_CA_FILE" description:"File containing the certificates to verify the peer when using HTTPS"`
+	TLSCertFile string `long:"tls-cert-file" env:"CI_SERVER_TLS_CERT_FILE" description:"File containing certificate for TLS client auth with runner when using HTTPS"`
+	TLSKeyFile  string `long:"tls-key-file" env:"CI_SERVER_TLS_KEY_FILE" description:"File containing private key for TLS client auth with runner when using HTTPS"`
 }
 
 func (j *JobCredentials) GetURL() string {
@@ -238,6 +242,14 @@ func (j *JobCredentials) GetTLSCAFile() string {
 	return j.TLSCAFile
 }
 
+func (j *JobCredentials) GetTLSCertFile() string {
+	return j.TLSCertFile
+}
+
+func (j *JobCredentials) GetTLSKeyFile() string {
+	return j.TLSKeyFile
+}
+
 func (j *JobCredentials) GetToken() string {
 	return j.Token
 }
diff --git a/docs/configuration/advanced-configuration.md b/docs/configuration/advanced-configuration.md
index 2a559ce8ccb..89f69358f31 100644
--- a/docs/configuration/advanced-configuration.md
+++ b/docs/configuration/advanced-configuration.md
@@ -39,6 +39,8 @@ This defines one runner entry.
 | `url`                | CI URL |
 | `token`              | runner token |
 | `tls-ca-file`        | file containing the certificates to verify the peer when using HTTPS |
+| `tls-cert-file`      | file containing the certificate to authenticate with the peer when using HTTPS |
+| `tls-key-file`       | file containing the private key to authenticate with the peer when using HTTPS |
 | `tls-skip-verify`    | whether to verify the TLS certificate when using HTTPS, default: false |
 | `limit`              | limit how many jobs can be handled concurrently by this token. `0` (default) simply means don't limit |
 | `executor`           | select how a project should be built, see next section |
diff --git a/network/client.go b/network/client.go
index 99f02c55e57..8e3a2dfd96e 100644
--- a/network/client.go
+++ b/network/client.go
@@ -29,6 +29,8 @@ type requestCredentials interface {
 	GetURL() string
 	GetToken() string
 	GetTLSCAFile() string
+	GetTLSCertFile() string
+	GetTLSKeyFile() string
 }
 
 var dialer = net.Dialer{
@@ -40,6 +42,8 @@ type client struct {
 	http.Client
 	url                  *url.URL
 	caFile               string
+	certFile             string
+	keyFile              string
 	caData               []byte
 	skipVerify           bool
 	updateTime           time.Time
@@ -47,6 +51,12 @@ type client struct {
 	compatibleWithGitLab bool
 }
 
+type ResponseTLSData struct {
+	CAChain  string
+	CertFile string
+	KeyFile  string
+}
+
 func (n *client) getLastUpdate() string {
 	return n.lastUpdate
 }
@@ -63,6 +73,16 @@ func (n *client) ensureTLSConfig() {
 		n.Transport = nil
 	}
 
+	// client certificate got modified
+	if stat, err := os.Stat(n.certFile); err == nil && n.updateTime.Before(stat.ModTime()) {
+		n.Transport = nil
+	}
+
+	// client private key got modified
+	if stat, err := os.Stat(n.keyFile); err == nil && n.updateTime.Before(stat.ModTime()) {
+		n.Transport = nil
+	}
+
 	// create or update transport
 	if n.Transport == nil {
 		n.updateTime = time.Now()
@@ -70,14 +90,8 @@ func (n *client) ensureTLSConfig() {
 	}
 }
 
-func (n *client) createTransport() {
-	// create reference TLS config
-	tlsConfig := tls.Config{
-		MinVersion:         tls.VersionTLS10,
-		InsecureSkipVerify: n.skipVerify,
-	}
-
-	// load TLS certificate
+func (n *client) addTLSCA(tlsConfig *tls.Config) {
+	// load TLS CA certificate
 	if file := n.caFile; file != "" && !n.skipVerify {
 		logrus.Debugln("Trying to load", file, "...")
 
@@ -96,6 +110,34 @@ func (n *client) createTransport() {
 			}
 		}
 	}
+}
+
+func (n *client) addTLSAuth(tlsConfig *tls.Config) {
+	// load TLS client keypair
+	if cert, key := n.certFile, n.keyFile; cert != "" && key != "" {
+		logrus.Debugln("Trying to load", cert, "and", key, "pair...")
+
+		certificate, err := tls.LoadX509KeyPair(cert, key)
+		if err == nil {
+			tlsConfig.Certificates = []tls.Certificate{certificate}
+			tlsConfig.BuildNameToCertificate()
+		} else {
+			if !os.IsNotExist(err) {
+				logrus.Errorln("Failed to load", cert, key, err)
+			}
+		}
+	}
+}
+
+func (n *client) createTransport() {
+	// create reference TLS config
+	tlsConfig := tls.Config{
+		MinVersion:         tls.VersionTLS10,
+		InsecureSkipVerify: n.skipVerify,
+	}
+
+	n.addTLSCA(&tlsConfig)
+	n.addTLSAuth(&tlsConfig)
 
 	// create transport
 	n.Transport = &http.Transport{
@@ -175,13 +217,13 @@ func (n *client) do(uri, method string, request io.Reader, requestType string, h
 	return
 }
 
-func (n *client) doJSON(uri, method string, statusCode int, request interface{}, response interface{}) (int, string, string) {
+func (n *client) doJSON(uri, method string, statusCode int, request interface{}, response interface{}) (int, string, ResponseTLSData) {
 	var body io.Reader
 
 	if request != nil {
 		requestBody, err := json.Marshal(request)
 		if err != nil {
-			return -1, fmt.Sprintf("failed to marshal project object: %v", err), ""
+			return -1, fmt.Sprintf("failed to marshal project object: %v", err), ResponseTLSData{}
 		}
 		body = bytes.NewReader(requestBody)
 	}
@@ -193,7 +235,7 @@ func (n *client) doJSON(uri, method string, statusCode int, request interface{},
 
 	res, err := n.do(uri, method, body, "application/json", headers)
 	if err != nil {
-		return -1, err.Error(), ""
+		return -1, err.Error(), ResponseTLSData{}
 	}
 	defer res.Body.Close()
 	defer io.Copy(ioutil.Discard, res.Body)
@@ -202,20 +244,26 @@ func (n *client) doJSON(uri, method string, statusCode int, request interface{},
 		if response != nil {
 			isApplicationJSON, err := isResponseApplicationJSON(res)
 			if !isApplicationJSON {
-				return -1, err.Error(), ""
+				return -1, err.Error(), ResponseTLSData{}
 			}
 
 			d := json.NewDecoder(res.Body)
 			err = d.Decode(response)
 			if err != nil {
-				return -1, fmt.Sprintf("Error decoding json payload %v", err), ""
+				return -1, fmt.Sprintf("Error decoding json payload %v", err), ResponseTLSData{}
 			}
 		}
 	}
 
 	n.setLastUpdate(res.Header)
 
-	return res.StatusCode, res.Status, n.getCAChain(res.TLS)
+	TLSData := ResponseTLSData{
+		CAChain:  n.getCAChain(res.TLS),
+		CertFile: n.certFile,
+		KeyFile:  n.keyFile,
+	}
+
+	return res.StatusCode, res.Status, TLSData
 }
 
 func isResponseApplicationJSON(res *http.Response) (result bool, err error) {
@@ -241,6 +289,16 @@ func fixCIURL(url string) string {
 	return url
 }
 
+func (n *client) findCertificate(certificate *string, base string, name string) {
+	if *certificate != "" {
+		return
+	}
+	path := filepath.Join(base, name)
+	if _, err := os.Stat(path); err == nil {
+		*certificate = path
+	}
+}
+
 func newClient(requestCredentials requestCredentials) (c *client, err error) {
 	url, err := url.Parse(fixCIURL(requestCredentials.GetURL()) + "/api/v4/")
 	if err != nil {
@@ -255,12 +313,16 @@ func newClient(requestCredentials requestCredentials) (c *client, err error) {
 	c = &client{
 		url:                  url,
 		caFile:               requestCredentials.GetTLSCAFile(),
+		certFile:             requestCredentials.GetTLSCertFile(),
+		keyFile:              requestCredentials.GetTLSKeyFile(),
 		compatibleWithGitLab: true,
 	}
 
-	if CertificateDirectory != "" && c.caFile == "" {
-		hostAndPort := strings.Split(url.Host, ":")
-		c.caFile = filepath.Join(CertificateDirectory, hostAndPort[0]+".crt")
+	host := strings.Split(url.Host, ":")[0]
+	if CertificateDirectory != "" {
+		c.findCertificate(&c.caFile, CertificateDirectory, host+".crt")
+		c.findCertificate(&c.certFile, CertificateDirectory, host+".auth.crt")
+		c.findCertificate(&c.keyFile, CertificateDirectory, host+".auth.key")
 	}
 
 	return
diff --git a/network/client_test.go b/network/client_test.go
index 4f680770bdf..2ae168872e8 100644
--- a/network/client_test.go
+++ b/network/client_test.go
@@ -1,18 +1,24 @@
 package network
 
 import (
+	"crypto/rsa"
+	"crypto/tls"
+	"crypto/x509"
 	"encoding/pem"
 	"errors"
 	"fmt"
 	"io/ioutil"
+	"net"
 	"net/http"
 	"net/http/httptest"
+	"net/url"
 	"os"
 	"path/filepath"
 	"testing"
 
 	"github.com/Sirupsen/logrus"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 
 	. "gitlab.com/gitlab-org/gitlab-ci-multi-runner/common"
 )
@@ -56,6 +62,33 @@ func writeTLSCertificate(s *httptest.Server, file string) error {
 	return ioutil.WriteFile(file, encoded, 0600)
 }
 
+func writeTLSKeyPair(s *httptest.Server, certFile string, keyFile string) error {
+	c := s.TLS.Certificates[0]
+	if c.Certificate == nil || c.Certificate[0] == nil {
+		return errors.New("no predefined certificate")
+	}
+
+	encodedCert := pem.EncodeToMemory(&pem.Block{
+		Type:  "CERTIFICATE",
+		Bytes: c.Certificate[0],
+	})
+
+	if err := ioutil.WriteFile(certFile, encodedCert, 0600); err != nil {
+		return err
+	}
+
+	switch k := c.PrivateKey.(type) {
+	case *rsa.PrivateKey:
+		encodedKey := pem.EncodeToMemory(&pem.Block{
+			Type:  "RSA PRIVATE KEY",
+			Bytes: x509.MarshalPKCS1PrivateKey(k),
+		})
+		return ioutil.WriteFile(keyFile, encodedKey, 0600)
+	default:
+		return errors.New("unexpected private key type")
+	}
+}
+
 func TestNewClient(t *testing.T) {
 	c, err := newClient(&RunnerCredentials{
 		URL: "http://test.example.com/ci///",
@@ -137,29 +170,134 @@ func TestClientTLSCAFile(t *testing.T) {
 		URL:       s.URL,
 		TLSCAFile: file.Name(),
 	})
-	statusCode, statusText, certificates := c.doJSON("test/ok", "GET", 200, nil, nil)
+	statusCode, statusText, tlsData := c.doJSON("test/ok", "GET", 200, nil, nil)
 	assert.Equal(t, 200, statusCode, statusText)
-	assert.NotEmpty(t, certificates)
+	assert.NotEmpty(t, tlsData.CAChain)
 }
 
 func TestClientCertificateInPredefinedDirectory(t *testing.T) {
 	s := httptest.NewTLSServer(http.HandlerFunc(clientHandler))
 	defer s.Close()
 
+	serverURL, err := url.Parse(s.URL)
+	require.NoError(t, err)
+	hostname, _, err := net.SplitHostPort(serverURL.Host)
+	require.NoError(t, err)
+
+	tempDir, err := ioutil.TempDir("", "certs")
+	assert.NoError(t, err)
+	defer os.RemoveAll(tempDir)
+	CertificateDirectory = tempDir
+
+	err = writeTLSCertificate(s, filepath.Join(tempDir, hostname+".crt"))
+	assert.NoError(t, err)
+
+	c, _ := newClient(&RunnerCredentials{
+		URL: s.URL,
+	})
+	statusCode, statusText, tlsData := c.doJSON("test/ok", "GET", 200, nil, nil)
+	assert.Equal(t, 200, statusCode, statusText)
+	assert.NotEmpty(t, tlsData.CAChain)
+}
+
+func TestClientInvalidTLSAuth(t *testing.T) {
+	s := httptest.NewUnstartedServer(http.HandlerFunc(clientHandler))
+	s.TLS = new(tls.Config)
+	s.TLS.ClientAuth = tls.RequireAnyClientCert
+	s.StartTLS()
+	defer s.Close()
+
+	ca, err := ioutil.TempFile("", "cert_")
+	assert.NoError(t, err)
+	ca.Close()
+	defer os.Remove(ca.Name())
+
+	err = writeTLSCertificate(s, ca.Name())
+	assert.NoError(t, err)
+
+	c, _ := newClient(&RunnerCredentials{
+		URL:       s.URL,
+		TLSCAFile: ca.Name(),
+	})
+	statusCode, statusText, _ := c.doJSON("test/ok", "GET", 200, nil, nil)
+	assert.Equal(t, -1, statusCode, statusText)
+	assert.Contains(t, statusText, "tls: bad certificate")
+}
+
+func TestClientTLSAuth(t *testing.T) {
+	s := httptest.NewUnstartedServer(http.HandlerFunc(clientHandler))
+	s.TLS = new(tls.Config)
+	s.TLS.ClientAuth = tls.RequireAnyClientCert
+	s.StartTLS()
+	defer s.Close()
+
+	ca, err := ioutil.TempFile("", "cert_")
+	assert.NoError(t, err)
+	ca.Close()
+	defer os.Remove(ca.Name())
+
+	err = writeTLSCertificate(s, ca.Name())
+	assert.NoError(t, err)
+
+	cert, err := ioutil.TempFile("", "cert_")
+	assert.NoError(t, err)
+	cert.Close()
+	defer os.Remove(cert.Name())
+
+	key, err := ioutil.TempFile("", "key_")
+	assert.NoError(t, err)
+	key.Close()
+	defer os.Remove(key.Name())
+
+	err = writeTLSKeyPair(s, cert.Name(), key.Name())
+	assert.NoError(t, err)
+
+	c, _ := newClient(&RunnerCredentials{
+		URL:         s.URL,
+		TLSCAFile:   ca.Name(),
+		TLSCertFile: cert.Name(),
+		TLSKeyFile:  key.Name(),
+	})
+	statusCode, statusText, tlsData := c.doJSON("test/ok", "GET", 200, nil, nil)
+	assert.Equal(t, 200, statusCode, statusText)
+	assert.NotEmpty(t, tlsData.CAChain)
+	assert.Equal(t, cert.Name(), tlsData.CertFile)
+	assert.Equal(t, key.Name(), tlsData.KeyFile)
+}
+
+func TestClientTLSAuthCertificatesInPredefinedDirectory(t *testing.T) {
+	s := httptest.NewUnstartedServer(http.HandlerFunc(clientHandler))
+	s.TLS = new(tls.Config)
+	s.TLS.ClientAuth = tls.RequireAnyClientCert
+	s.StartTLS()
+	defer s.Close()
+
 	tempDir, err := ioutil.TempDir("", "certs")
 	assert.NoError(t, err)
 	defer os.RemoveAll(tempDir)
 	CertificateDirectory = tempDir
 
-	err = writeTLSCertificate(s, filepath.Join(tempDir, "127.0.0.1.crt"))
+	serverURL, err := url.Parse(s.URL)
+	require.NoError(t, err)
+	hostname, _, err := net.SplitHostPort(serverURL.Host)
+	require.NoError(t, err)
+
+	err = writeTLSCertificate(s, filepath.Join(tempDir, hostname+".crt"))
+	assert.NoError(t, err)
+
+	err = writeTLSKeyPair(s,
+		filepath.Join(tempDir, hostname+".auth.crt"),
+		filepath.Join(tempDir, hostname+".auth.key"))
 	assert.NoError(t, err)
 
 	c, _ := newClient(&RunnerCredentials{
 		URL: s.URL,
 	})
-	statusCode, statusText, certificates := c.doJSON("test/ok", "GET", 200, nil, nil)
+	statusCode, statusText, tlsData := c.doJSON("test/ok", "GET", 200, nil, nil)
 	assert.Equal(t, 200, statusCode, statusText)
-	assert.NotEmpty(t, certificates)
+	assert.NotEmpty(t, tlsData.CAChain)
+	assert.NotEmpty(t, tlsData.CertFile)
+	assert.NotEmpty(t, tlsData.KeyFile)
 }
 
 func TestUrlFixing(t *testing.T) {
diff --git a/network/gitlab.go b/network/gitlab.go
index ae73995c265..33a049e311b 100644
--- a/network/gitlab.go
+++ b/network/gitlab.go
@@ -35,7 +35,7 @@ func (n *GitLabClient) getClient(credentials requestCredentials) (c *client, err
 	if n.clients == nil {
 		n.clients = make(map[string]*client)
 	}
-	key := fmt.Sprintf("%s_%s_%s", credentials.GetURL(), credentials.GetToken(), credentials.GetTLSCAFile())
+	key := fmt.Sprintf("%s_%s_%s_%s", credentials.GetURL(), credentials.GetToken(), credentials.GetTLSCAFile(), credentials.GetTLSCertFile())
 	c = n.clients[key]
 	if c == nil {
 		c, err = newClient(credentials)
@@ -86,10 +86,10 @@ func (n *GitLabClient) doRaw(credentials requestCredentials, method, uri string,
 	return c.do(uri, method, request, requestType, headers)
 }
 
-func (n *GitLabClient) doJSON(credentials requestCredentials, method, uri string, statusCode int, request interface{}, response interface{}) (int, string, string) {
+func (n *GitLabClient) doJSON(credentials requestCredentials, method, uri string, statusCode int, request interface{}, response interface{}) (int, string, ResponseTLSData) {
 	c, err := n.getClient(credentials)
 	if err != nil {
-		return clientError, err.Error(), ""
+		return clientError, err.Error(), ResponseTLSData{}
 	}
 
 	return c.doJSON(uri, method, statusCode, request, response)
@@ -197,6 +197,24 @@ func (n *GitLabClient) UnregisterRunner(runner common.RunnerCredentials) bool {
 	}
 }
 
+func addTLSData(response *common.JobResponse, tlsData ResponseTLSData) {
+	if tlsData.CAChain != "" {
+		response.TLSCAChain = tlsData.CAChain
+	}
+
+	if tlsData.CertFile != "" && tlsData.KeyFile != "" {
+		data, err := ioutil.ReadFile(tlsData.CertFile)
+		if err == nil {
+			response.TLSAuthCert = string(data)
+		}
+		data, err = ioutil.ReadFile(tlsData.KeyFile)
+		if err == nil {
+			response.TLSAuthKey = string(data)
+		}
+
+	}
+}
+
 func (n *GitLabClient) RequestJob(config common.RunnerConfig) (*common.JobResponse, bool) {
 	request := common.JobRequest{
 		Info:       n.getRunnerVersion(config),
@@ -205,7 +223,7 @@ func (n *GitLabClient) RequestJob(config common.RunnerConfig) (*common.JobRespon
 	}
 
 	var response common.JobResponse
-	result, statusText, certificates := n.doJSON(&config.RunnerCredentials, "POST", "jobs/request", 201, &request, &response)
+	result, statusText, tlsData := n.doJSON(&config.RunnerCredentials, "POST", "jobs/request", 201, &request, &response)
 
 	switch result {
 	case 201:
@@ -213,7 +231,7 @@ func (n *GitLabClient) RequestJob(config common.RunnerConfig) (*common.JobRespon
 			"job":      strconv.Itoa(response.ID),
 			"repo_url": response.RepoCleanURL(),
 		}).Println("Checking for jobs...", "received")
-		response.TLSCAChain = certificates
+		addTLSData(&response, tlsData)
 		return &response, true
 	case 403:
 		config.Log().Errorln("Checking for jobs...", "forbidden")
diff --git a/network/gitlab_test.go b/network/gitlab_test.go
index bf570c41a2e..bab2319c033 100644
--- a/network/gitlab_test.go
+++ b/network/gitlab_test.go
@@ -42,12 +42,26 @@ func TestClients(t *testing.T) {
 		URL:       "http://test/",
 		TLSCAFile: "ca_file",
 	})
-	c6, c6err := c.getClient(&brokenCredentials)
+	c6, _ := c.getClient(&RunnerCredentials{
+		URL:         "http://test/",
+		TLSCAFile:   "ca_file",
+		TLSCertFile: "cert_file",
+		TLSKeyFile:  "key_file",
+	})
+	c7, _ := c.getClient(&RunnerCredentials{
+		URL:         "http://test/",
+		TLSCAFile:   "ca_file",
+		TLSCertFile: "cert_file",
+		TLSKeyFile:  "key_file2",
+	})
+	c8, c8err := c.getClient(&brokenCredentials)
 	assert.NotEqual(t, c1, c2)
 	assert.NotEqual(t, c1, c4)
 	assert.Equal(t, c4, c5)
-	assert.Nil(t, c6)
-	assert.Error(t, c6err)
+	assert.NotEqual(t, c5, c6)
+	assert.Equal(t, c6, c7)
+	assert.Nil(t, c8)
+	assert.Error(t, c8err)
 }
 
 func testRegisterRunnerHandler(w http.ResponseWriter, r *http.Request, t *testing.T) {
diff --git a/shells/abstract.go b/shells/abstract.go
index 41d09f9702b..78c9d5d6838 100644
--- a/shells/abstract.go
+++ b/shells/abstract.go
@@ -28,15 +28,9 @@ func (b *AbstractShell) writeExports(w ShellWriter, info common.ShellScriptInfo)
 	}
 }
 
-func (b *AbstractShell) writeTLSCAInfo(w ShellWriter, build *common.Build, key string) {
-	if build.TLSCAChain != "" {
-		w.Variable(common.JobVariable{
-			Key:      key,
-			Value:    build.TLSCAChain,
-			Public:   true,
-			Internal: true,
-			File:     true,
-		})
+func (b *AbstractShell) writeGitExports(w ShellWriter, info common.ShellScriptInfo) {
+	for _, variable := range info.Build.GetGitTLSVariables() {
+		w.Variable(variable)
 	}
 }
 
@@ -293,7 +287,7 @@ func (b *AbstractShell) writeSubmoduleUpdateCmds(w ShellWriter, info common.Shel
 
 func (b *AbstractShell) writeGetSourcesScript(w ShellWriter, info common.ShellScriptInfo) (err error) {
 	b.writeExports(w, info)
-	b.writeTLSCAInfo(w, info.Build, "GIT_SSL_CAINFO")
+	b.writeGitExports(w, info)
 
 	if info.PreCloneScript != "" && info.Build.GetGitStrategy() != common.GitNone {
 		b.writeCommands(w, info.PreCloneScript)
@@ -313,7 +307,6 @@ func (b *AbstractShell) writeGetSourcesScript(w ShellWriter, info common.ShellSc
 func (b *AbstractShell) writeRestoreCacheScript(w ShellWriter, info common.ShellScriptInfo) (err error) {
 	b.writeExports(w, info)
 	b.writeCdBuildDir(w, info)
-	b.writeTLSCAInfo(w, info.Build, "CI_SERVER_TLS_CA_FILE")
 
 	// Try to restore from main cache, if not found cache for master
 	b.cacheExtractor(w, info)
@@ -323,7 +316,6 @@ func (b *AbstractShell) writeRestoreCacheScript(w ShellWriter, info common.Shell
 func (b *AbstractShell) writeDownloadArtifactsScript(w ShellWriter, info common.ShellScriptInfo) (err error) {
 	b.writeExports(w, info)
 	b.writeCdBuildDir(w, info)
-	b.writeTLSCAInfo(w, info.Build, "CI_SERVER_TLS_CA_FILE")
 
 	// Process all artifacts
 	b.downloadAllArtifacts(w, info)
@@ -503,7 +495,6 @@ func (b *AbstractShell) writeAfterScript(w ShellWriter, info common.ShellScriptI
 func (b *AbstractShell) writeArchiveCacheScript(w ShellWriter, info common.ShellScriptInfo) (err error) {
 	b.writeExports(w, info)
 	b.writeCdBuildDir(w, info)
-	b.writeTLSCAInfo(w, info.Build, "CI_SERVER_TLS_CA_FILE")
 
 	// Find cached files and archive them
 	b.cacheArchiver(w, info)
@@ -513,7 +504,6 @@ func (b *AbstractShell) writeArchiveCacheScript(w ShellWriter, info common.Shell
 func (b *AbstractShell) writeUploadArtifactsScript(w ShellWriter, info common.ShellScriptInfo) (err error) {
 	b.writeExports(w, info)
 	b.writeCdBuildDir(w, info)
-	b.writeTLSCAInfo(w, info.Build, "CI_SERVER_TLS_CA_FILE")
 
 	// Upload artifacts
 	b.uploadArtifacts(w, info)
-- 
GitLab