Skip to content

Commit

Permalink
add min-tls and ciphers-suite application options
Browse files Browse the repository at this point in the history
Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>

fix typos

Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>

add test of TLS configuration

Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>
  • Loading branch information
arsenalzp committed Feb 13, 2025
1 parent f53b818 commit b006b1c
Show file tree
Hide file tree
Showing 5 changed files with 321 additions and 0 deletions.
47 changes: 47 additions & 0 deletions cmd/trust-manager/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ limitations under the License.
package app

import (
"crypto/tls"
"errors"
"fmt"
"net/http"
"slices"

"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -89,6 +91,21 @@ func NewCommand() *cobra.Command {
Port: opts.Webhook.Port,
Host: opts.Webhook.Host,
CertDir: opts.Webhook.CertDir,
TLSOpts: []func(*tls.Config){
func(c *tls.Config) {
// Get Minimum TLS version from CLI arguments
c.MinVersion = getMinTLSVersionByName(opts.MinTLSVersion)
// Note that TLS 1.3 ciphersuites are not configurable.
if c.MinVersion == tls.VersionTLS13 {
return
}

var cipherSuiteList []*tls.CipherSuite
cipherSuiteList = append(cipherSuiteList, tls.CipherSuites()...)
cipherSuiteList = append(cipherSuiteList, tls.InsecureCipherSuites()...)
c.CipherSuites = getCipherSuiteByNames(opts.CiphersSuite, cipherSuiteList...)
},
},
}),
Metrics: server.Options{
BindAddress: fmt.Sprintf("0.0.0.0:%d", opts.MetricsPort),
Expand Down Expand Up @@ -173,3 +190,33 @@ func NewCommand() *cobra.Command {

return cmd
}

// Get Minimum TLS version Id from its name
func getMinTLSVersionByName(versionName string) uint16 {
var minVersion uint16
switch {
case versionName == "tls10":
minVersion = tls.VersionTLS10
case versionName == "tls11":
minVersion = tls.VersionTLS11
case versionName == "tls12":
minVersion = tls.VersionTLS12
case versionName == "tls13":
minVersion = tls.VersionTLS13
}

return minVersion
}

// Get Cipher IDs from their names
func getCipherSuiteByNames(names []string, cipherSuiteList ...*tls.CipherSuite) []uint16 {
var cipherSuiteIDs []uint16 = make([]uint16, 0)

for _, s := range cipherSuiteList {
if slices.Contains(names, s.Name) {
cipherSuiteIDs = append(cipherSuiteIDs, s.ID)
}
}

return cipherSuiteIDs
}
230 changes: 230 additions & 0 deletions cmd/trust-manager/app/ciphers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package app

import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"log"
"math/big"
"net"
"net/http"
"os"
"path"
"strings"
"testing"
"time"

"github.com/cert-manager/trust-manager/cmd/trust-manager/app/options"
"github.com/spf13/cobra"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)

func TestCiphersSuite(t *testing.T) {
tmpDir := t.TempDir()
setupWebHookServer(tmpDir)

t.Run("TLS 1.2 client connect to TLS 1.2 server, ciphers suite supported", func(t *testing.T) {
client := http.Client{
Transport: &http.Transport{
ForceAttemptHTTP2: false,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
},
},
},
}

_, err := client.Get(fmt.Sprintf("https://%s:%d", "localhost", 6443))
if err != nil {
t.Fatalf("error to connect to webhook server, %s\n", err)
}
})

t.Run("TLS 1.2 client connect to TLS 1.2 server, ciphers suite unsupported", func(t *testing.T) {
client := http.Client{
Transport: &http.Transport{
ForceAttemptHTTP2: false,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
},
},
},
}

_, err := client.Get(fmt.Sprintf("https://%s:%d", "localhost", 6443))
if err != nil {
return
}
})
}

func setupWebHookServer(tmpDir string) error {
err := genPrivAndCert(tmpDir)
if err != nil {
fmt.Printf("unable to generate certificate and privkey %s\n", err)
return err
}

args := []string{
"--readiness-probe-port=9443",
"--readiness-probe-path=/readyz",
"--leader-election-lease-duration=15s",
"--leader-election-renew-deadline=10s",
"--metrics-port=9402",
"--trust-namespace=cert-manager",
"--secret-targets-enabled=false",
"--filter-expired-certificates=false",
"--webhook-host=0.0.0.0",
"--webhook-port=6443",
"--min-tls=tls12",
"--ciphers-suite=TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_RSA_WITH_AES_128_GCM_SHA256",
}

opts := options.New()

cmd := &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
srv := webhook.NewServer(webhook.Options{
CertDir: tmpDir,
Port: opts.Port,

TLSOpts: []func(*tls.Config){
func(cfg *tls.Config) {
cfg.MinVersion = getMinTLSVersionByName(opts.MinTLSVersion)

var cipherSuiteList []*tls.CipherSuite
cipherSuiteList = append(cipherSuiteList, tls.CipherSuites()...)
cipherSuiteList = append(cipherSuiteList, tls.InsecureCipherSuites()...)
cfg.CipherSuites = getCipherSuiteByNames(opts.CiphersSuite, cipherSuiteList...)
},
},
})

if err != nil {
cmd.PrintErrf("error creating webhook server %s\n", err)
return err
}

go srv.Start(cmd.Context())

return nil
},
}
cmd.SetArgs(args)
opts.Prepare(cmd)
err = cmd.Execute()
if err != nil {
cmd.PrintErrf("error: %v\n", err)
return err
}
return nil
}

