// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package nomad import ( "context" "fmt" "os" "reflect" "strings" "testing" "time" nomadapi "github.com/hashicorp/nomad/api" "github.com/hashicorp/vault/helper/testhelpers" "github.com/hashicorp/vault/helper/testhelpers/docker" "github.com/hashicorp/vault/sdk/logical" "github.com/mitchellh/mapstructure" ) type Config struct { docker.ServiceURL Token string } func (c *Config) APIConfig() *nomadapi.Config { apiConfig := nomadapi.DefaultConfig() apiConfig.Address = c.URL().String() apiConfig.SecretID = c.Token return apiConfig } func (c *Config) Client() (*nomadapi.Client, error) { apiConfig := c.APIConfig() return nomadapi.NewClient(apiConfig) } func prepareTestContainer(t *testing.T, bootstrap bool) (func(), *Config) { if retAddress := os.Getenv("NOMAD_ADDR"); retAddress != "" { s, err := docker.NewServiceURLParse(retAddress) if err != nil { t.Fatal(err) } return func() {}, &Config{*s, os.Getenv("NOMAD_TOKEN")} } runner, err := docker.NewServiceRunner(docker.RunOptions{ ImageRepo: "multani/nomad", ImageTag: "1.1.6", ContainerName: "nomad", Ports: []string{"4646/tcp"}, Cmd: []string{"agent", "-dev"}, Env: []string{`NOMAD_LOCAL_CONFIG=bind_addr = "0.0.0.0" acl { enabled = true }`}, }) if err != nil { t.Fatalf("Could not start docker Nomad: %s", err) } var nomadToken string svc, err := runner.StartService(context.Background(), func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) { var err error nomadapiConfig := nomadapi.DefaultConfig() nomadapiConfig.Address = fmt.Sprintf("http://%s:%d/", host, port) nomad, err := nomadapi.NewClient(nomadapiConfig) if err != nil { return nil, err } _, err = nomad.Status().Leader() if err != nil { t.Logf("[DEBUG] Nomad is not ready yet: %s", err) return nil, err } if bootstrap { aclbootstrap, _, err := nomad.ACLTokens().Bootstrap(nil) if err != nil { return nil, err } nomadToken = aclbootstrap.SecretID t.Logf("[WARN] Generated Master token: %s", nomadToken) } nomadAuthConfig := nomadapi.DefaultConfig() nomadAuthConfig.Address = nomad.Address() if bootstrap { nomadAuthConfig.SecretID = nomadToken nomadAuth, err := nomadapi.NewClient(nomadAuthConfig) if err != nil { return nil, err } err = preprePolicies(nomadAuth) if err != nil { return nil, err } } u, _ := docker.NewServiceURLParse(nomadapiConfig.Address) return &Config{ ServiceURL: *u, Token: nomadToken, }, nil }) if err != nil { t.Fatalf("Could not start docker Nomad: %s", err) } return svc.Cleanup, svc.Config.(*Config) } func preprePolicies(nomadClient *nomadapi.Client) error { policy := &nomadapi.ACLPolicy{ Name: "test", Description: "test", Rules: `namespace "default" { policy = "read" } `, } anonPolicy := &nomadapi.ACLPolicy{ Name: "anonymous", Description: "Deny all access for anonymous requests", Rules: `namespace "default" { policy = "deny" } agent { policy = "deny" } node { policy = "deny" } `, } _, err := nomadClient.ACLPolicies().Upsert(policy, nil) if err != nil { return err } _, err = nomadClient.ACLPolicies().Upsert(anonPolicy, nil) if err != nil { return err } return nil } func TestBackend_config_Bootstrap(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(context.Background(), config) if err != nil { t.Fatal(err) } cleanup, svccfg := prepareTestContainer(t, false) defer cleanup() connData := map[string]interface{}{ "address": svccfg.URL().String(), "token": "", } confReq := &logical.Request{ Operation: logical.UpdateOperation, Path: "config/access", Storage: config.StorageView, Data: connData, } resp, err := b.HandleRequest(context.Background(), confReq) if err != nil || (resp != nil && resp.IsError()) || resp != nil { t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) } confReq.Operation = logical.ReadOperation resp, err = b.HandleRequest(context.Background(), confReq) if err != nil || (resp != nil && resp.IsError()) { t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) } expected := map[string]interface{}{ "address": connData["address"].(string), "max_token_name_length": 0, "ca_cert": "", "client_cert": "", } if !reflect.DeepEqual(expected, resp.Data) { t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data) } nomadClient, err := svccfg.Client() if err != nil { t.Fatalf("failed to construct nomaad client, %v", err) } token, _, err := nomadClient.ACLTokens().Bootstrap(nil) if err == nil { t.Fatalf("expected acl system to be bootstrapped already, but was able to get the bootstrap token : %v", token) } // NOTE: fragile test, but it's the only way, AFAIK, to check that nomad is // bootstrapped if !strings.Contains(err.Error(), "bootstrap already done") { t.Fatalf("expected acl system to be bootstrapped already: err: %v", err) } } func TestBackend_config_access(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(context.Background(), config) if err != nil { t.Fatal(err) } cleanup, svccfg := prepareTestContainer(t, true) defer cleanup() connData := map[string]interface{}{ "address": svccfg.URL().String(), "token": svccfg.Token, } confReq := &logical.Request{ Operation: logical.UpdateOperation, Path: "config/access", Storage: config.StorageView, Data: connData, } resp, err := b.HandleRequest(context.Background(), confReq) if err != nil || (resp != nil && resp.IsError()) || resp != nil { t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) } confReq.Operation = logical.ReadOperation resp, err = b.HandleRequest(context.Background(), confReq) if err != nil || (resp != nil && resp.IsError()) { t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) } expected := map[string]interface{}{ "address": connData["address"].(string), "max_token_name_length": 0, "ca_cert": "", "client_cert": "", } if !reflect.DeepEqual(expected, resp.Data) { t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data) } if resp.Data["token"] != nil { t.Fatalf("token should not be set in the response") } } func TestBackend_config_access_with_certs(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(context.Background(), config) if err != nil { t.Fatal(err) } cleanup, svccfg := prepareTestContainer(t, true) defer cleanup() connData := map[string]interface{}{ "address": svccfg.URL().String(), "token": svccfg.Token, "ca_cert": caCert, "client_cert": clientCert, "client_key": clientKey, } confReq := &logical.Request{ Operation: logical.UpdateOperation, Path: "config/access", Storage: config.StorageView, Data: connData, } resp, err := b.HandleRequest(context.Background(), confReq) if err != nil || (resp != nil && resp.IsError()) || resp != nil { t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) } confReq.Operation = logical.ReadOperation resp, err = b.HandleRequest(context.Background(), confReq) if err != nil || (resp != nil && resp.IsError()) { t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) } expected := map[string]interface{}{ "address": connData["address"].(string), "max_token_name_length": 0, "ca_cert": caCert, "client_cert": clientCert, } if !reflect.DeepEqual(expected, resp.Data) { t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data) } if resp.Data["token"] != nil { t.Fatalf("token should not be set in the response") } } func TestBackend_renew_revoke(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(context.Background(), config) if err != nil { t.Fatal(err) } cleanup, svccfg := prepareTestContainer(t, true) defer cleanup() connData := map[string]interface{}{ "address": svccfg.URL().String(), "token": svccfg.Token, } req := &logical.Request{ Storage: config.StorageView, Operation: logical.UpdateOperation, Path: "config/access", Data: connData, } resp, err := b.HandleRequest(context.Background(), req) if err != nil { t.Fatal(err) } req.Path = "role/test" req.Data = map[string]interface{}{ "policies": []string{"policy"}, "lease": "6h", } resp, err = b.HandleRequest(context.Background(), req) if err != nil { t.Fatal(err) } req.Operation = logical.ReadOperation req.Path = "creds/test" resp, err = b.HandleRequest(context.Background(), req) if err != nil { t.Fatal(err) } if resp == nil { t.Fatal("resp nil") } if resp.IsError() { t.Fatalf("resp is error: %v", resp.Error()) } generatedSecret := resp.Secret generatedSecret.TTL = 6 * time.Hour var d struct { Token string `mapstructure:"secret_id"` Accessor string `mapstructure:"accessor_id"` } if err := mapstructure.Decode(resp.Data, &d); err != nil { t.Fatal(err) } t.Logf("[WARN] Generated token: %s with accessor %s", d.Token, d.Accessor) // Build a client and verify that the credentials work nomadapiConfig := nomadapi.DefaultConfig() nomadapiConfig.Address = connData["address"].(string) nomadapiConfig.SecretID = d.Token client, err := nomadapi.NewClient(nomadapiConfig) if err != nil { t.Fatal(err) } t.Log("[WARN] Verifying that the generated token works...") _, err = client.Agent().Members, nil if err != nil { t.Fatal(err) } req.Operation = logical.RenewOperation req.Secret = generatedSecret resp, err = b.HandleRequest(context.Background(), req) if err != nil { t.Fatal(err) } if resp == nil { t.Fatal("got nil response from renew") } req.Operation = logical.RevokeOperation resp, err = b.HandleRequest(context.Background(), req) if err != nil { t.Fatal(err) } // Build a management client and verify that the token does not exist anymore nomadmgmtConfig := nomadapi.DefaultConfig() nomadmgmtConfig.Address = connData["address"].(string) nomadmgmtConfig.SecretID = connData["token"].(string) mgmtclient, err := nomadapi.NewClient(nomadmgmtConfig) if err != nil { t.Fatal(err) } q := &nomadapi.QueryOptions{ Namespace: "default", } t.Log("[WARN] Verifying that the generated token does not exist...") _, _, err = mgmtclient.ACLTokens().Info(d.Accessor, q) if err == nil { t.Fatal("err: expected error") } } func TestBackend_CredsCreateEnvVar(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(context.Background(), config) if err != nil { t.Fatal(err) } cleanup, svccfg := prepareTestContainer(t, true) defer cleanup() req := logical.TestRequest(t, logical.UpdateOperation, "role/test") req.Data = map[string]interface{}{ "policies": []string{"policy"}, "lease": "6h", } resp, err := b.HandleRequest(context.Background(), req) if err != nil { t.Fatal(err) } os.Setenv("NOMAD_TOKEN", svccfg.Token) defer os.Unsetenv("NOMAD_TOKEN") os.Setenv("NOMAD_ADDR", svccfg.URL().String()) defer os.Unsetenv("NOMAD_ADDR") req.Operation = logical.ReadOperation req.Path = "creds/test" resp, err = b.HandleRequest(context.Background(), req) if err != nil { t.Fatal(err) } if resp == nil { t.Fatal("resp nil") } if resp.IsError() { t.Fatalf("resp is error: %v", resp.Error()) } } func TestBackend_max_token_name_length(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(context.Background(), config) if err != nil { t.Fatal(err) } cleanup, svccfg := prepareTestContainer(t, true) defer cleanup() testCases := []struct { title string roleName string tokenLength int }{ { title: "Default", }, { title: "ConfigOverride", tokenLength: 64, }, { title: "ConfigOverride-LongName", roleName: "testlongerrolenametoexceed64charsdddddddddddddddddddddddd", tokenLength: 64, }, { title: "Notrim", roleName: "testlongersubrolenametoexceed64charsdddddddddddddddddddddddd", }, } for _, tc := range testCases { t.Run(tc.title, func(t *testing.T) { // setup config/access connData := map[string]interface{}{ "address": svccfg.URL().String(), "token": svccfg.Token, "max_token_name_length": tc.tokenLength, } expected := map[string]interface{}{ "address": svccfg.URL().String(), "max_token_name_length": tc.tokenLength, "ca_cert": "", "client_cert": "", } expectedMaxTokenNameLength := maxTokenNameLength if tc.tokenLength != 0 { expectedMaxTokenNameLength = tc.tokenLength } confReq := logical.Request{ Operation: logical.UpdateOperation, Path: "config/access", Storage: config.StorageView, Data: connData, } resp, err := b.HandleRequest(context.Background(), &confReq) if err != nil || (resp != nil && resp.IsError()) || resp != nil { t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) } confReq.Operation = logical.ReadOperation resp, err = b.HandleRequest(context.Background(), &confReq) if err != nil || (resp != nil && resp.IsError()) { t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) } // verify token length is returned in the config/access query if !reflect.DeepEqual(expected, resp.Data) { t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data) } // verify token is not returned if resp.Data["token"] != nil { t.Fatalf("token should not be set in the response") } // create a role to create nomad credentials with // Seeds random with current timestamp if tc.roleName == "" { tc.roleName = "test" } roleTokenName := testhelpers.RandomWithPrefix(tc.roleName) confReq.Path = "role/" + roleTokenName confReq.Operation = logical.UpdateOperation confReq.Data = map[string]interface{}{ "policies": []string{"policy"}, "lease": "6h", } resp, err = b.HandleRequest(context.Background(), &confReq) if err != nil { t.Fatal(err) } confReq.Operation = logical.ReadOperation confReq.Path = "creds/" + roleTokenName resp, err = b.HandleRequest(context.Background(), &confReq) if err != nil { t.Fatal(err) } if resp == nil { t.Fatal("resp nil") } if resp.IsError() { t.Fatalf("resp is error: %v", resp.Error()) } // extract the secret, so we can query nomad directly generatedSecret := resp.Secret generatedSecret.TTL = 6 * time.Hour var d struct { Token string `mapstructure:"secret_id"` Accessor string `mapstructure:"accessor_id"` } if err := mapstructure.Decode(resp.Data, &d); err != nil { t.Fatal(err) } // Build a client and verify that the credentials work nomadapiConfig := nomadapi.DefaultConfig() nomadapiConfig.Address = connData["address"].(string) nomadapiConfig.SecretID = d.Token client, err := nomadapi.NewClient(nomadapiConfig) if err != nil { t.Fatal(err) } // default query options for Nomad queries ... not sure if needed qOpts := &nomadapi.QueryOptions{ Namespace: "default", } // connect to Nomad and verify the token name does not exceed the // max_token_name_length token, _, err := client.ACLTokens().Self(qOpts) if err != nil { t.Fatal(err) } if len(token.Name) > expectedMaxTokenNameLength { t.Fatalf("token name exceeds max length (%d): %s (%d)", expectedMaxTokenNameLength, token.Name, len(token.Name)) } }) } } const caCert = `-----BEGIN CERTIFICATE----- MIIF7zCCA9egAwIBAgIINVVQic4bju8wDQYJKoZIhvcNAQELBQAwaDELMAkGA1UE BhMCVVMxFDASBgNVBAoMC1Vuc3BlY2lmaWVkMR8wHQYDVQQLDBZjYS0zODQzMDY2 NDA5ODI5MjQwNTU5MSIwIAYDVQQDDBl4cHMxNS5sb2NhbC5jaXBoZXJib3kuY29t MB4XDTIyMDYwMjIxMTgxN1oXDTIzMDcwNTIxMTgxN1owaDELMAkGA1UEBhMCVVMx FDASBgNVBAoMC1Vuc3BlY2lmaWVkMR8wHQYDVQQLDBZjYS0zODQzMDY2NDA5ODI5 MjQwNTU5MSIwIAYDVQQDDBl4cHMxNS5sb2NhbC5jaXBoZXJib3kuY29tMIICIjAN BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA35VilgfqMUKhword7wORXRFyPbpz 8uqO7eRaylMnkAkbk5eoQB/iYfXjJ6ZBs5mJGQVz5ZNvh9EzZsk1J6wqYgbwVKUx fh4kvW6sXtDirtb4ZQAK7OTLEoapUQGnGcvm+aEYfvC1sTBl4fbex7yyN5FYMJTM TAUumhdq2pwujaj2xkN9DwZa89Tk7tbj9HE9DTRji7bnciEtrmTAOIOfOrT/1l3x YW1BwYXpQ0TamJ58pC/iNgEp5FAxKt9d3RggesMA7pvG/f8fNgsa/Tku/PeEXNPA +Yx4CcAipujmqpBKiKwJ6TOzp80m2zrZ7Da4Av5vVS5GsNJxhFYD1h8hU1ptK9BS 2CaTwBpV421C9BfEmtSAksGDIWYujfiHb6XNaQrt8Hu85GBuPUudVn0lpoXLn2xD rGK8WEK2gWZ4eez3ZDLbpLui6c1m7AVlMtj374s+LHcD7JIxY475Na7pXmEWReqM RUyCEq1spOOn70fOdhphhmpY6DoklOTOriPawCLNmkPWRnhrIwqyP1gse9YMqQ2n LhWUkv/08m/0pb4e5ijVhsZNzv+1PXPWCk968nzt0BMDgJT+0ZiXsaU7FILXuo7Y Ijgrj7dpXWx2MBdMGPFQdveog7Pa80Yb7r4ERW0DL78TxYC6m/S1p14PHwZpDZzQ LrPrBcpI5XzI7osCAwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCAqQwDAYDVR0TBAUw AwEB/zA0BgNVHR4ELTAroCkwG4IZeHBzMTUubG9jYWwuY2lwaGVyYm95LmNvbTAK hwh/AAAB/wAAADAkBgNVHREEHTAbghl4cHMxNS5sb2NhbC5jaXBoZXJib3kuY29t MB0GA1UdDgQWBBR3bHgDp5RpzerMKRkaGDFN/ZeImjANBgkqhkiG9w0BAQsFAAOC AgEArkuDYYWYHYxIoTeZkQz5L1y0H27ZWPJx5jBBuktPonDLQxBGAwkl6NZbJGLU v+usII+eyjPKIgjhCiTXJAmeIngwWoN3PHOLMIPe9axuNt6qVoP4dQtzfpPR3buK CWj9i3H0ixK73klk7QWZiBUDinYfEMSNRpU3G7NsqmqCXD4s5gB+8y9c7+zIiJyN IaJBWpzI4eQBi/4cBhtM7Xa+CMB/8whhWYR6H+GXGZdNcP5f7bwneMstWKceTadk IEzFucJHDySpEkIA2A9t33pV54FmEp+JVwvxAH4FABCnjPmhg0j1IonWV5pySWpG hhEZpnRRH1XfpTA5i6dlyUA5DJjL8X1lYrgOK+LaoR52mQh5JBsMoVHFzN50DiMA RTsbq4Qzozf23hU1BqW4NOzPTukgSGEcbT/DhXKPPPLL8JD0rPelJPq76X3TJjgZ C9uMnZaDnxjppDXp5oBIXqC05FDxJ5sSODNOpKGyuzOU2qQLMau33yYOgaSAttBk r29+LNFJ+0QzMuPjYXPznpxbsI+lrlZ3F2tDGGs8+JVceC1YX+cBEsEOiqNGTIip /DY3b9gu5oiTwhcFyQW8+WFsirRS/g5t+M40WLKVPdK09z96krFXQMkL6a7LHLY1 n9ivwj+sTG1XmJYXp8naLg4wdzIUf2fJxaFNI5Yq4elZ8sY= -----END CERTIFICATE-----` const clientCert = `-----BEGIN CERTIFICATE----- MIIEsDCCApigAwIBAgIIRY1JBRIynFYwDQYJKoZIhvcNAQELBQAwaDELMAkGA1UE BhMCVVMxFDASBgNVBAoMC1Vuc3BlY2lmaWVkMR8wHQYDVQQLDBZjYS0zODQzMDY2 NDA5ODI5MjQwNTU5MSIwIAYDVQQDDBl4cHMxNS5sb2NhbC5jaXBoZXJib3kuY29t MB4XDTIyMDYwMjIxMTgxOFoXDTIzMDcwNTIxMTgxOFowRzELMAkGA1UEBhMCVVMx FDASBgNVBAoMC1Vuc3BlY2lmaWVkMSIwIAYDVQQDDBl4cHMxNS5sb2NhbC5jaXBo ZXJib3kuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs+XYhsW2 vTwN7gY3xMxgbNN8d3aoeqCswOp05BBf0Vgv3febahm422ubXXd5Mg2UGiU7sJVe 4tUpDeupVVRX5Qr/hpiXgEyfRDAAAJKqrl65KSS62TCbT/eJZ0ah25HV1evI4uM2 0kl5QWhtQjDyaVlTS38YFqXXQvpOuU5DG6UbKnpMcpsCPTyUKEJvJ95ZLcz0HJ8I kIHrnX0Lt0pOhkllj5Nk4cXhU8CFk8IGNz7SVAycrUsffAUMNNEbrIOIfOTPHR1c q3X9hO4/5pt80uIDMFwwumoA7nQR0AhlKkw9SskCIzJhKwKwssQY7fmovNG0fOEd /+vSHK7OsYW+gwIDAQABo38wfTAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYI KwYBBQUHAwIwCQYDVR0TBAIwADAqBgNVHREEIzAhghl4cHMxNS5sb2NhbC5jaXBo ZXJib3kuY29thwR/AAABMB8GA1UdIwQYMBaAFHdseAOnlGnN6swpGRoYMU39l4ia MA0GCSqGSIb3DQEBCwUAA4ICAQBUSP4ZJglCCrYkM5Le7McdvfkM5uYv1aQn0sM4 gbyDEWO0fnv50vLpD3y4ckgHLoD52pAZ0hN8a7rwAUae21GA6DvEchSH5x/yvJiS 7FBlq39sAafe03ZlzDErNYJRkLcnPAqG74lJ1SSsMcs9gCPHM8R7HtNnhAga06L7 K8/G43dsGZCmEb+xcX2B9McCt8jBG6TJPTGafb3BJ0JTmR/tHdoLFIiNwI+qzd2U lMnGlkIApULX8tmIMsWO0rjdiFkPWGcmfn9ChC0iDpQOAcKSDBcZlWrDNpzKk0mK l0TbE6cxcmCUUpiwaXFrbkwVWQw4W0c4b3sWFtWifFbiR1qZ/OT2Y2sHbkbxwvPl PjjXMDBAdRRwtNcTP1E55I5zvwzzBxUpxOob0miorhTJrZR9So0rgv7Roce4ED6M WETYa/mGhe+Q7gBQygIVoryfQLgGBsHC+7V4RDvYTazwZkz9nLQxHLI/TAZU5ofM WqdoUkMd68rxTTEUoMfGbftxjKA0raxGcO7/PjLR3O743EwCqeqYJ7OKWgGRLnui kIKNUJlZ9umURUFzL++Bx4Pr95jWXb2WYqYYQxhDz0oR5q5smnFm5+/1/MLDMvDU TrgBK6pey4QF33B/I55H1+7tGdv85Q57Z8UrNi/IQxR2sFlsOTeCwStpBQ56sdZk Wi4+cQ== -----END CERTIFICATE-----` const clientKey = `-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCz5diGxba9PA3u BjfEzGBs03x3dqh6oKzA6nTkEF/RWC/d95tqGbjba5tdd3kyDZQaJTuwlV7i1SkN 66lVVFflCv+GmJeATJ9EMAAAkqquXrkpJLrZMJtP94lnRqHbkdXV68ji4zbSSXlB aG1CMPJpWVNLfxgWpddC+k65TkMbpRsqekxymwI9PJQoQm8n3lktzPQcnwiQgeud fQu3Sk6GSWWPk2ThxeFTwIWTwgY3PtJUDJytSx98BQw00Rusg4h85M8dHVyrdf2E 7j/mm3zS4gMwXDC6agDudBHQCGUqTD1KyQIjMmErArCyxBjt+ai80bR84R3/69Ic rs6xhb6DAgMBAAECggEAPBcja2kxcCZWNNKo4DiwYMmHwtPE1SlEazAlmWSKzP+b BZbGt/sdj1VzURYuSnTUqqMTPBm41yYCj57PMix5K42v6sKfoIB3lqw94/MZxiLn 0IFvVErzJhP2NqQWPqSI++rFcFwbHMTkFuAN1tVIs73dn9M1NaNxsvKvRyCIM/wz 5YQSDyTkdW4jQM2RvUFOoqwmeyAlQoBRMgQ4bHfLHxmPEjFgw1MAmmG8bJdkupin MVzhZyKj4Fh80Xa2MU4KokijjG41hmYbg/sjNHaHJFDA92Rwq13dhWytrauJDxa/ 3yj8pHWc23Y3hXvRAf/cibDVzXmmLj49W1i06KuUCQKBgQDj5yF/DJV0IOkhfbol +f5AGH4ZrEXA/JwA5SxHU+aKhUuPEqK/LeUWqiy3szFjOz2JOnCC0LMN42nsmMyK sdQEKHp2SPd2wCxsAKZAuxrEi6yBt1mEPFFU5yzvZbdMqYChKJjm9fbRHtuc63s8 PyVw67Ii9o4ij+PxfTobIs18xwKBgQDKE59w3uUDt2uoqNC8x4m5onL2p2vtcTHC CxU57mu1+9CRM8N2BEp2VI5JaXjqt6W4u9ISrmOqmsPgTwosAquKpA/nu3bVvR9g WlN9dh2Xgza0/AFaA9CB++ier8RJq5xFlcasMUmgkhYt3zgKNgRDfjfREWM0yamm P++hAYRcZQKBgHEuYQk6k6J3ka/rQ54GmEj2oPFZB88+5K7hIWtO9IhIiGzGYYK2 ZTYrT0fvuxA/5GCZYDTnNnUoQnuYqsQaamOiQqcpt5QG/kiozegJw9JmV0aYauFs HyweHsfJaQ2uhE4E3mKdNnVGcORuYeZaqdp5gx8v+QibEyXj/g5p60kTAoGBALKp TMOHXmW9yqKwtvThWoRU+13WQlcJSFvuXpL8mCCrBgkLAhqaypb6RV7ksLKdMhk1 fhNkOdxBv0LXvv+QUMhgK2vP084/yrjuw3hecOVfboPvduZ2DuiNp2p9rocQAjeH p8LgRN+Bqbhe7fYhMf3WX1UqEVM/pQ3G43+vjq39AoGAOyD2/hFSIx6BMddUNTHG BEsMUc/DHYslZebbF1zAWnkKdTt+URhtHAFB2tYRDgkZfwW+wr/w12dJTIkX965o HO7tI4FgpU9b0i8FTuwYkBfjwp2j0Xd2/VBR8Qpd17qKl3I6NXDsf3ykjGZAvldH Tll+qwEZpXSRa5OWWTpGV8I= -----END PRIVATE KEY-----`