Commit 4ca3cc90 by bergquist

Merge branch 'mattbostock-verify_datasource_tls'

parents b76de790 79afd638
...@@ -6,14 +6,13 @@ import ( ...@@ -6,14 +6,13 @@ import (
"net/http" "net/http"
"time" "time"
"gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/api/pluginproxy" "github.com/grafana/grafana/pkg/api/pluginproxy"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
macaron "gopkg.in/macaron.v1"
) )
var pluginProxyTransport = &http.Transport{ var pluginProxyTransport = &http.Transport{
......
...@@ -3,6 +3,7 @@ package models ...@@ -3,6 +3,7 @@ package models
import ( import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"errors"
"net" "net"
"net/http" "net/http"
"sync" "sync"
...@@ -45,9 +46,16 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) { ...@@ -45,9 +46,16 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) {
return t.Transport, nil return t.Transport, nil
} }
var tlsSkipVerify, tlsClientAuth, tlsAuthWithCACert bool
if ds.JsonData != nil {
tlsClientAuth = ds.JsonData.Get("tlsAuth").MustBool(false)
tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false)
tlsSkipVerify = ds.JsonData.Get("tlsSkipVerify").MustBool(false)
}
transport := &http.Transport{ transport := &http.Transport{
TLSClientConfig: &tls.Config{ TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, InsecureSkipVerify: tlsSkipVerify,
Renegotiation: tls.RenegotiateFreelyAsClient, Renegotiation: tls.RenegotiateFreelyAsClient,
}, },
Proxy: http.ProxyFromEnvironment, Proxy: http.ProxyFromEnvironment,
...@@ -62,30 +70,24 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) { ...@@ -62,30 +70,24 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) {
IdleConnTimeout: 90 * time.Second, IdleConnTimeout: 90 * time.Second,
} }
var tlsAuth, tlsAuthWithCACert bool if tlsClientAuth || tlsAuthWithCACert {
if ds.JsonData != nil {
tlsAuth = ds.JsonData.Get("tlsAuth").MustBool(false)
tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false)
}
if tlsAuth {
transport.TLSClientConfig.InsecureSkipVerify = false
decrypted := ds.SecureJsonData.Decrypt() decrypted := ds.SecureJsonData.Decrypt()
if tlsAuthWithCACert && len(decrypted["tlsCACert"]) > 0 { if tlsAuthWithCACert && len(decrypted["tlsCACert"]) > 0 {
caPool := x509.NewCertPool() caPool := x509.NewCertPool()
ok := caPool.AppendCertsFromPEM([]byte(decrypted["tlsCACert"])) ok := caPool.AppendCertsFromPEM([]byte(decrypted["tlsCACert"]))
if ok { if !ok {
transport.TLSClientConfig.RootCAs = caPool return nil, errors.New("Failed to parse TLS CA PEM certificate")
} }
transport.TLSClientConfig.RootCAs = caPool
} }
cert, err := tls.X509KeyPair([]byte(decrypted["tlsClientCert"]), []byte(decrypted["tlsClientKey"])) if tlsClientAuth {
if err != nil { cert, err := tls.X509KeyPair([]byte(decrypted["tlsClientCert"]), []byte(decrypted["tlsClientKey"]))
return nil, err if err != nil {
return nil, err
}
transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
} }
transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
} }
ptc.cache[ds.Id] = cachedTransport{ ptc.cache[ds.Id] = cachedTransport{
......
...@@ -29,61 +29,140 @@ func TestDataSourceCache(t *testing.T) { ...@@ -29,61 +29,140 @@ func TestDataSourceCache(t *testing.T) {
Convey("Should be using the cached proxy", func() { Convey("Should be using the cached proxy", func() {
So(t2, ShouldEqual, t1) So(t2, ShouldEqual, t1)
}) })
Convey("Should verify TLS by default", func() {
So(t1.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
})
Convey("Should have no TLS client certificate configured", func() {
So(len(t1.TLSClientConfig.Certificates), ShouldEqual, 0)
})
Convey("Should have no user-supplied TLS CA onfigured", func() {
So(t1.TLSClientConfig.RootCAs, ShouldBeNil)
})
}) })
Convey("When getting kubernetes datasource proxy", t, func() { Convey("When caching a datasource proxy then updating it", t, func() {
clearCache() clearCache()
setting.SecretKey = "password" setting.SecretKey = "password"
json := simplejson.New() json := simplejson.New()
json.Set("tlsAuth", true)
json.Set("tlsAuthWithCACert", true) json.Set("tlsAuthWithCACert", true)
t := time.Now() tlsCaCert, err := util.Encrypt([]byte(caCert), "password")
So(err, ShouldBeNil)
ds := DataSource{
Id: 1,
Url: "http://k8s:8001",
Type: "Kubernetes",
SecureJsonData: map[string][]byte{"tlsCACert": tlsCaCert},
Updated: time.Now().Add(-2 * time.Minute),
}
t1, err := ds.GetHttpTransport()
So(err, ShouldBeNil)
Convey("Should verify TLS by default", func() {
So(t1.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
})
Convey("Should have no TLS client certificate configured", func() {
So(len(t1.TLSClientConfig.Certificates), ShouldEqual, 0)
})
Convey("Should have no user-supplied TLS CA configured", func() {
So(t1.TLSClientConfig.RootCAs, ShouldBeNil)
})
ds.JsonData = nil
ds.SecureJsonData = map[string][]byte{}
ds.Updated = time.Now()
t2, err := ds.GetHttpTransport()
So(err, ShouldBeNil)
Convey("Should have no user-supplied TLS CA configured after the update", func() {
So(t2.TLSClientConfig.RootCAs, ShouldBeNil)
})
})
Convey("When caching a datasource proxy with TLS client authentication enabled", t, func() {
clearCache()
setting.SecretKey = "password"
json := simplejson.New()
json.Set("tlsAuth", true)
tlsClientCert, err := util.Encrypt([]byte(clientCert), "password")
So(err, ShouldBeNil)
tlsClientKey, err := util.Encrypt([]byte(clientKey), "password")
So(err, ShouldBeNil)
ds := DataSource{ ds := DataSource{
Url: "http://k8s:8001", Id: 1,
Type: "Kubernetes", Url: "http://k8s:8001",
Updated: t.Add(-2 * time.Minute), Type: "Kubernetes",
JsonData: json,
SecureJsonData: map[string][]byte{
"tlsClientCert": tlsClientCert,
"tlsClientKey": tlsClientKey,
},
} }
transport, err := ds.GetHttpTransport() tr, err := ds.GetHttpTransport()
So(err, ShouldBeNil) So(err, ShouldBeNil)
Convey("Should have no cert", func() { Convey("Should verify TLS by default", func() {
So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true) So(tr.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
}) })
Convey("Should have a TLS client certificate configured", func() {
So(len(tr.TLSClientConfig.Certificates), ShouldEqual, 1)
})
})
Convey("When caching a datasource proxy with a user-supplied TLS CA", t, func() {
clearCache()
setting.SecretKey = "password"
ds.JsonData = json json := simplejson.New()
json.Set("tlsAuthWithCACert", true)
tlsCaCert, _ := util.Encrypt([]byte(caCert), "password") tlsCaCert, err := util.Encrypt([]byte(caCert), "password")
tlsClientCert, _ := util.Encrypt([]byte(clientCert), "password") So(err, ShouldBeNil)
tlsClientKey, _ := util.Encrypt([]byte(clientKey), "password")
ds.SecureJsonData = map[string][]byte{ ds := DataSource{
"tlsCACert": tlsCaCert, Id: 1,
"tlsClientCert": tlsClientCert, Url: "http://k8s:8001",
"tlsClientKey": tlsClientKey, Type: "Kubernetes",
JsonData: json,
SecureJsonData: map[string][]byte{"tlsCACert": tlsCaCert},
} }
ds.Updated = t.Add(-1 * time.Minute)
transport, err = ds.GetHttpTransport() tr, err := ds.GetHttpTransport()
So(err, ShouldBeNil) So(err, ShouldBeNil)
Convey("Should add cert", func() { Convey("Should verify TLS by default", func() {
So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false) So(tr.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 1) })
Convey("Should have a TLS CA configured", func() {
So(len(tr.TLSClientConfig.RootCAs.Subjects()), ShouldEqual, 1)
}) })
})
ds.JsonData = nil Convey("When caching a datasource proxy when user skips TLS verification", t, func() {
ds.SecureJsonData = map[string][]byte{} clearCache()
ds.Updated = t
json := simplejson.New()
json.Set("tlsSkipVerify", true)
ds := DataSource{
Id: 1,
Url: "http://k8s:8001",
Type: "Kubernetes",
JsonData: json,
}
transport, err = ds.GetHttpTransport() tr, err := ds.GetHttpTransport()
So(err, ShouldBeNil) So(err, ShouldBeNil)
Convey("Should remove cert", func() { Convey("Should skip TLS verification", func() {
So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true) So(tr.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true)
So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 0)
}) })
}) })
} }
...@@ -115,7 +194,8 @@ FHoXIyGOdq1chmRVocdGBCF8fUoGIbuF14r53rpvcbEKtKnnP8+96luKAZLq0a4n ...@@ -115,7 +194,8 @@ FHoXIyGOdq1chmRVocdGBCF8fUoGIbuF14r53rpvcbEKtKnnP8+96luKAZLq0a4n
3lb92xM= 3lb92xM=
-----END CERTIFICATE-----` -----END CERTIFICATE-----`
const clientCert string = `-----BEGIN CERTIFICATE----- const clientCert string = `
-----BEGIN CERTIFICATE-----
MIICsjCCAZoCCQCcd8sOfstQLzANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxj MIICsjCCAZoCCQCcd8sOfstQLzANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxj
YS1rOHMtc3RobG0wHhcNMTYxMTAyMDkyNTE1WhcNMTcxMTAyMDkyNTE1WjAfMR0w YS1rOHMtc3RobG0wHhcNMTYxMTAyMDkyNTE1WhcNMTcxMTAyMDkyNTE1WjAfMR0w
GwYDVQQDDBRhZG0tZGFuaWVsLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQAD GwYDVQQDDBRhZG0tZGFuaWVsLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQAD
......
<div class="gf-form-group"> <div class="gf-form-group">
<h3 class="page-heading">Http settings</h3> <h3 class="page-heading">HTTP settings</h3>
<div class="gf-form-group"> <div class="gf-form-group">
<div class="gf-form-inline"> <div class="gf-form-inline">
<div class="gf-form max-width-30"> <div class="gf-form max-width-30">
<span class="gf-form-label width-7">Url</span> <span class="gf-form-label width-7">URL</span>
<input class="gf-form-input" type="text" <input class="gf-form-input" type="text"
ng-model='current.url' placeholder="{{suggestUrl}}" ng-model='current.url' placeholder="{{suggestUrl}}"
bs-typeahead="getSuggestUrls" min-length="0" bs-typeahead="getSuggestUrls" min-length="0"
ng-pattern="/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/" required></input> ng-pattern="/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/" required></input>
<info-popover mode="right-absolute"> <info-popover mode="right-absolute">
<p>Specify a complete HTTP url (for example http://your_server:8080)</p> <p>Specify a complete HTTP URL (for example http://your_server:8080)</p>
<span ng-show="current.access === 'direct'"> <span ng-show="current.access === 'direct'">
Your access method is <em>Direct</em>, this means the url Your access method is <em>Direct</em>, this means the URL
needs to be accessible from the browser. needs to be accessible from the browser.
</span> </span>
<span ng-show="current.access === 'proxy'"> <span ng-show="current.access === 'proxy'">
Your access method is currently <em>Proxy</em>, this means the url Your access method is currently <em>Proxy</em>, this means the URL
needs to be accessible from the grafana backend. needs to be accessible from the grafana backend.
</span> </span>
</info-popover> </info-popover>
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
<div class="gf-form-select-wrapper gf-form-select-wrapper--has-help-icon max-width-24"> <div class="gf-form-select-wrapper gf-form-select-wrapper--has-help-icon max-width-24">
<select class="gf-form-input" ng-model="current.access" ng-options="f for f in ['direct', 'proxy']"></select> <select class="gf-form-input" ng-model="current.access" ng-options="f for f in ['direct', 'proxy']"></select>
<info-popover mode="right-absolute"> <info-popover mode="right-absolute">
Direct = url is used directly from browser<br> Direct = URL is used directly from browser<br>
Proxy = Grafana backend will proxy the request Proxy = Grafana backend will proxy the request
</info-popover> </info-popover>
</div> </div>
...@@ -38,27 +38,21 @@ ...@@ -38,27 +38,21 @@
</div> </div>
</div> </div>
<h3 class="page-heading">Http Auth</h3> <h3 class="page-heading">HTTP Auth</h3>
<div class="gf-form-group">
<div class="gf-form-inline">
<gf-form-switch class="gf-form" label="Basic Auth" checked="current.basicAuth" label-class="width-8" switch-class="max-width-6"></gf-form-switch>
<gf-form-switch class="gf-form" label="With Credentials" tooltip="Whether credentials such as cookies or auth headers should be sent with cross-site requests." checked="current.withCredentials" label-class="width-11" switch-class="max-width-6"></gf-form-switch>
</div>
<div class="gf-form-inline">
<gf-form-switch class="gf-form" ng-if="current.access=='proxy'" label="TLS Client Auth" label-class="width-8" checked="current.jsonData.tlsAuth" switch-class="max-width-6"></gf-form-switch>
<gf-form-switch class="gf-form" ng-if="current.access=='proxy'" label="With CA Cert" tooltip="Needed for verifing self-signed TLS Certs" checked="current.jsonData.tlsAuthWithCACert" label-class="width-11" switch-class="max-width-6"></gf-form-switch>
</div>
</div>
<div class="gf-form-inline"> <div class="gf-form-inline">
<gf-form-switch class="gf-form" <gf-form-switch class="gf-form" ng-if="current.access=='proxy'" label="Skip TLS Verification (Insecure)" label-class="width-16" checked="current.jsonData.tlsSkipVerify" switch-class="max-width-6"></gf-form-switch>
label="Basic Auth" </div>
checked="current.basicAuth" label-class="width-8" switch-class="max-width-6">
</gf-form-switch>
<gf-form-switch class="gf-form"
label="With Credentials" tooltip="Whether credentials such as cookies or auth headers should be sent with cross-site requests."
checked="current.withCredentials" label-class="width-11" switch-class="max-width-6">
</gf-form-switch>
</div>
<div class="gf-form-inline">
<gf-form-switch class="gf-form" ng-if="current.access=='proxy'"
label="TLS Client Auth" label-class="width-8"
checked="current.jsonData.tlsAuth" switch-class="max-width-6">
</gf-form-switch>
<gf-form-switch class="gf-form" ng-if="current.access=='proxy'"
label="With CA Cert" tooltip="Optional. Needed for self-signed TLS Certs."
checked="current.jsonData.tlsAuthWithCACert" label-class="width-11" switch-class="max-width-6">
</gf-form-switch>
</div> </div>
</div> </div>
...@@ -79,7 +73,7 @@ ...@@ -79,7 +73,7 @@
</div> </div>
</div> </div>
<div class="gf-form-group" ng-if="current.jsonData.tlsAuth && current.access=='proxy'"> <div class="gf-form-group" ng-if="(current.jsonData.tlsAuth || current.jsonData.tlsAuthWithCACert) && current.access=='proxy'">
<div class="gf-form"> <div class="gf-form">
<h6>TLS Auth Details</h6> <h6>TLS Auth Details</h6>
<info-popover mode="header">TLS Certs are encrypted and stored in the Grafana database.</info-popover> <info-popover mode="header">TLS Certs are encrypted and stored in the Grafana database.</info-popover>
...@@ -90,7 +84,7 @@ ...@@ -90,7 +84,7 @@
<label class="gf-form-label width-7">CA Cert</label> <label class="gf-form-label width-7">CA Cert</label>
</div> </div>
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsCACert"> <div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsCACert">
<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsCACert" placeholder="Begins with -----BEGIN CERTIFICATE-----. The CA Certificate is necessary if you are using self-signed certificates."></textarea> <textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsCACert" placeholder="Begins with -----BEGIN CERTIFICATE-----"></textarea>
</div> </div>
<div class="gf-form" ng-if="current.secureJsonFields.tlsCACert"> <div class="gf-form" ng-if="current.secureJsonFields.tlsCACert">
...@@ -100,29 +94,31 @@ ...@@ -100,29 +94,31 @@
</div> </div>
</div> </div>
<div class="gf-form-inline"> <div ng-if="current.jsonData.tlsAuth">
<div class="gf-form gf-form--v-stretch"> <div class="gf-form-inline">
<label class="gf-form-label width-7">Client Cert</label> <div class="gf-form gf-form--v-stretch">
</div> <label class="gf-form-label width-7">Client Cert</label>
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientCert"> </div>
<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientCert" placeholder="Begins with -----BEGIN CERTIFICATE-----" required></textarea> <div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientCert">
</div> <textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientCert" placeholder="Begins with -----BEGIN CERTIFICATE-----" required></textarea>
<div class="gf-form" ng-if="current.secureJsonFields.tlsClientCert"> </div>
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured"> <div class="gf-form" ng-if="current.secureJsonFields.tlsClientCert">
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientCert = false">reset</a> <input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientCert = false">reset</a>
</div>
</div> </div>
</div>
<div class="gf-form-inline"> <div class="gf-form-inline">
<div class="gf-form gf-form--v-stretch"> <div class="gf-form gf-form--v-stretch">
<label class="gf-form-label width-7">Client Key</label> <label class="gf-form-label width-7">Client Key</label>
</div> </div>
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientKey"> <div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientKey">
<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientKey" placeholder="Begins with -----BEGIN RSA PRIVATE KEY-----" required></textarea> <textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientKey" placeholder="Begins with -----BEGIN RSA PRIVATE KEY-----" required></textarea>
</div> </div>
<div class="gf-form" ng-if="current.secureJsonFields.tlsClientKey"> <div class="gf-form" ng-if="current.secureJsonFields.tlsClientKey">
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured"> <input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientKey = false">reset</a> <a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientKey = false">reset</a>
</div>
</div> </div>
</div> </div>
</div> </div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment