Skip to content

Commit

Permalink
feat(security): implement secrets-config proxy tls (edgexfoundry#2930)
Browse files Browse the repository at this point in the history
* feat(security): implement secrets-config proxy tls

Add implementation for secrets-config: proxy tls subcommand
It uploads the user-provided Kong TLS certificate/key pair to proxy server.

Closes: edgexfoundry#2866
Signed-off-by: Jim Wang <yutsung.jim.wang@intel.com>
  • Loading branch information
jim-wang-intel authored and Siggi Skulason committed Mar 30, 2021
1 parent dbfdf27 commit 96f1b22
Show file tree
Hide file tree
Showing 7 changed files with 635 additions and 27 deletions.
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 &quot;localhost,kong&quot;.
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):
&quot;Currently, the only server names supported are DNS hostnames&quot;,
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"
)
267 changes: 258 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,42 @@
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
}

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

cmd := cmd{
loggingClient: lc,
client: secretstoreclient.NewRequestor(lc).Insecure(),
configuration: configuration,
}
var dummy string
Expand All @@ -45,6 +62,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 +80,239 @@ 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
// 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() (certificateIDs, error) {
// definition of Kong snis related objects
// KongCertObj is the structure for part of certificate object returned from Kong's API /snis
type KongCertObj struct {
CertId string `json:"id"`
}

// SniCertObj is the structure for part of snis object returned from Kong's API /snis
type SniCertObj struct {
SnisName string `json:"name"`
KongCert KongCertObj `json:"certificate"`
}

// KongSnisObj is the top level structure for part of json data object returned from Kong's API /snis
type KongSnisObj struct {
Entries []SniCertObj `json:"data,omitempty"`
}

// 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)
}

snisCerts := KongSnisObj{}

switch resp.StatusCode {
case http.StatusOK:
if err := json.NewDecoder(bytes.NewReader(responseBody)).Decode(&snisCerts); 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)
}

serverNameToCertIDs := make(map[string]certificateIDs)
for _, entry := range snisCerts.Entries {
if certIDs, exists := serverNameToCertIDs[entry.SnisName]; !exists {
// initialize for a new sni name
serverNameToCertIDs[entry.SnisName] = []string{entry.KongCert.CertId}
} else {
// update the existing certificateID array
certIDs = append(certIDs, entry.KongCert.CertId)
serverNameToCertIDs[entry.SnisName] = certIDs
}
}

// only get to-be-deleted certificates with which unique certIDs that match the server name indicators
return c.getUniqueCertIDsMatchServerNames(serverNameToCertIDs), 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 (c *cmd) getUniqueCertIDsMatchServerNames(snisToCertIDs map[string]certificateIDs) certificateIDs {
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 := snisToCertIDs[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
}

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

0 comments on commit 96f1b22

Please sign in to comment.