Skip to content

Commit

Permalink
Merge c9dbc9a into f3337bd
Browse files Browse the repository at this point in the history
  • Loading branch information
foot committed Feb 22, 2022
2 parents f3337bd + c9dbc9a commit 4be468f
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 4 deletions.
1 change: 1 addition & 0 deletions cmd/gitops/cmderrors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ var (
ErrNoClientID = errors.New("the OIDC client ID flag (--oidc-client-id) has not been set")
ErrNoClientSecret = errors.New("the OIDC client secret flag (--oidc-client-secret) has not been set")
ErrNoRedirectURL = errors.New("the OIDC redirect URL flag (--oidc-redirect-url) has not been set")
ErrNoTLSCertOrKey = errors.New("both tls private key and cert must be specified")
)
53 changes: 49 additions & 4 deletions cmd/gitops/ui/run/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/weaveworks/weave-gitops/pkg/kube"
"github.com/weaveworks/weave-gitops/pkg/server"
"github.com/weaveworks/weave-gitops/pkg/server/auth"
"github.com/weaveworks/weave-gitops/pkg/server/tls"
)

// Options contains all the options for the `ui run` command.
Expand All @@ -42,6 +43,10 @@ type Options struct {
LoggingEnabled bool
OIDC OIDCAuthenticationOptions
NotificationControllerAddress string
Insecure bool
TLSCert string
TLSKey string
NoTLS bool
}

// OIDCAuthenticationOptions contains the OIDC authentication options for the
Expand Down Expand Up @@ -78,6 +83,11 @@ func NewCommand() *cobra.Command {
cmd.Flags().StringVar(&options.NotificationControllerAddress, "notification-controller-address", "", "the address of the notification-controller running in the cluster")
cmd.Flags().IntVar(&options.WatcherPort, "watcher-port", 9443, "the port on which the watcher is running")

cmd.Flags().BoolVar(&options.Insecure, "insecure", false, "allow insecure TLS requests")
cmd.Flags().StringVar(&options.TLSCert, "tls-cert-file", "", "filename for the TLS certficate, in-memory generated if omitted")
cmd.Flags().StringVar(&options.TLSKey, "tls-private-key", "", "filename for the TLS key, in-memory generated if omitted")
cmd.Flags().BoolVar(&options.NoTLS, "no-tls", false, "do not attempt to read TLS certificates")

if server.AuthEnabled() {
cmd.Flags().StringVar(&options.OIDC.IssuerURL, "oidc-issuer-url", "", "The URL of the OpenID Connect issuer")
cmd.Flags().StringVar(&options.OIDC.ClientID, "oidc-client-id", "", "The client ID for the OpenID Connect client")
Expand Down Expand Up @@ -194,8 +204,8 @@ func runCmd(cmd *cobra.Command, args []string) error {
return fmt.Errorf("invalid redirect URL: %w", err)
}

var oidcIssueSecureCookies bool
if redirectURL.Scheme == "https" {
oidcIssueSecureCookies := false
if redirectURL.Scheme == "https" && !options.Insecure {
oidcIssueSecureCookies = true
}

Expand Down Expand Up @@ -248,6 +258,7 @@ func runCmd(cmd *cobra.Command, args []string) error {
}))

addr := net.JoinHostPort("0.0.0.0", options.Port)

srv := &http.Server{
Addr: addr,
Handler: mux,
Expand All @@ -256,14 +267,19 @@ func runCmd(cmd *cobra.Command, args []string) error {
go func() {
log.Infof("Serving on port %s", options.Port)

if err := srv.ListenAndServe(); err != nil {
if err := ListenAndServe(srv, options, log); err != nil {
log.Error(err, "server exited")
os.Exit(1)
}
}()

if isatty.IsTerminal(os.Stdout.Fd()) {
url := fmt.Sprintf("http://%s/%s", addr, options.Path)
scheme := "https"
if options.NoTLS {
scheme = "http"
}

url := fmt.Sprintf("%s://%s/%s", scheme, addr, options.Path)

log.Printf("Opening browser at %s", url)

Expand All @@ -290,6 +306,35 @@ func runCmd(cmd *cobra.Command, args []string) error {
return nil
}

func ListenAndServe(srv *http.Server, options Options, log logrus.FieldLogger) error {
if options.NoTLS {
log.Info("TLS connections disabled")
return srv.ListenAndServe()
}

if options.TLSCert == "" && options.TLSKey == "" {
log.Info("TLS cert and key not specified, generating and using in-memory keys")

tlsConfig, err := tls.TLSConfig([]string{"localhost", "0.0.0.0", "127.0.0.1"})
if err != nil {
return fmt.Errorf("failed to generate a TLSConfig: %w", err)
}

srv.TLSConfig = tlsConfig
// if TLSCert and TLSKey are both empty (""), ListenAndServeTLS will ignore
// and happily use the TLSConfig supplied above
return srv.ListenAndServeTLS("", "")
}

if options.TLSCert == "" || options.TLSKey == "" {
return cmderrors.ErrNoTLSCertOrKey
}

log.Infof("Using TLS from %q and %q", options.TLSCert, options.TLSKey)

return srv.ListenAndServeTLS(options.TLSCert, options.TLSKey)
}

//go:embed dist/*
var static embed.FS

Expand Down
12 changes: 12 additions & 0 deletions cmd/gitops/ui/run/cmd_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package run_test

import (
"net/http"
"os"
"testing"

"github.com/go-resty/resty/v2"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/weaveworks/weave-gitops/cmd/gitops/cmderrors"
"github.com/weaveworks/weave-gitops/cmd/gitops/root"
"github.com/weaveworks/weave-gitops/cmd/gitops/ui/run"
)

func TestNoIssuerURL(t *testing.T) {
Expand Down Expand Up @@ -71,3 +74,12 @@ func TestNoRedirectURL(t *testing.T) {
err := cmd.Execute()
assert.ErrorIs(t, err, cmderrors.ErrNoRedirectURL)
}

func TestMissingTLSKeyOrCert(t *testing.T) {
log := logrus.New()
err := run.ListenAndServe(&http.Server{}, run.Options{TLSCert: "foo"}, log)
assert.ErrorIs(t, err, cmderrors.ErrNoTLSCertOrKey)

err = run.ListenAndServe(&http.Server{}, run.Options{TLSKey: "bar"}, log)
assert.ErrorIs(t, err, cmderrors.ErrNoTLSCertOrKey)
}
98 changes: 98 additions & 0 deletions pkg/server/tls/tls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package tls

import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"net"
"time"
)

// TLSConfig is adapted from http.Server.ServeTLS
func TLSConfig(hosts []string) (*tls.Config, error) {
certPEMBlock, keyPEMBlock, err := generateKeyPair(hosts)
if err != nil {
return nil, fmt.Errorf("Failed to generate TLS keys %w", err)
}

cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
if err != nil {
return nil, fmt.Errorf("Failed to generate X509 key pair %w", err)
}

tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
}

return tlsConfig, nil
}

// Adapted from https://go.dev/src/crypto/tls/generate_cert.go
func generateKeyPair(hosts []string) ([]byte, []byte, error) {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, fmt.Errorf("Failing to generate new ecdsa key: %w", err)
}

// A CA is supposed to choose unique serial numbers, that is, unique for the CA.
maxSerialNumber := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, maxSerialNumber)

if err != nil {
return nil, nil, fmt.Errorf("Failed to generate a random serial number: %w", err)
}

template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"Weaveworks"},
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour * 24 * 365),

KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}

