Skip to content

Commit

Permalink
feat: Add tls support kafka (#1667)
Browse files Browse the repository at this point in the history
* Add Kafka TLS config

Signed-off-by: Shan Gardezi <shangardezi@gmail.com>

* remove unused files

Signed-off-by: Shan Gardezi <shangardezi@gmail.com>

* Use ticker instead

Signed-off-by: Shan Gardezi <shangardezi@gmail.com>

* generate docs

Signed-off-by: Shan Gardezi <shangardezi@gmail.com>

* Add InsecureSkipVerify in conf

Signed-off-by: Shan Gardezi <shangardezi@gmail.com>

* fix test

Signed-off-by: Shan Gardezi <shangardezi@gmail.com>

* explicity set InsecureSkipVerify to false; ignore linter warning

Signed-off-by: Shan Gardezi <shangardezi@gmail.com>

* Cancel context in tests

Signed-off-by: Shan Gardezi <shangardezi@gmail.com>

* Cancel context in publisher test

Signed-off-by: Shan Gardezi <shangardezi@gmail.com>

* unexport tlsreloader and tls version

Signed-off-by: Shan Gardezi <shangardezi@gmail.com>

* remove unncessary InsecureSkipVerify default

Signed-off-by: Shan Gardezi <shangardezi@gmail.com>

* allow zero value for ReloadInterval; allow tls encryption without client cert

Signed-off-by: Shan Gardezi <shangardezi@gmail.com>

* don't throw error in reload loop rather log

Signed-off-by: Shan Gardezi <shangardezi@gmail.com>

* continue when erroring to load cert; remove error return in reload func

Signed-off-by: Shan Gardezi <shangardezi@gmail.com>

* fix docs

Signed-off-by: Shan Gardezi <shangardezi@gmail.com>

* return when context done

Signed-off-by: Shan Gardezi <shangardezi@gmail.com>

* no longer set ClientCAs

Signed-off-by: Shan Gardezi <shangardezi@gmail.com>

* restore nolint:staticcheck

Signed-off-by: Shan Gardezi <shangardezi@gmail.com>

---------

Signed-off-by: Shan Gardezi <shangardezi@gmail.com>
  • Loading branch information
shangardezi committed Jul 6, 2023
1 parent 210ccc3 commit beb3a65
Show file tree
Hide file tree
Showing 16 changed files with 527 additions and 12 deletions.
7 changes: 7 additions & 0 deletions docs/modules/configuration/partials/fullconfiguration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ audit:
path: /path/to/file.log # Path to the log file to use as output. The special values stdout and stderr can be used to write to stdout or stderr respectively.
kafka:
ack: all # Ack mode for producing messages. Valid values are "none", "leader" or "all" (default). Idempotency is disabled when mode is not "all".
authentication: # Authentication
tls:
caPath: /path/to/ca.crt # Required. CAPath is the path to the CA certificate.
certPath: /path/to/tls.cert # CertPath is the path to the client certificate.
insecureSkipVerify: true # InsecureSkipVerify controls whether the server's certificate chain and host name are verified. Default is false.
keyPath: /path/to/tls.key # KeyPath is the path to the client key.
reloadInterval: 5m # ReloadInterval is the interval at which the TLS certificates are reloaded. The default is 0 (no reload).
brokers: ['localhost:9092'] # Required. Brokers list to seed the Kafka client.
clientID: cerbos # ClientID reported in Kafka connections.
closeTimeout: 30s # CloseTimeout sets how long when closing the client to wait for any remaining messages to be flushed.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ require (
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/twmb/franz-go/pkg/kmsg v1.4.0 // indirect
github.com/twmb/franz-go/pkg/kmsg v1.5.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2417,8 +2417,8 @@ github.com/twmb/franz-go v1.13.6 h1:DRh06Hy3GthZuA+fQhDo+IMV+QUZHQfS2TIiWf/rCw8=
github.com/twmb/franz-go v1.13.6/go.mod h1:jm/FtYxmhxDTN0gNSb26XaJY0irdSVcsckLiR5tQNMk=
github.com/twmb/franz-go/pkg/kadm v1.8.1 h1:SrzL855I7gQTGdMtOYGTHhebs7TPgPN29FPtjusqwlE=
github.com/twmb/franz-go/pkg/kadm v1.8.1/go.mod h1:qUSM7pxoMCU1UNu5H4USE64ODcVmeG9LS96mysv1nu8=
github.com/twmb/franz-go/pkg/kmsg v1.4.0 h1:tbp9hxU6m8qZhQTlpGiaIJOm4BXix5lsuEZ7K00dF0s=
github.com/twmb/franz-go/pkg/kmsg v1.4.0/go.mod h1:SxG/xJKhgPu25SamAq0rrucfp7lbzCpEXOC+vH/ELrY=
github.com/twmb/franz-go/pkg/kmsg v1.5.0 h1:eqVJquFQLdBNLrRMWX03pPDPpngn6PTjGZLlZnagouk=
github.com/twmb/franz-go/pkg/kmsg v1.5.0/go.mod h1:se9Mjdt0Nwzc9lnjJ0HyDtLyBnaBDAd7pCje47OhSyw=
github.com/twmb/franz-go/plugin/kzap v1.1.2 h1:0arX5xJ0soUPX1LlDay6ZZoxuWkWk1lggQ5M/IgRXAE=
github.com/twmb/franz-go/plugin/kzap v1.1.2/go.mod h1:53Cl9Uz1pbdOPDvUISIxLrZIWSa2jCuY1bTMauRMBmo=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
Expand Down
19 changes: 19 additions & 0 deletions internal/audit/kafka/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,29 @@ const (
defaultMaxBufferedRecords = 250
)

type Authentication struct {
TLS *TLS `yaml:"tls"`
}

type TLS struct {
// CAPath is the path to the CA certificate.
CAPath string `yaml:"caPath" conf:"required,example=/path/to/ca.crt"`
// CertPath is the path to the client certificate.
CertPath string `yaml:"certPath" conf:",example=/path/to/tls.cert"`
// KeyPath is the path to the client key.
KeyPath string `yaml:"keyPath" conf:",example=/path/to/tls.key"`
// ReloadInterval is the interval at which the TLS certificates are reloaded. The default is 0 (no reload).
ReloadInterval time.Duration `yaml:"reloadInterval" conf:",example=5m"`
// InsecureSkipVerify controls whether the server's certificate chain and host name are verified. Default is false.
InsecureSkipVerify bool `yaml:"insecureSkipVerify" conf:",example=true"`
}

// Conf is optional configuration for kafka Audit.
type Conf struct {
// Ack mode for producing messages. Valid values are "none", "leader" or "all" (default). Idempotency is disabled when mode is not "all".
Ack string `yaml:"ack" conf:",example=all"`
// Authentication
Authentication Authentication `yaml:"authentication"`
// Topic to write audit entries to.
Topic string `yaml:"topic" conf:"required,example=cerbos.audit.log"`
// Encoding format. Valid values are "json" (default) or "protobuf".
Expand Down
142 changes: 136 additions & 6 deletions internal/audit/kafka/kafka_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package kafka_test
import (
"context"
"fmt"
"path/filepath"
"strconv"
"testing"
"time"
Expand All @@ -21,7 +22,7 @@ import (

auditv1 "github.com/cerbos/cerbos/api/genpb/cerbos/audit/v1"
"github.com/cerbos/cerbos/internal/audit"
_ "github.com/cerbos/cerbos/internal/audit/kafka"
"github.com/cerbos/cerbos/internal/audit/kafka"
"github.com/cerbos/cerbos/internal/config"
"github.com/cerbos/cerbos/internal/util"
)
Expand All @@ -33,6 +34,55 @@ const (
defaultIntegrationTopic = "cerbos"
)

func TestProduceWithTLS(t *testing.T) {
t.Parallel()

ctx := context.Background()

// setup kafka
uri := newKafkaBrokerWithTLS(t, defaultIntegrationTopic, "testdata/valid/ca.crt", "testdata/valid/client/tls.crt", "testdata/valid/client/tls.key")
log, err := newLog(map[string]any{
"audit": map[string]any{
"enabled": true,
"backend": "kafka",
"kafka": map[string]any{
"authentication": map[string]any{
"tls": map[string]any{
"caPath": "testdata/valid/ca.crt",
"certPath": "testdata/valid/client/tls.crt",
"keyPath": "testdata/valid/client/tls.key",
"reloadInterval": "10s",
},
},
"brokers": []string{uri},
"topic": defaultIntegrationTopic,
"produceSync": true,
},
},
})
require.NoError(t, err)

// write audit log entries
err = log.WriteAccessLogEntry(ctx, func() (*auditv1.AccessLogEntry, error) {
return &auditv1.AccessLogEntry{
CallId: "01ARZ3NDEKTSV4RRFFQ69G5FA1",
}, nil
})
require.NoError(t, err)

err = log.WriteDecisionLogEntry(ctx, func() (*auditv1.DecisionLogEntry, error) {
return &auditv1.DecisionLogEntry{
CallId: "01ARZ3NDEKTSV4RRFFQ69G5FA2",
}, nil
})
require.NoError(t, err)

// validate we see this entries in kafka
records, err := fetchKafkaTopic(t, uri, defaultIntegrationTopic, true)
require.NoError(t, err)
require.Len(t, records, 2, "unexpected number of published audit log entries")
}

func TestSyncProduce(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -69,7 +119,7 @@ func TestSyncProduce(t *testing.T) {
require.NoError(t, err)

// validate we see this entries in kafka
records, err := fetchKafkaTopic(uri, defaultIntegrationTopic)
records, err := fetchKafkaTopic(t, uri, defaultIntegrationTopic, false)
require.NoError(t, err)
require.Len(t, records, 2, "unexpected number of published audit log entries")
}
Expand Down Expand Up @@ -110,7 +160,7 @@ func TestCompression(t *testing.T) {
}

// validate we see these entries in kafka
records, err := fetchKafkaTopic(uri, defaultIntegrationTopic)
records, err := fetchKafkaTopic(t, uri, defaultIntegrationTopic, false)
require.NoError(t, err)
require.Len(t, records, 5, "unexpected number of published audit log entries")
}
Expand Down Expand Up @@ -152,12 +202,76 @@ func TestAsyncProduce(t *testing.T) {

// validate we see this entries in kafka, eventually
require.Eventually(t, func() bool {
records, err := fetchKafkaTopic(uri, defaultIntegrationTopic)
records, err := fetchKafkaTopic(t, uri, defaultIntegrationTopic, false)
require.NoError(t, err)
return len(records) == 2
}, 10*time.Second, 100*time.Millisecond, "expected to see audit log entries in kafka")
}

func newKafkaBrokerWithTLS(t *testing.T, topic, caPath, certPath, keyPath string) string {
t.Helper()

testDataAbsPath, err := filepath.Abs("testdata/valid")
require.NoError(t, err)

pool, err := dockertest.NewPool("")
require.NoError(t, err, "Failed to connect to Docker")

hostPort := 65136

resource, err := pool.RunWithOptions(&dockertest.RunOptions{
Repository: redpandaImage,
Tag: redpandaVersion,
Cmd: []string{
"redpanda",
"start",
"--mode", "dev-container",
// kafka admin client will retrieve the advertised address from the broker
// so we need it to use the same port that is exposed on the container
"--config", "/etc/redpanda/rpconfig.yaml",
"--advertise-kafka-addr", fmt.Sprintf("localhost:%d", hostPort),
},
ExposedPorts: []string{
"9092/tcp",
},
PortBindings: map[docker.Port][]docker.PortBinding{
"9092/tcp": {{HostIP: "localhost", HostPort: strconv.Itoa(hostPort)}},
},
Mounts: []string{
fmt.Sprintf("%s:/etc/redpanda", testDataAbsPath),
},
}, func(config *docker.HostConfig) {
config.AutoRemove = true
})
require.NoError(t, err, "Failed to start container")

t.Cleanup(func() {
_ = pool.Purge(resource)
})

brokerDSN := fmt.Sprintf("localhost:%d", hostPort)
duration := 10 * time.Second
skipVerify := false

ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)

tlsConfig, err := kafka.NewTLSConfig(ctx, duration, skipVerify, caPath, certPath, keyPath)
require.NoError(t, err)

client, err := kgo.NewClient(kgo.SeedBrokers(brokerDSN), kgo.DialTLSConfig(tlsConfig))
require.NoError(t, err)

require.NoError(t, pool.Retry(func() error {
return client.Ping(context.Background())
}), "Failed to connect to Kafka")
// create topic
_, err = kadm.NewClient(client).CreateTopic(context.Background(), 1, 1, nil, topic)
require.NoError(t, err, "Failed to create Kafka topic")

return brokerDSN
}

func newKafkaBroker(t *testing.T, topic string) string {
t.Helper()

Expand Down Expand Up @@ -187,6 +301,7 @@ func newKafkaBroker(t *testing.T, topic string) string {
}, func(config *docker.HostConfig) {
config.AutoRemove = true
})

require.NoError(t, err, "Failed to start container")

t.Cleanup(func() {
Expand All @@ -208,8 +323,23 @@ func newKafkaBroker(t *testing.T, topic string) string {
return brokerDSN
}

func fetchKafkaTopic(uri, topic string) ([]*kgo.Record, error) {
client, err := kgo.NewClient(kgo.SeedBrokers(uri))
func fetchKafkaTopic(t *testing.T, uri string, topic string, tlsEnabled bool) ([]*kgo.Record, error) {
kgoOptions := []kgo.Opt{kgo.SeedBrokers(uri)}
if tlsEnabled {
duration := 10 * time.Second
skipVerify := false
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)

tlsConfig, err := kafka.NewTLSConfig(ctx, duration, skipVerify, "testdata/valid/ca.crt", "testdata/valid/client/tls.crt", "testdata/valid/client/tls.key")
if err != nil {
return nil, err
}

kgoOptions = append(kgoOptions, kgo.DialTLSConfig(tlsConfig))
}

client, err := kgo.NewClient(kgoOptions...)
if err != nil {
return nil, err
}
Expand Down
18 changes: 16 additions & 2 deletions internal/audit/kafka/publisher.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func init() {
return nil, fmt.Errorf("failed to read kafka audit log configuration: %w", err)
}

return NewPublisher(conf, decisionFilter)
return NewPublisher(ctx, conf, decisionFilter)
})
}

Expand All @@ -81,7 +81,7 @@ type Publisher struct {
closeTimeout time.Duration
}

func NewPublisher(conf *Conf, decisionFilter audit.DecisionLogEntryFilter) (*Publisher, error) {
func NewPublisher(ctx context.Context, conf *Conf, decisionFilter audit.DecisionLogEntryFilter) (*Publisher, error) {
clientOpts := []kgo.Opt{
kgo.ClientID(conf.ClientID),
kgo.SeedBrokers(conf.Brokers...),
Expand Down Expand Up @@ -111,6 +111,20 @@ func NewPublisher(conf *Conf, decisionFilter audit.DecisionLogEntryFilter) (*Pub

clientOpts = append(clientOpts, kgo.ProducerBatchCompression(compression...))

if conf.Authentication.TLS != nil {
tlsConfig, err := NewTLSConfig(ctx,
conf.Authentication.TLS.ReloadInterval,
conf.Authentication.TLS.InsecureSkipVerify,
conf.Authentication.TLS.CAPath,
conf.Authentication.TLS.CertPath,
conf.Authentication.TLS.KeyPath)
if err != nil {
return nil, err
}

clientOpts = append(clientOpts, kgo.DialTLSConfig(tlsConfig))
}

client, err := kgo.NewClient(clientOpts...)
if err != nil {
return nil, err
Expand Down
5 changes: 4 additions & 1 deletion internal/audit/kafka/publisher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,10 @@ func newPublisher(t *testing.T, cfg kafka.Conf) (*kafka.Publisher, *mockClient)
config.Encoding = cfg.Encoding
config.ProduceSync = cfg.ProduceSync

publisher, err := kafka.NewPublisher(config, nil)
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)

publisher, err := kafka.NewPublisher(ctx, config, nil)
require.NoError(t, err)

kafkaClient := &mockClient{}
Expand Down
22 changes: 22 additions & 0 deletions internal/audit/kafka/testdata/valid/ca.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDrDCCApSgAwIBAgIUGGL6FgvIOjAexRLaeJo3syPP4R8wDQYJKoZIhvcNAQEL
BQAwbTELMAkGA1UEBhMCR0IxEDAOBgNVBAgTB0VuZ2xhbmQxDzANBgNVBAcTBkxv
bmRvbjENMAsGA1UEChMEVGVzdDEVMBMGA1UECxMMVGVzdCBSb290IENBMRUwEwYD
VQQDEwxUZXN0IFJvb3QgQ0EwIBcNMjMwNjIyMTAyMDAwWhgPMjIyMzA2MTYwMjIw
MDBaMG0xCzAJBgNVBAYTAkdCMRAwDgYDVQQIEwdFbmdsYW5kMQ8wDQYDVQQHEwZM
b25kb24xDTALBgNVBAoTBFRlc3QxFTATBgNVBAsTDFRlc3QgUm9vdCBDQTEVMBMG
A1UEAxMMVGVzdCBSb290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
AQEAm8kgp7tJWwUmIqzZ2xNoE9Kt2twsmLANVNQVyX6axWgOQx8+0GjvJ/tlkSeX
Dnct8NN5Xm+NYMXQ2nOEms/9+EhQFrceSDVx1JASv53kdyrB3ZhNf/iRuPfTv4ov
kmRzErm7l4QVgXg6bQ+6BSg8l7607C5CxzaxsiYVkSX+KmLBojyMAtGAt60F3mkc
DLamqxPyEJimB69pYakS6ByFVY/l+zApA+W8xnMZd4VvCfTH8neh2djPYwxPwAq6
VFMRHEcV37VKmXN6TnaXUmVAtDCcktImjubFMpkzOQ+4Cnb60gWHHGjAtQRQqE4h
HOHpoksUeVYstKtrDEG8+RnuEwIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD
VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUi5yLOciDVzxjOW10NBYEFkbPg8QwDQYJ
KoZIhvcNAQELBQADggEBAFBJU24UqIEY2ncoy5vGN5qhL7D5Hj13O8HaC7uAZeXf
zRHDU51FIIX8ICFDYDYx9MSwn2RvgoyKX+vc1x9Pn7CiWepZuv9/JkVgWZGLnjhV
RF98Aj5llHu93EE78WPkPBJW7M0hQe3q8wHdD5U57Ct5tMgrW1x3ZStJN3Stybll
Y8+WhxyQqm1BUMUSyuUtP3jLbYwIDcwGZSpXLWo44rll0d+puL+58Z0pi4V22AuS
jRO3rBwXPELnIISPv7y1Rqq4yRCjTRCrfPEjD+QmIDeDQNoUHGqKgcfy0G+zxNFd
v1GbdI8U3FOEuhpmmSVtV0S0vQWRXl09PjuepxqIh6A=
-----END CERTIFICATE-----
22 changes: 22 additions & 0 deletions internal/audit/kafka/testdata/valid/client/tls.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDmzCCAoOgAwIBAgIUMi8eHlY+fO6sXSSCLR+7b/yLP4swDQYJKoZIhvcNAQEL
BQAwbTELMAkGA1UEBhMCR0IxEDAOBgNVBAgTB0VuZ2xhbmQxDzANBgNVBAcTBkxv
bmRvbjENMAsGA1UEChMEVGVzdDEVMBMGA1UECxMMVGVzdCBSb290IENBMRUwEwYD
VQQDEwxUZXN0IFJvb3QgQ0EwIBcNMjMwNjIyMTAzMjAwWhgPMjIyMzA2MTYwMjMy
MDBaMBExDzANBgNVBAMTBmNsaWVudDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAPqud60L1zyG0nh/u9DqDYnAp7QK40Kd/pau3U0GTm6d+Fd18DyTplWO
cjAXxiIC9GfRO6AKlLctYO5Lr5x2Hc8QM7Ol6BDz2Op9MFQhCcYrAGSF/GLRlMyG
cxWDaMHSGDRYmXPCnZ48B29teIPaNgBwz4wNLc4wnk1PiMxVSvkaggXa4G4jbA0j
lkSijXbkSvM2FZ2tmro2p6dhLlpZ7OLtoYy2lAhiRrkxxKuZEhddMmQSrwlhHzAu
qRgEh8kFG2XNR624L+fD3W81l+ukkIKr76WtIj8BiWtsxuKvvzT0GCRJQdK3SLqS
BaaCqRbhhavgkvexOQAlO3eTU26je9UCAwEAAaOBjDCBiTAOBgNVHQ8BAf8EBAMC
BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU
ACaMaKhhu8MuOwClkkyCMW1xK1QwHwYDVR0jBBgwFoAUi5yLOciDVzxjOW10NBYE
FkbPg8QwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQCS
FG+TtgCiqgYPCuVcNzNDoDEaVwYgjv8WxP08stCK/hFBBur0HG3s2+RY8ru4Js5d
w+8b17573y3H3zM9DNxfapRg7j1EuyulD2utCBD5ajt/+xsea/yfxo6hr6tMzNAE
5vTzyanBgkiuxYLA566T+k6u+jwoqGcGr9GJD2FHjTqxcLD/EmTEnA7PkcR3jjvh
ZLsLYfYSwj1cn9IwdO+DXCXFna1unGCEltlUQPSVXskKTo9dJsw4agfS5Xa4CTI6
lF4/Ym6Gan7/iznNIL2Hk/aU2X9HDIB/5GLa4qktK73PuTfAsPGsd0GffLz62kKW
lK/90M6PGwLnXCuCxAgA
-----END CERTIFICATE-----
27 changes: 27 additions & 0 deletions internal/audit/kafka/testdata/valid/client/tls.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA+q53rQvXPIbSeH+70OoNicCntArjQp3+lq7dTQZObp34V3Xw
PJOmVY5yMBfGIgL0Z9E7oAqUty1g7kuvnHYdzxAzs6XoEPPY6n0wVCEJxisAZIX8
YtGUzIZzFYNowdIYNFiZc8KdnjwHb214g9o2AHDPjA0tzjCeTU+IzFVK+RqCBdrg
biNsDSOWRKKNduRK8zYVna2aujanp2EuWlns4u2hjLaUCGJGuTHEq5kSF10yZBKv
CWEfMC6pGASHyQUbZc1Hrbgv58PdbzWX66SQgqvvpa0iPwGJa2zG4q+/NPQYJElB
0rdIupIFpoKpFuGFq+CS97E5ACU7d5NTbqN71QIDAQABAoIBAQCOmuWV2aCN3Byr
U+7iP+OHs8MzVuZFpV+JFNzrzmSb5N2702ng7BW5ohkvZrbd7lRfItYKizpiojv8
E65JEaCPhKYE0qKb0jxZ2PG/SjQnOZs1lEspZBSD7RBW8rSLXhtK3AQfqFzGAVwx
dVMTnvUNdx96ipy+KUZEsLQfmX3XCq8l1jHBWSC8BNJQEqe7KqSEjyAVxadqI591
x+s5fJIYpXVDt9b5zrdcwdmVxYzJ9Nn6TsW8AP0NAnjq6RCHIFzIiOKq8KLzS0h9
6MqsU8fGP26HlXpjdaQlL7Tb2df+ga5cZW5zENQI083mRYiMQWod76/FRbupN+hT
ENPpvekBAoGBAP8Lqux+L54XqQAsd1YYtVRWlceC4p/I9xzaIDxtFtQYMrNDtW5M
bbApRHUnP5FzOmA7U2JaeJBPKxiPPe+D1vsfpMjGUzp3sW7BGdS5x39XOQA7pqK2
W6FCwtPlv0I1+DQqkudO/OYHZjEdkolkMg4/XMBBf1UiOz3eonn7QluhAoGBAPue
nnrwAFCbv9ckbQbnn1gRX9LNVvDIABkDSMQachS0bjtOcu+IJG0sjydWGizNtXNP
WJbBLqwk9Ib68JE1x3ikDdvdakYz+Vtnf1Zyp00PVDHuP49Hf2pe/kjEuYd41zCG
Cg0qey2ODTNECuXa9w4RRep9r7F5K6Nnr44iU9O1AoGAMIRUsIZUoptXn9vVm8A5
hmCuP3TLjZ/aOlfYOAZ8iD5OLsHbmq7ZUuCW0D52HkIwQawncZdKRhF5XkOpgY2v
8LeVTkhD/uRUEYCUXF428Cd0hXHTgjJ0fdnIXCzhVEQWAj5zEaN6Anw32XIJtS8l
QoaVK1GKWnSXlm5qtA+zEOECgYEA4cD/D+4lWi1jgfP8niVSogF0p/3z8zR+YfLA
ZrITiOAZxrwsAx1zEUDZb8Gg2nH2Su713MyWw3ykqDadgKtvvJ30kT+nCjW3lHrX
lQhpcoo+UE4iWLsdZqK0IzXd/947tB2PByEGQ2kgDs8NuA0tfEGjKTB7YhAFRybQ
LAZAj8ECgYBRwOtEahgdWsuv0sESwXeZ+0DsMLimcnSBNyvhXUbsL8XQdV9wfd/0
nSBqCWqEoXqwqmVLQrVTLfQ9gyRThQ4d0iBLii/DTy0LNYgqOgOucG5m/+Cosger
AK0ahnzJsvWGxXnJH/clTeYiRzAqFA0CruLwjbD+9l/vJoqOAFh7lQ==
-----END RSA PRIVATE KEY-----

0 comments on commit beb3a65

Please sign in to comment.