// This implementation was taken as is from
// https://go.dev/src/crypto/tls/generate_cert.go source file
func genPrivAndCert(dir string) error {
var priv any
var err error
priv, err = rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return fmt.Errorf("Failed to generate private key: %v", err)
}

keyUsage := x509.KeyUsageDigitalSignature
if _, isRSA := priv.(*rsa.PrivateKey); isRSA {
keyUsage |= x509.KeyUsageKeyEncipherment
}

var notBefore = time.Now()
notAfter := notBefore.Add(365 * 24 * time.Hour)

serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return fmt.Errorf("Failed to generate serial number: %v", err)
}

template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"Acme Co"},
},
NotBefore: notBefore,
NotAfter: notAfter,

KeyUsage: keyUsage,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}

hosts := strings.Split("localhost, 127.0.0.1", ",")
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, publicKey(priv), priv)
if err != nil {
return fmt.Errorf("Failed to create certificate: %v", err)
}

certOut, err := os.Create(path.Join(dir, "tls.crt"))
if err != nil {
return fmt.Errorf("Failed to open cert.pem for writing: %v", err)
}
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
return fmt.Errorf("Failed to write data to tls.crt: %v", err)
}
if err := certOut.Close(); err != nil {
return fmt.Errorf("Error closing tls.crt: %v", err)
}
log.Print("wrote tls.crt\n")

keyOut, err := os.OpenFile(path.Join(dir, "tls.key"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return fmt.Errorf("Failed to open tls.key for writing: %v", err)
}
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return fmt.Errorf("Unable to marshal private key: %v", err)
}
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
return fmt.Errorf("Failed to write data to tls.key: %v", err)
}
if err := keyOut.Close(); err != nil {
return fmt.Errorf("Error closing tls.key: %v", err)
}
log.Print("wrote tls.key\n")
return nil
}

func publicKey(priv any) any {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &k.PublicKey
case *ecdsa.PrivateKey:
return &k.PublicKey
case ed25519.PrivateKey:
return k.Public().(ed25519.PublicKey)
default:
return nil
}
}
26 changes: 26 additions & 0 deletions cmd/trust-manager/app/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package options

import (
"crypto/tls"
"errors"
"fmt"
"log/slog"
Expand Down Expand Up @@ -80,6 +81,20 @@ type Options struct {
log logOptions

LeaderElectionConfig LeaderElectionConfig

// Leader election lease duration
LeaseDuration time.Duration

// Leader election lease renew duration
RenewDeadline time.Duration

// Minimum TLS version
MinTLSVersion string

// Ciphers Suite
CiphersSuite []string

TLSConfig tls.Config
}

type logOptions struct {
Expand Down Expand Up @@ -173,6 +188,7 @@ func (o *Options) addFlags(cmd *cobra.Command) {
o.addBundleFlags(nfs.FlagSet("Bundle"))
o.addLoggingFlags(nfs.FlagSet("Logging"))
o.addWebhookFlags(nfs.FlagSet("Webhook"))
o.addTLSConfigFlags(nfs.FlagSet("TLSConfig"))
o.kubeConfigFlags = genericclioptions.NewConfigFlags(true)
o.kubeConfigFlags.AddFlags(nfs.FlagSet("Kubernetes"))

Expand Down Expand Up @@ -261,3 +277,13 @@ func (o *Options) addWebhookFlags(fs *pflag.FlagSet) {
"Certificate and private key must be named 'tls.crt' and 'tls.key' "+
"respectively.")
}

func (o *Options) addTLSConfigFlags(fs *pflag.FlagSet) {
fs.StringVar(&o.MinTLSVersion,
"min-tls", "",
"MinVersion contains the minimum TLS version that is acceptable.")

fs.StringSliceVar(&o.CiphersSuite,
"ciphers-suite", nil,
"CipherSuites is a list of enabled TLS 1.0–1.2 cipher suites.")
}
2 changes: 2 additions & 0 deletions deploy/charts/trust-manager/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ spec:
initialDelaySeconds: 3
periodSeconds: 7
args:
- "--min-tls={{.Values.app.minTLSVersion}}"
- "--ciphers-suite={{.Values.app.ciphersSuite}}"
- "--log-format={{.Values.app.logFormat}}"
- "--log-level={{.Values.app.logLevel}}"
- "--metrics-port={{.Values.app.metrics.port}}"
Expand Down
Loading

0 comments on commit b006b1c

Please sign in to comment.