ContainerImage.Pinniped/test/integration/securetls_test.go
Monis Khan cd686ffdf3
Force the use of secure TLS config
This change updates the TLS config used by all pinniped components.
There are no configuration knobs associated with this change.  Thus
this change tightens our static defaults.

There are four TLS config levels:

1. Secure (TLS 1.3 only)
2. Default (TLS 1.2+ best ciphers that are well supported)
3. Default LDAP (TLS 1.2+ with less good ciphers)
4. Legacy (currently unused, TLS 1.2+ with all non-broken ciphers)

Highlights per component:

1. pinniped CLI
   - uses "secure" config against KAS
   - uses "default" for all other connections
2. concierge
   - uses "secure" config as an aggregated API server
   - uses "default" config as a impersonation proxy API server
   - uses "secure" config against KAS
   - uses "default" config for JWT authenticater (mostly, see code)
   - no changes to webhook authenticater (see code)
3. supervisor
   - uses "default" config as a server
   - uses "secure" config against KAS
   - uses "default" config against OIDC IDPs
   - uses "default LDAP" config against LDAP IDPs

Signed-off-by: Monis Khan <mok@vmware.com>
2021-11-17 16:55:35 -05:00

258 lines
7.7 KiB
Go

// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package integration
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"fmt"
"net/http"
"os/exec"
"regexp"
"sort"
"strconv"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"go.pinniped.dev/internal/crypto/ptls"
"go.pinniped.dev/internal/testutil/tlsserver"
"go.pinniped.dev/test/testlib"
)
// TLS checks safe to run in parallel with serial tests, see main_test.go.
func TestSecureTLSPinnipedCLIToKAS_Parallel(t *testing.T) {
_ = testlib.IntegrationEnv(t)
server := tlsserver.TLSTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tlsserver.AssertTLS(t, r, ptls.Secure) // pinniped CLI uses ptls.Secure when talking to KAS
w.Header().Set("content-type", "application/json")
fmt.Fprint(w, `{"kind":"TokenCredentialRequest","apiVersion":"login.concierge.pinniped.dev/v1alpha1",`+
`"status":{"credential":{"token":"some-fancy-token"}}}`)
}), tlsserver.RecordTLSHello)
ca := tlsserver.TLSTestServerCA(server)
pinnipedExe := testlib.PinnipedCLIPath(t)
stdout, stderr := runPinnipedCLI(t, nil, pinnipedExe, "login", "static",
"--token", "does-not-matter",
"--concierge-authenticator-type", "webhook",
"--concierge-authenticator-name", "does-not-matter",
"--concierge-ca-bundle-data", base64.StdEncoding.EncodeToString(ca),
"--concierge-endpoint", server.URL,
"--enable-concierge",
"--credential-cache", "",
)
require.Empty(t, stderr)
require.Equal(t, `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1",`+
`"spec":{"interactive":false},"status":{"expirationTimestamp":null,"token":"some-fancy-token"}}
`, stdout)
}
// TLS checks safe to run in parallel with serial tests, see main_test.go.
func TestSecureTLSPinnipedCLIToSupervisor_Parallel(t *testing.T) {
_ = testlib.IntegrationEnv(t)
server := tlsserver.TLSTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tlsserver.AssertTLS(t, r, ptls.Default) // pinniped CLI uses ptls.Default when talking to supervisor
w.Header().Set("content-type", "application/json")
fmt.Fprint(w, `{"issuer":"https://not-a-good-issuer"}`)
}), tlsserver.RecordTLSHello)
ca := tlsserver.TLSTestServerCA(server)
pinnipedExe := testlib.PinnipedCLIPath(t)
stdout, stderr := runPinnipedCLI(&fakeT{T: t}, nil, pinnipedExe, "login", "oidc",
"--ca-bundle-data", base64.StdEncoding.EncodeToString(ca),
"--issuer", server.URL,
"--credential-cache", "",
"--upstream-identity-provider-flow", "cli_password",
"--upstream-identity-provider-name", "does-not-matter",
"--upstream-identity-provider-type", "oidc",
)
require.Equal(t, `Error: could not complete Pinniped login: could not perform OIDC discovery for "`+
server.URL+`": oidc: issuer did not match the issuer returned by provider, expected "`+
server.URL+`" got "https://not-a-good-issuer"
`, stderr)
require.Empty(t, stdout)
}
// TLS checks safe to run in parallel with serial tests, see main_test.go.
func TestSecureTLSConciergeAggregatedAPI_Parallel(t *testing.T) {
env := testlib.IntegrationEnv(t)
cancelCtx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
startKubectlPortForward(cancelCtx, t, "10446", "443", env.ConciergeAppName+"-api", env.ConciergeNamespace)
stdout, stderr := runNmapSSLEnum(t, "127.0.0.1", 10446)
require.Empty(t, stderr)
require.Contains(t, stdout, getExpectedCiphers(ptls.Secure), "stdout:\n%s", stdout)
}
func TestSecureTLSSupervisor(t *testing.T) { // does not run in parallel because of the createTLSCertificateSecret call
env := testlib.IntegrationEnv(t)
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
adminClient := testlib.NewKubernetesClientset(t)
// make sure the supervisor has a default TLS cert during this test so that it can handle a TLS connection
_ = createTLSCertificateSecret(ctx, t, env.SupervisorNamespace, "cert-hostname-doesnt-matter", nil, defaultTLSCertSecretName(env), adminClient)
startKubectlPortForward(ctx, t, "10447", "443", env.SupervisorAppName+"-clusterip", env.SupervisorNamespace)
stdout, stderr := runNmapSSLEnum(t, "127.0.0.1", 10447)
// supervisor's cert is ECDSA
defaultECDSAOnly := func(rootCAs *x509.CertPool) *tls.Config {
c := ptls.Default(rootCAs)
ciphers := make([]uint16, 0, len(c.CipherSuites)/2)
for _, id := range c.CipherSuites {
id := id
if !strings.Contains(tls.CipherSuiteName(id), "_ECDSA_") {
continue
}
ciphers = append(ciphers, id)
}
c.CipherSuites = ciphers
return c
}
require.Empty(t, stderr)
require.Contains(t, stdout, getExpectedCiphers(defaultECDSAOnly), "stdout:\n%s", stdout)
}
type fakeT struct {
*testing.T
}
func (t *fakeT) FailNow() {
t.Errorf("fakeT ignored FailNow")
}
func (t *fakeT) Errorf(format string, args ...interface{}) {
t.Cleanup(func() {
if !t.Failed() {
return
}
t.Logf("reporting previously ignored errors since main test failed:\n"+format, args...)
})
}
func runNmapSSLEnum(t *testing.T, host string, port uint16) (string, string) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
version, err := exec.CommandContext(ctx, "nmap", "-V").CombinedOutput()
require.NoError(t, err)
versionMatches := regexp.MustCompile(`Nmap version 7\.(?P<minor>\d+)`).FindStringSubmatch(string(version))
require.Len(t, versionMatches, 2)
minorVersion, err := strconv.Atoi(versionMatches[1])
require.NoError(t, err)
require.GreaterOrEqual(t, minorVersion, 92, "nmap >= 7.92.x is required")
var stdout, stderr bytes.Buffer
//nolint:gosec // we are not performing malicious argument injection against ourselves
cmd := exec.CommandContext(ctx, "nmap", "--script", "ssl-enum-ciphers",
"-p", strconv.FormatUint(uint64(port), 10),
host,
)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
require.NoErrorf(t, cmd.Run(), "stderr:\n%s\n\nstdout:\n%s\n\n", stderr.String(), stdout.String())
return stdout.String(), stderr.String()
}
func getExpectedCiphers(configFunc ptls.ConfigFunc) string {
config := configFunc(nil)
secureConfig := ptls.Secure(nil)
skip12 := config.MinVersion == secureConfig.MinVersion
var tls12Bit, tls13Bit string
if !skip12 {
sort.SliceStable(config.CipherSuites, func(i, j int) bool {
a := tls.CipherSuiteName(config.CipherSuites[i])
b := tls.CipherSuiteName(config.CipherSuites[j])
ok1 := strings.Contains(a, "_ECDSA_")
ok2 := strings.Contains(b, "_ECDSA_")
if ok1 && ok2 {
return false
}
return ok1
})
var s strings.Builder
for i, id := range config.CipherSuites {
s.WriteString(fmt.Sprintf(tls12Item, tls.CipherSuiteName(id)))
if i == len(config.CipherSuites)-1 {
break
}
s.WriteString("\n")
}
tls12Bit = fmt.Sprintf(tls12Base, s.String())
}
var s strings.Builder
for i, id := range secureConfig.CipherSuites {
s.WriteString(fmt.Sprintf(tls13Item, strings.Replace(tls.CipherSuiteName(id), "TLS_", "TLS_AKE_WITH_", 1)))
if i == len(secureConfig.CipherSuites)-1 {
break
}
s.WriteString("\n")
}
tls13Bit = fmt.Sprintf(tls13Base, s.String())
return fmt.Sprintf(baseItem, tls12Bit, tls13Bit)
}
const (
// this surrounds the tls 1.2 and 1.3 text in a way that guarantees that other TLS versions are not supported.
baseItem = `/tcp open unknown
| ssl-enum-ciphers: %s%s
|_ least strength: A
Nmap done: 1 IP address (1 host up) scanned in`
// the "cipher preference: client" bit a bug in nmap.
// https://github.com/nmap/nmap/issues/1691#issuecomment-536919978
tls12Base = `
| TLSv1.2:
| ciphers:
%s
| compressors:
| NULL
| cipher preference: client`
tls13Base = `
| TLSv1.3:
| ciphers:
%s
| cipher preference: server`
tls12Item = `| %s (secp256r1) - A`
tls13Item = `| %s (ecdh_x25519) - A`
)