for _, h := range hosts {
if ip := net.ParseIP(h); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
template.DNSNames = append(template.DNSNames, h)
}
}

derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
return nil, nil, fmt.Errorf("Failed to create certificate: %w", err)
}

certPEMBlock := &bytes.Buffer{}

err = pem.Encode(certPEMBlock, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
if err != nil {
return nil, nil, fmt.Errorf("Failed to encode cert pem: %w", err)
}

keyPEMBlock := &bytes.Buffer{}

b, err := x509.MarshalECPrivateKey(priv)
if err != nil {
return nil, nil, fmt.Errorf("Unable to marshal ECDSA private key: %v", err)
}

err = pem.Encode(keyPEMBlock, &pem.Block{Type: "EC PRIVATE KEY", Bytes: b})
if err != nil {
return nil, nil, fmt.Errorf("Failed to encode key pem: %w", err)
}

return certPEMBlock.Bytes(), keyPEMBlock.Bytes(), nil
}
90 changes: 90 additions & 0 deletions pkg/server/tls/tls_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package tls_test

import (
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
wegotls "github.com/weaveworks/weave-gitops/pkg/server/tls"
)

func TestGenerateKeyPair(t *testing.T) {
tlsConfig, err := wegotls.TLSConfig([]string{"foo"})
assert.NoError(t, err)

require.Len(t, tlsConfig.Certificates, 1)
cert, err := x509.ParseCertificate(tlsConfig.Certificates[0].Certificate[0])
require.NoError(t, err)

// Make sure DNS name is included
assert.Equal(t, []string{"foo"}, cert.DNSNames)
// Important for Chrome that we mark it for server auth
assert.Equal(t, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, cert.ExtKeyUsage)
// Serial number should be unique, (sorry if you randomly get 1!)
assert.NotEqual(t, 1, cert.SerialNumber, "Maybe you randomly did get a 1?")
}

func TestTLSConfigCanBeServed(t *testing.T) {
tlsConfig, err := wegotls.TLSConfig([]string{"127.0.0.1"})
require.NoError(t, err)

ts, client := getTestClient(t, tlsConfig)
ts.StartTLS()

defer ts.Close()

res, err := client.Get(ts.URL)
require.NoError(t, err)
greeting, err := io.ReadAll(res.Body)
require.NoError(t, err)
res.Body.Close()
assert.Equal(t, "hello", string(greeting))
}

func TestTLSHostnameIsChecked(t *testing.T) {
tlsConfig, err := wegotls.TLSConfig([]string{"123.123.123.123"})
require.NoError(t, err)

ts, client := getTestClient(t, tlsConfig)
ts.StartTLS()

defer ts.Close()

_, err = client.Get(ts.URL)
require.Regexp(t, "x509: certificate is valid for 123\\.123\\.123\\.123, not 127\\.0\\.0\\.1", err)
}

func getTestClient(t *testing.T, tlsConfig *tls.Config) (*httptest.Server, http.Client) {
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "hello")
}))

ts.TLS = tlsConfig

certs := x509.NewCertPool()

for _, c := range tlsConfig.Certificates {
roots, err := x509.ParseCertificates(c.Certificate[len(c.Certificate)-1])
require.NoError(t, err)

for _, root := range roots {
certs.AddCert(root)
}
}

client := http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: certs,
},
},
}

return ts, client
}

0 comments on commit 4be468f

Please sign in to comment.