Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(security): implement secrets-config proxy tls #2930

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions cmd/secrets-config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ Proxy configuration commands (listed below) require access to the secret store m

Path to TLS private key (PEM-encoded).

* **--snis** _comma_separated_list_for_server_names_ (optional)

A comma separated extra server DNS names in addition to the built-in
server name indications. The built-in names are "localhost,kong".
These names will be associated with the user-provided certificate for Kong's TLS to use.
Based on the specification [RFC4366](https://tools.ietf.org/html/rfc4366):
"Currently, the only server names supported are DNS hostnames",
so the IP address-based input is not allowed.

* **adduser**

Create an API gateway user using specified token type. Requires additional arguments:
Expand Down
12 changes: 6 additions & 6 deletions internal/security/config/command/proxy/adduser/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"strings"

"github.com/edgexfoundry/edgex-go/internal"
"github.com/edgexfoundry/edgex-go/internal/security/config/command/proxy/common"
"github.com/edgexfoundry/edgex-go/internal/security/config/interfaces"
"github.com/edgexfoundry/edgex-go/internal/security/proxy/config"
"github.com/edgexfoundry/edgex-go/internal/security/secretstoreclient"
Expand All @@ -27,8 +28,7 @@ import (
)

const (
CommandName string = "adduser"
urlEncodedForm string = "application/x-www-form-urlencoded"
CommandName string = "adduser"
)

type cmd struct {
Expand Down Expand Up @@ -134,7 +134,7 @@ func (c *cmd) createConsumer() error {
if err != nil {
return fmt.Errorf("Failed to prepare new consumer request %s: %w", c.username, err)
}
req.Header.Add(clients.ContentType, urlEncodedForm)
req.Header.Add(clients.ContentType, common.UrlEncodedForm)
resp, err := c.client.Do(req)
if err != nil {
return fmt.Errorf("Failed to send new consumer request %s: %w", c.username, err)
Expand Down Expand Up @@ -174,7 +174,7 @@ func (c *cmd) addUserToGroup() error {
if err != nil {
return fmt.Errorf("Failed to build request to associate consumer %s to group %s: %w", c.username, c.group, err)
}
req.Header.Add(clients.ContentType, urlEncodedForm)
req.Header.Add(clients.ContentType, common.UrlEncodedForm)
resp, err := c.client.Do(req)
if err != nil {
return fmt.Errorf("Failed to submit request to associate consumer %s to group %s: %w", c.username, c.group, err)
Expand Down Expand Up @@ -234,7 +234,7 @@ func (c *cmd) ExecuteAddJwt() (int, error) {
if err != nil {
return interfaces.StatusCodeExitWithError, fmt.Errorf("Failed to prepare request to associate JWT to user %s: %w", c.username, err)
}
req.Header.Add(clients.ContentType, urlEncodedForm)
req.Header.Add(clients.ContentType, common.UrlEncodedForm)
resp, err := c.client.Do(req)
if err != nil {
return interfaces.StatusCodeExitWithError, fmt.Errorf("Failed to send request to associate JWT to user %s: %w", c.username, err)
Expand Down Expand Up @@ -298,7 +298,7 @@ func (c *cmd) ExecuteAddOAuth2() (statusCode int, err error) {
if err != nil {
return interfaces.StatusCodeExitWithError, fmt.Errorf("Failed to prepare request to create oauth application %s: %w", c.username, err)
}
req.Header.Add(clients.ContentType, urlEncodedForm)
req.Header.Add(clients.ContentType, common.UrlEncodedForm)
resp, err := c.client.Do(req)
if err != nil {
return interfaces.StatusCodeExitWithError, fmt.Errorf("Failed to send request to create oauth application %s: %w", c.username, err)
Expand Down
12 changes: 12 additions & 0 deletions internal/security/config/command/proxy/common/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// Copyright (c) 2020 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0'
//

package common

const (
// UrlEncodedForm is the content type for web form data being encoded
UrlEncodedForm = "application/x-www-form-urlencoded"
)
266 changes: 257 additions & 9 deletions internal/security/config/command/proxy/tls/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,45 @@
package tls

import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"

"github.com/edgexfoundry/edgex-go/internal"
"github.com/edgexfoundry/edgex-go/internal/security/config/command/proxy/common"
"github.com/edgexfoundry/edgex-go/internal/security/config/interfaces"
"github.com/edgexfoundry/edgex-go/internal/security/proxy/config"
"github.com/edgexfoundry/edgex-go/internal/security/secretstoreclient"
bootstrapConfig "github.com/edgexfoundry/go-mod-bootstrap/config"

"github.com/edgexfoundry/go-mod-core-contracts/clients"
"github.com/edgexfoundry/go-mod-core-contracts/clients/logger"
)

const (
CommandName = "tls"

builtinSNISList = "localhost,kong"
)

type certificateIDs []string

type cmd struct {
loggingClient logger.LoggingClient
configuration *config.ConfigurationStruct
certificatePath string
privateKeyPath string
loggingClient logger.LoggingClient
client internal.HttpCaller
configuration *config.ConfigurationStruct
certificatePath string
privateKeyPath string
serverNameIndicatorList string

// states related to certificates and snis
serverNameToCertIDs map[string]certificateIDs
}

func NewCommand(
Expand All @@ -36,6 +55,7 @@ func NewCommand(

cmd := cmd{
loggingClient: lc,
client: secretstoreclient.NewRequestor(lc).Insecure(),
configuration: configuration,
}
var dummy string
Expand All @@ -45,6 +65,8 @@ func NewCommand(

flagSet.StringVar(&cmd.certificatePath, "incert", "", "Path to PEM-encoded leaf certificate")
flagSet.StringVar(&cmd.privateKeyPath, "inkey", "", "Path to PEM-encoded private key")
flagSet.StringVar(&cmd.serverNameIndicatorList, "snis", "",
"[Optional] comma-separated extra server name indications list to associate with this certificate")

err := flagSet.Parse(args)
if err != nil {
Expand All @@ -61,9 +83,235 @@ func NewCommand(
}

func (c *cmd) Execute() (statusCode int, err error) {
fmt.Println("TODO: Configure inbound TLS certificate.")
fmt.Printf("--incert %s\n", c.certificatePath)
fmt.Printf("--inkey %s\n", c.privateKeyPath)
err = fmt.Errorf("tls command is unimplemented")
return interfaces.StatusCodeExitWithError, err

if err := c.uploadProxyTlsCert(); err != nil {
return interfaces.StatusCodeExitWithError, err
}

return
}

func (c *cmd) readCertKeyPairFromFiles() (*bootstrapConfig.CertKeyPair, error) {
certPem, err := ioutil.ReadFile(c.certificatePath)
if err != nil {
return nil, fmt.Errorf("Failed to read TLS certificate from file %s: %w", c.certificatePath, err)
}
prvKey, err := ioutil.ReadFile(c.privateKeyPath)
if err != nil {
return nil, fmt.Errorf("Failed to read private key from file %s: %w", c.privateKeyPath, err)
}
return &bootstrapConfig.CertKeyPair{Cert: string(certPem), Key: string(prvKey)}, nil
}

func (c *cmd) uploadProxyTlsCert() error {
// try to read both files and make sure they are existing
certKeyPair, err := c.readCertKeyPairFromFiles()
if err != nil {
return err
}

// to see if any proxy certificates already exists
// if yes, then delete them all first
// and then upload the new TLS certificate
existingCertIDs, err := c.listKongTLSCertificates()
if err != nil {
return err
}

c.loggingClient.Debug(fmt.Sprintf("number of existing Kong tls certs = %d", len(existingCertIDs)))

if len(existingCertIDs) > 0 {
// delete the existing certs first
// ideally, it should only have one certificate to be deleted
// Disclaimer: Kong TLS certificate should only be uploaded via secret-config utility
for _, certID := range existingCertIDs {
if err := c.deleteKongTLSCertificateById(certID); err != nil {
return err
}
}
}

// post the certKeyPair as the new one
if err := c.postKongTLSCertificate(certKeyPair); err != nil {
return err
}

return nil
}

func (c *cmd) listKongTLSCertificates() ([]string, error) {
// list snis certificates association to see if any already exists
certKongURL := strings.Join([]string{c.configuration.KongURL.GetProxyBaseURL(), "snis"}, "/")
c.loggingClient.Info(fmt.Sprintf("list snis tls certificates on the endpoint of %s", certKongURL))
req, err := http.NewRequest(http.MethodGet, certKongURL, http.NoBody)
if err != nil {
return nil, fmt.Errorf("Failed to prepare request to list Kong snis tls certs: %w", err)
}
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("Failed to send request to list Kong snis tls certs: %w", err)
}
defer resp.Body.Close()

responseBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("Failed to read response body to list Kong snis tls certs: %w", err)
}

var parsedResponse map[string]interface{}

switch resp.StatusCode {
case http.StatusOK:
if err := json.NewDecoder(bytes.NewReader(responseBody)).Decode(&parsedResponse); err != nil {
return nil, fmt.Errorf("Unable to parse response from list snis certificate: %w", err)
}
default:
return nil, fmt.Errorf("List Kong tls snis certificates request failed with code: %d", resp.StatusCode)
}

var jsonData []byte
jsonData, err = json.Marshal(parsedResponse["data"])
if err != nil {
return nil, fmt.Errorf("Failed to json marshal parsed response data: %w", err)
}

// the list snis get API returns an array of snis-certs association
var parsedSnisCertData []map[string]interface{}
if err := json.NewDecoder(bytes.NewReader(jsonData)).Decode(&parsedSnisCertData); err != nil {
return nil, fmt.Errorf("Unable to parse response for parsed SNIS cert data: %w", err)
}
jim-wang-intel marked this conversation as resolved.
Show resolved Hide resolved

c.serverNameToCertIDs = make(map[string]certificateIDs)
for _, sniCerts := range parsedSnisCertData {
// extract sni name and certificate id from parsed json data
sni := fmt.Sprintf("%s", sniCerts["name"])
certID := fmt.Sprintf("%s", sniCerts["certificate"].(map[string]interface{})["id"])

if certIDs, exists := c.serverNameToCertIDs[sni]; !exists {
// initialize for a new sni name
c.serverNameToCertIDs[sni] = []string{certID}
} else {
// update the existing certificateID array
certIDs = append(certIDs, certID)
c.serverNameToCertIDs[sni] = certIDs
}
}

uniqueSnisFromInput := getServerNameIndicators(c.serverNameIndicatorList)
// collect the unique certificate ids that match the given snis from the input to be deleted
uniqueCertIDs := make(map[string]bool)
for _, sniFromInput := range uniqueSnisFromInput {
if certIDs, exists := c.serverNameToCertIDs[sniFromInput]; exists {
for _, certID := range certIDs {
uniqueCertIDs[certID] = true
}
}
}

// get the unique certificate ids
certIDs := make([]string, 0, len(uniqueCertIDs))
for certID := range uniqueCertIDs {
certIDs = append(certIDs, certID)
}

return certIDs, nil
}

func (c *cmd) deleteKongTLSCertificateById(certId string) error {
delCertKongURL := strings.Join([]string{c.configuration.KongURL.GetProxyBaseURL(),
"certificates", certId}, "/")
c.loggingClient.Info(fmt.Sprintf("deleting tls certificate on the endpoint of %s", delCertKongURL))
req, err := http.NewRequest(http.MethodDelete, delCertKongURL, http.NoBody)
if err != nil {
return fmt.Errorf("Failed to prepare request to delete Kong tls cert: %w", err)
}
resp, err := c.client.Do(req)
if err != nil {
return fmt.Errorf("Failed to send request to delete Kong tls cert: %w", err)
}
defer resp.Body.Close()

switch resp.StatusCode {
case http.StatusNoContent:
c.loggingClient.Info("Successfully deleted Kong tls cert")
case http.StatusNotFound:
// not able to find this certId but should be ok to proceed and post a new certificate
c.loggingClient.Warn(fmt.Sprintf("Unable to delete Kong tls cert because the certificate Id %s not found", certId))
default:
return fmt.Errorf("Delete Kong tls certificate request failed with code: %d", resp.StatusCode)
}
return nil
}

func (c *cmd) postKongTLSCertificate(certKeyPair *bootstrapConfig.CertKeyPair) error {
postCertKongURL := strings.Join([]string{c.configuration.KongURL.GetProxyBaseURL(),
"certificates"}, "/")
c.loggingClient.Info(fmt.Sprintf("posting tls certificate on the endpoint of %s", postCertKongURL))

form := url.Values{
"cert": []string{certKeyPair.Cert},
"key": []string{certKeyPair.Key},
"snis": getServerNameIndicators(c.serverNameIndicatorList),
}

formVal := form.Encode()
req, err := http.NewRequest(http.MethodPost, postCertKongURL, strings.NewReader(formVal))
if err != nil {
return fmt.Errorf("Failed to prepare request to post Kong tls cert: %w", err)
}

req.Header.Add(clients.ContentType, common.UrlEncodedForm)
resp, err := c.client.Do(req)
if err != nil {
return fmt.Errorf("Failed to send request to post Kong tls cert: %w", err)
}
defer resp.Body.Close()
responseBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("Failed to read response body: %v", err)
}

switch resp.StatusCode {
case http.StatusCreated:
c.loggingClient.Info("Successfully posted Kong tls cert")
case http.StatusBadRequest:
return fmt.Errorf("BadRequest as unable to post Kong tls cert due to error: %s", string(responseBody))
default:
return fmt.Errorf("Post Kong tls certificate request failed with code: %d", resp.StatusCode)
}
return nil
}

func getServerNameIndicators(snisList string) []string {
// this will parse out the internal default server name indications and extra ones from input if given
var snis []string
snis = append(snis, parseAndTrimSpaces(builtinSNISList)...)

uniqueSnisMap := make(map[string]bool)
for _, s := range snis {
uniqueSnisMap[s] = true
}

// sanitize the user given list
if len(snisList) > 0 {
splitSnis := parseAndTrimSpaces(snisList)
for _, sni := range splitSnis {
if _, exists := uniqueSnisMap[sni]; !exists {
uniqueSnisMap[sni] = true
snis = append(snis, sni)
}
}
}
return snis
}

func parseAndTrimSpaces(commaSep string) (ret []string) {
splitStrs := strings.Split(commaSep, ",")
for _, s := range splitStrs {
trimmed := strings.TrimSpace(s)
if len(trimmed) > 0 { // effective only it is non-empty string
ret = append(ret, trimmed)
}
}
return
}
Loading