feat: add ssl support to sync service (#1479) (#1501)

Adds SSL support to the flagd sync service

---------

Signed-off-by: Alexandra Oberaigner <alexandra.oberaigner@dynatrace.com>
Co-authored-by: Simon Schrottner <simon.schrottner@dynatrace.com>
Co-authored-by: Todd Baert <todd.baert@dynatrace.com>
This commit is contained in:
alexandraoberaigner 2025-01-09 21:44:58 +01:00 committed by GitHub
parent 9891df2d0c
commit d50fcc821c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 342 additions and 115 deletions

View File

@ -145,7 +145,7 @@ func buildTransportCredentials(_ context.Context, cfg CollectorConfig) (credenti
tlsConfig := &tls.Config{
RootCAs: capool,
MinVersion: tls.VersionTLS13,
MinVersion: tls.VersionTLS12,
GetCertificate: func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
certs, err := reloader.GetCertificate()
if err != nil {

View File

@ -2,6 +2,7 @@ package sync
import (
"context"
"crypto/tls"
"fmt"
"net"
"slices"
@ -12,6 +13,7 @@ import (
"github.com/open-feature/flagd/core/pkg/store"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
type ISyncService interface {
@ -28,6 +30,8 @@ type SvcConfigurations struct {
Sources []string
Store *store.Flags
ContextValues map[string]any
CertPath string
KeyPath string
}
type Service struct {
@ -39,6 +43,23 @@ type Service struct {
startupTracker syncTracker
}
func loadTLSCredentials(certPath string, keyPath string) (credentials.TransportCredentials, error) {
// Load server's certificate and private key
serverCert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, fmt.Errorf("failed to load key pair from certificate paths '%s' and '%s': %w", certPath, keyPath, err)
}
// Create the credentials and return it
config := &tls.Config{
Certificates: []tls.Certificate{serverCert},
ClientAuth: tls.NoClientCert,
MinVersion: tls.VersionTLS12,
}
return credentials.NewTLS(config), nil
}
func NewSyncService(cfg SvcConfigurations) (*Service, error) {
l := cfg.Logger
mux, err := NewMux(cfg.Store, cfg.Sources)
@ -46,7 +67,17 @@ func NewSyncService(cfg SvcConfigurations) (*Service, error) {
return nil, fmt.Errorf("error initializing multiplexer: %w", err)
}
server := grpc.NewServer()
var server *grpc.Server
if cfg.CertPath != "" && cfg.KeyPath != "" {
tlsCredentials, err := loadTLSCredentials(cfg.CertPath, cfg.KeyPath)
if err != nil {
return nil, fmt.Errorf("failed to load TLS cert and key: %w", err)
}
server = grpc.NewServer(grpc.Creds(tlsCredentials))
} else {
server = grpc.NewServer()
}
syncv1grpc.RegisterFlagSyncServiceServer(server, &syncHandler{
mux: mux,
log: l,

View File

@ -3,6 +3,7 @@ package sync
import (
"context"
"fmt"
"log"
"testing"
"time"
@ -14,134 +15,173 @@ import (
)
func TestSyncServiceEndToEnd(t *testing.T) {
// given
port := 18016
store, sources := getSimpleFlagStore()
service, err := NewSyncService(SvcConfigurations{
Logger: logger.NewLogger(nil, false),
Port: uint16(port),
Sources: sources,
Store: store,
})
if err != nil {
t.Fatal("error creating the service: %w", err)
return
testCases := []struct {
certPath string
keyPath string
clientCertPath string
tls bool
wantErr bool
}{
{"./test-cert/server-cert.pem", "./test-cert/server-key.pem", "./test-cert/ca-cert.pem", true, false},
{"", "", "", false, false},
{"./lol/not/a/cert", "./test-cert/server-key.pem", "./test-cert/ca-cert.pem", true, true},
}
ctx, cancelFunc := context.WithCancel(context.Background())
doneChan := make(chan interface{})
go func() {
// error ignored, tests will fail if start is not successful
_ = service.Start(ctx)
close(doneChan)
}()
// trigger manual emits matching sources, so that service can start
for _, source := range sources {
service.Emit(false, source)
}
// when - derive a client for sync service
con, err := grpc.DialContext(ctx, fmt.Sprintf("localhost:%d", port), grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
t.Fatal(fmt.Printf("error creating grpc dial ctx: %v", err))
return
}
serviceClient := syncv1grpc.NewFlagSyncServiceClient(con)
// then
// sync flags request
flags, err := serviceClient.SyncFlags(ctx, &v1.SyncFlagsRequest{})
if err != nil {
t.Fatal(fmt.Printf("error from sync request: %v", err))
return
}
syncRsp, err := flags.Recv()
if err != nil {
t.Fatal(fmt.Printf("stream error: %v", err))
return
}
if len(syncRsp.GetFlagConfiguration()) == 0 {
t.Error("expected non empty sync response, but got empty")
}
// validate emits
dataReceived := make(chan interface{})
go func() {
_, err := flags.Recv()
if err != nil {
return
for _, tc := range testCases {
var testTitle string
if tc.tls {
testTitle = "Testing Sync Service with TLS Connection"
} else {
testTitle = "Testing Sync Service without TLS Connection"
}
t.Run(testTitle, func(t *testing.T) {
// given
port := 18016
store, sources := getSimpleFlagStore()
dataReceived <- nil
}()
service, err := NewSyncService(SvcConfigurations{
Logger: logger.NewLogger(nil, false),
Port: uint16(port),
Sources: sources,
Store: store,
CertPath: tc.certPath,
KeyPath: tc.keyPath,
})
// Emit as a resync
service.Emit(true, "A")
if tc.wantErr {
if err == nil {
t.Fatal("expected error creating the service!")
}
return
} else if err != nil {
t.Fatal("unexpected error creating the service: %w", err)
return
}
select {
case <-dataReceived:
t.Fatal("expected no data as this is a resync")
case <-time.After(1 * time.Second):
break
}
ctx, cancelFunc := context.WithCancel(context.Background())
doneChan := make(chan interface{})
// Emit as a resync
service.Emit(false, "A")
go func() {
// error ignored, tests will fail if start is not successful
_ = service.Start(ctx)
close(doneChan)
}()
select {
case <-dataReceived:
break
case <-time.After(1 * time.Second):
t.Fatal("expected data but timeout waiting for sync")
}
// trigger manual emits matching sources, so that service can start
for _, source := range sources {
service.Emit(false, source)
}
// fetch all flags
allRsp, err := serviceClient.FetchAllFlags(ctx, &v1.FetchAllFlagsRequest{})
if err != nil {
t.Fatal(fmt.Printf("fetch all error: %v", err))
return
}
// when - derive a client for sync service
var con *grpc.ClientConn
if tc.tls {
tlsCredentials, e := loadTLSClientCredentials(tc.clientCertPath)
if e != nil {
log.Fatal("cannot load TLS credentials: ", e)
}
con, err = grpc.Dial(fmt.Sprintf("0.0.0.0:%d", port), grpc.WithTransportCredentials(tlsCredentials))
} else {
con, err = grpc.DialContext(ctx, fmt.Sprintf("localhost:%d", port), grpc.WithTransportCredentials(insecure.NewCredentials()))
}
if err != nil {
t.Fatal(fmt.Printf("error creating grpc dial ctx: %v", err))
return
}
if allRsp.GetFlagConfiguration() != syncRsp.GetFlagConfiguration() {
t.Errorf("expected both sync and fetch all responses to be same, but got %s from sync & %s from fetch all",
syncRsp.GetFlagConfiguration(), allRsp.GetFlagConfiguration())
}
serviceClient := syncv1grpc.NewFlagSyncServiceClient(con)
// metadata request
metadataRsp, err := serviceClient.GetMetadata(ctx, &v1.GetMetadataRequest{})
if err != nil {
t.Fatal(fmt.Printf("metadata error: %v", err))
return
}
// then
asMap := metadataRsp.GetMetadata().AsMap()
// sync flags request
flags, err := serviceClient.SyncFlags(ctx, &v1.SyncFlagsRequest{})
if err != nil {
t.Fatal(fmt.Printf("error from sync request: %v", err))
return
}
// expect `sources` to be present
if asMap["sources"] == nil {
t.Fatal("expected sources entry in the metadata, but got nil")
}
syncRsp, err := flags.Recv()
if err != nil {
t.Fatal(fmt.Printf("stream error: %v", err))
return
}
if asMap["sources"] != "A,B,C" {
t.Fatal("incorrect sources entry in metadata")
}
if len(syncRsp.GetFlagConfiguration()) == 0 {
t.Error("expected non empty sync response, but got empty")
}
// validate shutdown from context cancellation
go func() {
cancelFunc()
}()
// validate emits
dataReceived := make(chan interface{})
go func() {
_, err := flags.Recv()
if err != nil {
return
}
select {
case <-doneChan:
// exit successful
return
case <-time.After(2 * time.Second):
t.Fatal("service did not exist within sufficient timeframe")
dataReceived <- nil
}()
// Emit as a resync
service.Emit(true, "A")
select {
case <-dataReceived:
t.Fatal("expected no data as this is a resync")
case <-time.After(1 * time.Second):
break
}
// Emit as a resync
service.Emit(false, "A")
select {
case <-dataReceived:
break
case <-time.After(1 * time.Second):
t.Fatal("expected data but timeout waiting for sync")
}
// fetch all flags
allRsp, err := serviceClient.FetchAllFlags(ctx, &v1.FetchAllFlagsRequest{})
if err != nil {
t.Fatal(fmt.Printf("fetch all error: %v", err))
return
}
if allRsp.GetFlagConfiguration() != syncRsp.GetFlagConfiguration() {
t.Errorf("expected both sync and fetch all responses to be same, but got %s from sync & %s from fetch all",
syncRsp.GetFlagConfiguration(), allRsp.GetFlagConfiguration())
}
// metadata request
metadataRsp, err := serviceClient.GetMetadata(ctx, &v1.GetMetadataRequest{})
if err != nil {
t.Fatal(fmt.Printf("metadata error: %v", err))
return
}
asMap := metadataRsp.GetMetadata().AsMap()
// expect `sources` to be present
if asMap["sources"] == nil {
t.Fatal("expected sources entry in the metadata, but got nil")
}
if asMap["sources"] != "A,B,C" {
t.Fatal("incorrect sources entry in metadata")
}
// validate shutdown from context cancellation
go func() {
cancelFunc()
}()
select {
case <-doneChan:
// exit successful
return
case <-time.After(2 * time.Second):
t.Fatal("service did not exist within sufficient timeframe")
}
})
}
}

View File

@ -0,0 +1,30 @@
-----BEGIN CERTIFICATE-----
MIIFJTCCAw2gAwIBAgIUHkLmoT3U1jDYbXAqX84fjR/Qw5kwDQYJKoZIhvcNAQEL
BQAwITEfMB0GA1UEAwwWZmxhZ0QgdGVzdCBjZXJ0aWZpY2F0ZTAgFw0yNTAxMDgw
OTI0MThaGA8yMDUyMDUyNTA5MjQxOFowITEfMB0GA1UEAwwWZmxhZ0QgdGVzdCBj
ZXJ0aWZpY2F0ZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALQK8r5J
7wOvdMsbGy63wlOie97lMHT/HSfneyTEnvRM4ZenKNjrKI6zlyYMQX9/RQs9qCxF
QQDFMhLoMrQKkDptVur+PhYCa/uAWUtTFVXJMbJ2Zdzbg86WCgOlRLwMqGiwnu0l
25dk5Ja5nE4HSSg5SipLxtCCdxT3n5YmQBO+Kkyc1Uxgk6yQMQcdQENGhQHazVzy
fAsrutJFZNtkYXObR9t6/MyYgg3zFjNRCpOYhc9misT54TVteb9L0smLqMhVfQkh
CFWkVEWmaeGyTYAR7gGvFy9b7N/45FfBvlumgR7KiG/uLJNadCQtf7v3pRN36SBH
aZleAp8KWmy6N2IMuWx/hMQCVnzoi05gvYkCWJSoseobhiwaXsDFQCc8ZB/c2H9C
yMmyKRL+c3RM7lI/InxLmpS94xJIhDuvDiEPYTWqY3BPqCvAly8LXEYBqgJNsnOa
+pdoJQ/rl87pIdDt3CK/wWQ1GIlp1v7aYY53riM4i0Hvnk6SPNBgpUXbqAZjpnOb
fsIrKGO/FAJ5Hc3mfiuTj15jDoxPPUPqzj4yP2h7WqbdYwEpDvTd0Cd+tjgCuQUj
JU2MpTcNUQQQFdXASZQgfXHOTW873zIx4mourcO7jkEDwId13H9QMU4JPG9ZsLG6
p08S26yx8G0chV/Q7BNVfOTCKAc8GH+94CynAgMBAAGjUzBRMB0GA1UdDgQWBBQs
ubjWSGXffy7mMQGV2EdbdUwtPjAfBgNVHSMEGDAWgBQsubjWSGXffy7mMQGV2Edb
dUwtPjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQBwJyNT3ldS
DsvOOFH5VT+yMByXv690FH7aYBu5VeSpd8oZPJZgzRh58fTzUxrnPk7iyuQzlgCw
ZZssvObDEA1eK9vG1svomY8yu8VrpKG/yl0YItnZpuQTvCfJsNOm1Qe0j8FPJVOu
Jgges3i+QV18o2dmtQKTTWdExu2J30eQgL8SB7hG9vE6tfcVkzCw6+5ev+fyO9kd
DxrN9YY/FKjNMY2WSCjbQ99NFNS3Qf0MFLLy+V3oiYlT1/qkuX9omLIL6qDabuXE
8XbppIG+ku610hiVRd4ZZyTH1EIThqevs7y3zQYFQNDL8wPfYHRTl00Fto/NLcTH
+cAhf5nC5R4PqvSJZ1IZkxUj/8oljUtMsbFKTekgJGnP/VzDFoNmYO6lW3rBFM1L
ovFJhaesnAKQ6WFk+Yru9kFylX2dHqFsB0SlggGS+r8kwbrXlXJhWAE/WpPVyU+H
IUyYluNLKKvMBYa9GqUC6C7XlZmiuemZy0oCy4gHqdmII09Bj3C3nT7+Ms4D21jh
fZKN4WZcpsA2XaE27XsAqJp2gIV+SL3VqD3hAOFIRWAwQVvTlVJ1GWM1DJBVnLpp
BFK4mO0Mga0AbzJ6VL9ElvXVjqwRl8gbeT7yHWQ8A0r7yl4n0i4FloDeoCmr7i9O
9dqUWaknNa6rIUKwvIodkn39C1G7uCpeLw==
-----END CERTIFICATE-----

View File

@ -0,0 +1,17 @@
:'
This script can be used to recreate the SSL certificates that are used in the sync service test
Warning: there might be issues running the script on Windows with the -subj argument
-> workaround: run the commands manually without the -subj argument and provide info when asked by the console output
'
rm *.pem
# 1. Generate CA's private key and self-signed certificate
openssl req -x509 -newkey rsa:4096 -days 9999 -nodes -keyout ca-key.pem -out ca-cert.pem -subj "/CN=flagD test certificate"
# 2. Generate web server's private key and certificate signing request (CSR)
openssl req -newkey rsa:4096 -nodes -keyout server-key.pem -out server-req.pem -subj "/CN=flagD test server PR and CSR"
# 3. Use CA's private key to sign web server's CSR and get back the signed certificate
openssl x509 -req -in server-req.pem -days 60 -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -extfile server-ext.cnf

View File

@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIE6TCCAtGgAwIBAgIUNCaL1+pmzRcfRNGaIgTrD2t3lqswDQYJKoZIhvcNAQEL
BQAwITEfMB0GA1UEAwwWZmxhZ0QgdGVzdCBjZXJ0aWZpY2F0ZTAeFw0yNTAxMDgw
OTI2MTJaFw0yNTAzMDkwOTI2MTJaMCcxJTAjBgNVBAMMHGZsYWdEIHRlc3Qgc2Vy
dmVyIFBSIGFuZCBDU1IwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC3
8cZVWtUihF8lQmmKE8ZG896s7KSqPoNDdqrzp0pebwbErHf/iVarUsl9IWuxxCeG
Oh65yRQfZH8zp39Yf6oAxgiF+sFITkoWrGwB5HbT6qRwUQC5kLytuY9THTJKcpyo
UNTsR46Ne0ix+yM+55nI504tK4U8UdVnQkbnU0tnRlb4LOSA4emDYnCWsl814l1T
Np4h1e7Sb8ppwDju07UQq92Kt4oSZIjHQXeJmOt4vko4c8rv3xfUA12cPSajUEw7
oiohwd50LKtIm4MyG3jisTvie46xIGHD9AJ0AejuwsSSrAUhZ6S+fVwFVdzPpo9C
SKjjrz0fw6FkSeN3Kc61ipYKHf44dsZZB/IQB8CMpLk3cotktu8lwyTDb9URAg0c
PpBLZl7PFY4LgMZrIwsUy5CZbcmkzPRqNVfmfKMCy8D/CNV7jhxSj8FxD8zcTn63
lrbmMoAIZtDZKBqpzRqVkawxVGPjvn4wXVi/CJC5o04l9bWpXJaGLgS0SVtLT8Iz
yWYPXYLadMY7UIn7mqWBRb+x9GHhWGibJLHOD7+oQA+m3/7Sj9fbRHSbKVl8zoRx
Fmn8r2104ym+Cgqh54eUg73lc6D1bXAgMbwJu5qbF9rHn3IpKZSI19UB/2RlsxMX
py8PrGlKEhS1vwiZDyTe5jEzmgfKzzCe+274JP+ZlQIDAQABoxMwETAPBgNVHREE
CDAGhwQAAAAAMA0GCSqGSIb3DQEBCwUAA4ICAQBxCHpqFX3IA+2tE5Sjf+Y3La7q
DogiNDsSj5Ngu3wxs76pbUDepSesI4EmpCnaSsjTfChCd24ZXa5seeoK8P8BTHcm
yAmWy6G03MvFovnuOlxfBlXhhhCywUbevLPHw1Md9+I8OZP13IYF+agv9CktGulp
keYJKcaEvaxS3dLe4GIJC5KVy/hUz47U0yZQbpqlzKkuDQ1RrWoJFh2sSpaPc1Dy
/3QwB1IC+WeP6dkDhxikoPHodpPvcrh5S9/HvihE9AUo682wi9MJ0BfhWt6zDJb/
WJaYQ0J5whS5PtXhdWdobte6Pz7cInusd2OF1BbR2tc/JBB4VfQWUH1ok3AKQ3kq
5yq7+qA/QWeuQiIUmxHtfzfKcfi5QvDgNoNRAp7yHpAXXmYKBfBYF/5JaRon9FHg
LaFXEiexoOYZpJkAPbsU3fiSSC+QRRo1imqnLx3L52BUn0/0Z7sLpBL26nRrr4z9
3LxgKxpPyBX7/Wv3GPMXPkXP5JVqFs6vMUXrAvg/prLJpvIQWvl4/YiIibmKGONy
BK9tYSdV1b/MrTMnzXYgh2/XZ3HaAU7TdQefk3Cs4/sD+l8pRZGvysaxhUoX3Ua2
jrOcfhqlr/XbeBWrHosJB5HVbpgGKJ/Dix6PuRqGcRyi3QgzavUyQ81V7MyhtVMI
7AWcs98UoJz2+7oLbA==
-----END CERTIFICATE-----

View File

@ -0,0 +1 @@
subjectAltName=IP:0.0.0.0

View File

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
MIIJRQIBADANBgkqhkiG9w0BAQEFAASCCS8wggkrAgEAAoICAQC38cZVWtUihF8l
QmmKE8ZG896s7KSqPoNDdqrzp0pebwbErHf/iVarUsl9IWuxxCeGOh65yRQfZH8z
p39Yf6oAxgiF+sFITkoWrGwB5HbT6qRwUQC5kLytuY9THTJKcpyoUNTsR46Ne0ix
+yM+55nI504tK4U8UdVnQkbnU0tnRlb4LOSA4emDYnCWsl814l1TNp4h1e7Sb8pp
wDju07UQq92Kt4oSZIjHQXeJmOt4vko4c8rv3xfUA12cPSajUEw7oiohwd50LKtI
m4MyG3jisTvie46xIGHD9AJ0AejuwsSSrAUhZ6S+fVwFVdzPpo9CSKjjrz0fw6Fk
SeN3Kc61ipYKHf44dsZZB/IQB8CMpLk3cotktu8lwyTDb9URAg0cPpBLZl7PFY4L
gMZrIwsUy5CZbcmkzPRqNVfmfKMCy8D/CNV7jhxSj8FxD8zcTn63lrbmMoAIZtDZ
KBqpzRqVkawxVGPjvn4wXVi/CJC5o04l9bWpXJaGLgS0SVtLT8IzyWYPXYLadMY7
UIn7mqWBRb+x9GHhWGibJLHOD7+oQA+m3/7Sj9fbRHSbKVl8zoRxFmn8r2104ym+
Cgqh54eUg73lc6D1bXAgMbwJu5qbF9rHn3IpKZSI19UB/2RlsxMXpy8PrGlKEhS1
vwiZDyTe5jEzmgfKzzCe+274JP+ZlQIDAQABAoICAQCh2ZHazqaUzYZuYWY9wTKI
gdIfs8Ubqw+Sj9rRsxQjzWtWKC8Z4H0rGBgECyEYdHEWkRMyA7S5/pJSIAJUG1i5
f4ZGZSImfgSAuMv8SksoIeD4lr2dibYK4igzSJBUo04mZ6FCGaBb6utG96PGmMBe
3u+RnSaJsbOlPNLofgjt4R1rFw0kPiNaoIZSgrZ10iytqHQxb2zJKuYecK1nr041
UhQIF4DcuCsFsBv/LVebkUv7Kh+ZOmJcAW4fqErUDjZVjlWmCFC1RgycQYGJ2FRg
mvQHTxJ51fVQFucFrhyH4UZXjBajku+JUQJkC23UJEkPWKGKXUnaJide+Ai2dEnV
Qy84Z/6cLYxBNxFTr1L1+kcdMu163rI+tJhiNyvL3uvuedzI/QBIIJwcy9s5VotK
/36ocdgVhI2xDowJvVVdMDVSsAd573CiwiwPyGqBLKvc90K1M1g/PSn4E4vJmSwh
OmZUoh8NL42MiX3lavf7tqjlWiANOWiI7q+J5jK2a/HAlYQLGnNxm6NYSzfpp5x4
I7QBZJopTwTCgD3JhGCu6JTmYMTbcxjvKpLAFa2WyqOVI0K5Yk9XLaepsqRQsGUL
lam8Gv/vyv+qsMKSh3W2ExDrOI72P8FkyyuzgS2TfHM4hZyJfp18sQGC5HgQk3EM
LbVY1ZXPScFOxYNAm5yUQQKCAQEA2b+GX/YUBkTKsC6zGmIAUTzS6Tprrvpy7Fhr
fJXdKKCPqxjwmDYYK17fBR5I4plASeSBR6OzILa6aioyi4NmNh0TUFuxnZ4pJhl2
I9GwKxLqJCy65TivelRrkxFhzOzZ5PqAWC6QrmmWonI9zlKMfoTbdv0spWbKT0SU
RlX8jCqMKzFZB8WhHuR3vQHex7EizJbSltVRnDUFI9FW9z1/0lYLDdzu3M7/PP71
tR3t36yW98slrJrQ/SXTwx8AEUbpmqNh/fTg1e/eaXs4E9xL+xZ50J3nJjRTMJXn
1UiXY/GLjCE8HVEStpNE5u743VKC1sId2liFFmT9knJR8LepRQKCAQEA2EILQnE7
XA07Bso1ryXVQY8e3j+g+V8uW7AXgmM0x0WlBBtaPzQfj8YsxZB766PHvDdyzfKX
a7Q3hh0jWi/KhE52TgxmvHbssypF+QcBXRbVVxZ0B0jaqzrZADodSN3hFDOUhsKQ
T55sH6Nc+casMEb8EoH0vzkQZV6sI6ggKQ4Oe6IcSMqU+5jpxCkMDvP2MvZxzm2Y
yCyizEFCYHCQLC6cMxxcg6snuUODZtO2o2XTfgLe7Se4RoijrOPHAC8q3qNuX58P
fJrYmw7/k50qkWQK2s0thaKF/uWApj1CLsOxfavdiKBdC8pNHr65RCvsJc1Jj8xN
aJA19XWbBHesEQKCAQEApRAavQO9ikL7ozLDcmx38R06hLJUjwArvh4I3Rh93h5Y
ykrNl5TqHXZ9eVPLzHp/0YP2vGfLkjDyfygdyMSC5uKDkZbwvZr3dno2pFCASya7
d1CxHLIr03/LTGEQ0ld5laqPQEmMQ6qnFd2kHJNXDVGJTFn/TiLtmclS3T6xg099
kgCGjO2zhceLPSv9xULyLkTmvpBWnSNUEiLO2f00uC2hk5C3QYto0MQ1Xmahu70J
dC37ES0K39uc+3y0gGRREXhpACpxhbufzjYp/GQy9NPE4+/PGZbwuRPp+jRdDtY8
Aq3u9ApRNTXONYFSBfRWWpYsKyiPOrqzviALHX8cQQKCAQEAgixXDLqOCZ3pLvAf
GnvCf4EACrXwVstFY2l+7Tx8M4snhm5Uh4D/kpKutol/Hltqyk/yKifhn7JOTctS
UWI9HCECs35hhQZs+nfywLDH0FoDNzXLx+rBvZphrvJMWGU+q+NUfz20kkiBOxYh
zDQbx7+i0h0pzsUxqmMvaRM1sKDGdQMi1Woj/cKQzEQM/x84znpsDN8JvUyo/hw2
MUjwb7fqzBVBVvx6n9kUypub74VGpi5iNAzZrpNnOpWtXt4FhxiHQsXDE7U9tzBz
BU7wpa27nvMseKlY0RMium5bXTzspQIECs7E02kFvQD/EhsCPcrxgb5vxgYwhL0y
/6BtkQKCAQEA0PCdcoVCbhkSPlhrKOeBFtkexcHIlcqBvfWGUzrO9oqXHVncU3JG
F7qxgur5jXGMcJtbzKGzTh8o6nb7dJYkN6ozpgwACWPcuD/uMqMIGr9oTfVWiLdl
whi1MqX6IyA6568HEtDcNjDfdQ8efJ8PQXB5h4DzKj7EC4Z5oPUFMNWbDPMvh7ER
9k07wqe/ZHb3bxB3VVy0jN7fbMP03wFvpiDU2IONekrwx3UYcHXCZbjt4/PuFTVk
++mrDNq4EMXed8oF/Zk+s8wnKqKWWiEwvnZUn9mUwZONJ+PnisW9Xn+Rw3EO97YT
aFIhPf94JnJUQ/J9xwe2MvBIGtpAERp2gw==
-----END PRIVATE KEY-----

View File

@ -1,8 +1,14 @@
package sync
import (
"crypto/tls"
"crypto/x509"
"fmt"
"os"
"github.com/open-feature/flagd/core/pkg/model"
"github.com/open-feature/flagd/core/pkg/store"
"google.golang.org/grpc/credentials"
)
// getSimpleFlagStore returns a flag store pre-filled with flags from sources A & B & C, which C empty
@ -30,3 +36,24 @@ func getSimpleFlagStore() (*store.Flags, []string) {
return flagStore, []string{"A", "B", "C"}
}
func loadTLSClientCredentials(certPath string) (credentials.TransportCredentials, error) {
// Load certificate of the CA who signed server's certificate
pemServerCA, err := os.ReadFile(certPath)
if err != nil {
return nil, fmt.Errorf("failed to read file from path '%s'", certPath)
}
certPool := x509.NewCertPool()
if !certPool.AppendCertsFromPEM(pemServerCA) {
return nil, fmt.Errorf("failed to add server CA's certificate")
}
// Create the credentials and return it
config := &tls.Config{
RootCAs: certPool,
MinVersion: tls.VersionTLS12,
}
return credentials.NewTLS(config), nil
}