Introduce FIPS compatibility
Signed-off-by: Margo Crawford <margaretc@vmware.com>
This commit is contained in:
parent
2cffea5880
commit
53597bb824
@ -11,6 +11,7 @@ repos:
|
|||||||
- id: check-json
|
- id: check-json
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
|
exclude: 'securetls*' # prevent the linter from running in this file because it's not smart enough not to trim the nmap test output.
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
- id: check-byte-order-marker
|
- id: check-byte-order-marker
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
// Package main is the combined entrypoint for the Pinniped "kube-cert-agent" component.
|
// Package main is the combined entrypoint for the Pinniped "kube-cert-agent" component.
|
||||||
@ -13,6 +13,9 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
// this side effect import ensures that we use fipsonly crypto in fips_strict mode.
|
||||||
|
_ "go.pinniped.dev/internal/crypto/ptls"
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint: gochecknoglobals // these are swapped during unit tests.
|
//nolint: gochecknoglobals // these are swapped during unit tests.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
// Package main is the combined entrypoint for all Pinniped server components.
|
// Package main is the combined entrypoint for all Pinniped server components.
|
||||||
@ -15,6 +15,8 @@ import (
|
|||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
|
|
||||||
concierge "go.pinniped.dev/internal/concierge/server"
|
concierge "go.pinniped.dev/internal/concierge/server"
|
||||||
|
// this side effect import ensures that we use fipsonly crypto in fips_strict mode.
|
||||||
|
_ "go.pinniped.dev/internal/crypto/ptls"
|
||||||
lua "go.pinniped.dev/internal/localuserauthenticator"
|
lua "go.pinniped.dev/internal/localuserauthenticator"
|
||||||
supervisor "go.pinniped.dev/internal/supervisor/server"
|
supervisor "go.pinniped.dev/internal/supervisor/server"
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package main
|
package main
|
||||||
@ -9,6 +9,8 @@ import (
|
|||||||
"github.com/pkg/browser"
|
"github.com/pkg/browser"
|
||||||
|
|
||||||
"go.pinniped.dev/cmd/pinniped/cmd"
|
"go.pinniped.dev/cmd/pinniped/cmd"
|
||||||
|
// this side effect import ensures that we use fipsonly crypto in fips_strict mode.
|
||||||
|
_ "go.pinniped.dev/internal/crypto/ptls"
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint: gochecknoinits
|
//nolint: gochecknoinits
|
||||||
|
45
hack/Dockerfile_fips
Normal file
45
hack/Dockerfile_fips
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# syntax = docker/dockerfile:1.0-experimental
|
||||||
|
|
||||||
|
# Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
# this dockerfile is used to produce a binary of Pinniped that uses
|
||||||
|
# only fips-allowable ciphers.
|
||||||
|
|
||||||
|
# use go-boringcrypto rather than main go
|
||||||
|
FROM us-docker.pkg.dev/google.com/api-project-999119582588/go-boringcrypto/golang:1.17.8b7 as build-env
|
||||||
|
|
||||||
|
WORKDIR /work
|
||||||
|
COPY . .
|
||||||
|
ARG GOPROXY
|
||||||
|
|
||||||
|
# Build the executable binary (CGO_ENABLED=1 is required for go boring)
|
||||||
|
# Pass in GOCACHE (build cache) and GOMODCACHE (module cache) so they
|
||||||
|
# can be re-used between image builds.
|
||||||
|
RUN \
|
||||||
|
--mount=type=cache,target=/cache/gocache \
|
||||||
|
--mount=type=cache,target=/cache/gomodcache \
|
||||||
|
mkdir out && \
|
||||||
|
export CGO_ENABLED=1 GOOS=linux GOARCH=amd64 && \
|
||||||
|
go build -tags fips_strict,osusergo,netgo -v -trimpath -ldflags "$(hack/get-ldflags.sh) -w -linkmode=external -extldflags -static" -o /usr/local/bin/pinniped-concierge-kube-cert-agent ./cmd/pinniped-concierge-kube-cert-agent/... && \
|
||||||
|
go build -tags fips_strict,osusergo,netgo -v -trimpath -ldflags "$(hack/get-ldflags.sh) -w -linkmode=external -extldflags -static" -o /usr/local/bin/pinniped-server ./cmd/pinniped-server/... && \
|
||||||
|
ln -s /usr/local/bin/pinniped-server /usr/local/bin/pinniped-concierge && \
|
||||||
|
ln -s /usr/local/bin/pinniped-server /usr/local/bin/pinniped-supervisor && \
|
||||||
|
ln -s /usr/local/bin/pinniped-server /usr/local/bin/local-user-authenticator
|
||||||
|
|
||||||
|
# Use a distroless runtime image with CA certificates, timezone data, and not much else.
|
||||||
|
FROM gcr.io/distroless/static:nonroot@sha256:80c956fb0836a17a565c43a4026c9c80b2013c83bea09f74fa4da195a59b7a99
|
||||||
|
|
||||||
|
# Copy the server binary from the build-env stage.
|
||||||
|
COPY --from=build-env /usr/local/bin /usr/local/bin
|
||||||
|
|
||||||
|
# Document the default server ports for the various server apps
|
||||||
|
EXPOSE 8443 8444 10250
|
||||||
|
|
||||||
|
# Run as non-root for security posture
|
||||||
|
# Use the same non-root user as https://github.com/GoogleContainerTools/distroless/blob/fc3c4eaceb0518900f886aae90407c43be0a42d9/base/base.bzl#L9
|
||||||
|
# This is a workaround for https://github.com/GoogleContainerTools/distroless/issues/718
|
||||||
|
USER 65532:65532
|
||||||
|
|
||||||
|
# Set the entrypoint
|
||||||
|
ENTRYPOINT ["/usr/local/bin/pinniped-server"]
|
@ -49,6 +49,7 @@ help=no
|
|||||||
skip_build=no
|
skip_build=no
|
||||||
clean_kind=no
|
clean_kind=no
|
||||||
api_group_suffix="pinniped.dev" # same default as in the values.yaml ytt file
|
api_group_suffix="pinniped.dev" # same default as in the values.yaml ytt file
|
||||||
|
dockerfile_path=""
|
||||||
skip_chromedriver_check=no
|
skip_chromedriver_check=no
|
||||||
get_active_directory_vars="" # specify a filename for a script to get AD related env variables
|
get_active_directory_vars="" # specify a filename for a script to get AD related env variables
|
||||||
alternate_deploy="undefined"
|
alternate_deploy="undefined"
|
||||||
@ -91,6 +92,16 @@ while (("$#")); do
|
|||||||
get_active_directory_vars=$1
|
get_active_directory_vars=$1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
--dockerfile-path)
|
||||||
|
shift
|
||||||
|
# If there are no more command line arguments, or there is another command line argument but it starts with a dash, then error
|
||||||
|
if [[ "$#" == "0" || "$1" == -* ]]; then
|
||||||
|
log_error "--dockerfile-path requires a script name to be specified"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
dockerfile_path=$1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
--alternate-deploy)
|
--alternate-deploy)
|
||||||
shift
|
shift
|
||||||
if [[ "$#" == "0" || "$1" == -* ]]; then
|
if [[ "$#" == "0" || "$1" == -* ]]; then
|
||||||
@ -219,9 +230,14 @@ registry_repo_tag="${registry_repo}:${tag}"
|
|||||||
|
|
||||||
if [[ "$do_build" == "yes" ]]; then
|
if [[ "$do_build" == "yes" ]]; then
|
||||||
# Rebuild the code
|
# Rebuild the code
|
||||||
log_note "Docker building the app..."
|
if [[ "$dockerfile_path" != "" ]]; then
|
||||||
# DOCKER_BUILDKIT=1 is optional on MacOS but required on linux.
|
log_note "Docker building the app with dockerfile $dockerfile_path..."
|
||||||
DOCKER_BUILDKIT=1 docker build . --tag "$registry_repo_tag"
|
DOCKER_BUILDKIT=1 docker build . --tag "$registry_repo_tag" --file "$dockerfile_path"
|
||||||
|
else
|
||||||
|
log_note "Docker building the app..."
|
||||||
|
# DOCKER_BUILDKIT=1 is optional on MacOS but required on linux.
|
||||||
|
DOCKER_BUILDKIT=1 docker build . --tag "$registry_repo_tag"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Load it into the cluster
|
# Load it into the cluster
|
||||||
|
@ -145,12 +145,12 @@ type agentController struct {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
// controllerManagerLabels are the Kubernetes labels we expect on the kube-controller-manager Pod.
|
// controllerManagerLabels are the Kubernetes labels we expect on the kube-controller-manager Pod.
|
||||||
controllerManagerLabels = labels.SelectorFromSet(map[string]string{ //nolint: gochecknoglobals
|
controllerManagerLabels = labels.SelectorFromSet(map[string]string{ // nolint: gochecknoglobals
|
||||||
"component": "kube-controller-manager",
|
"component": "kube-controller-manager",
|
||||||
})
|
})
|
||||||
|
|
||||||
// agentLabels are the Kubernetes labels we always expect on the kube-controller-manager Pod.
|
// agentLabels are the Kubernetes labels we always expect on the kube-controller-manager Pod.
|
||||||
agentLabels = labels.SelectorFromSet(map[string]string{ //nolint: gochecknoglobals
|
agentLabels = labels.SelectorFromSet(map[string]string{ // nolint: gochecknoglobals
|
||||||
agentPodLabelKey: agentPodLabelValue,
|
agentPodLabelKey: agentPodLabelValue,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@ -543,12 +543,12 @@ func (c *agentController) newAgentDeployment(controllerManagerPod *corev1.Pod) *
|
|||||||
},
|
},
|
||||||
Resources: corev1.ResourceRequirements{
|
Resources: corev1.ResourceRequirements{
|
||||||
Limits: corev1.ResourceList{
|
Limits: corev1.ResourceList{
|
||||||
corev1.ResourceMemory: resource.MustParse("16Mi"),
|
corev1.ResourceMemory: resource.MustParse("32Mi"),
|
||||||
corev1.ResourceCPU: resource.MustParse("10m"),
|
corev1.ResourceCPU: resource.MustParse("20m"),
|
||||||
},
|
},
|
||||||
Requests: corev1.ResourceList{
|
Requests: corev1.ResourceList{
|
||||||
corev1.ResourceMemory: resource.MustParse("16Mi"),
|
corev1.ResourceMemory: resource.MustParse("32Mi"),
|
||||||
corev1.ResourceCPU: resource.MustParse("10m"),
|
corev1.ResourceCPU: resource.MustParse("20m"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -120,12 +120,12 @@ func TestAgentController(t *testing.T) {
|
|||||||
}},
|
}},
|
||||||
Resources: corev1.ResourceRequirements{
|
Resources: corev1.ResourceRequirements{
|
||||||
Limits: corev1.ResourceList{
|
Limits: corev1.ResourceList{
|
||||||
corev1.ResourceMemory: resource.MustParse("16Mi"),
|
corev1.ResourceMemory: resource.MustParse("32Mi"),
|
||||||
corev1.ResourceCPU: resource.MustParse("10m"),
|
corev1.ResourceCPU: resource.MustParse("20m"),
|
||||||
},
|
},
|
||||||
Requests: corev1.ResourceList{
|
Requests: corev1.ResourceList{
|
||||||
corev1.ResourceMemory: resource.MustParse("16Mi"),
|
corev1.ResourceMemory: resource.MustParse("32Mi"),
|
||||||
corev1.ResourceCPU: resource.MustParse("10m"),
|
corev1.ResourceCPU: resource.MustParse("20m"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ImagePullPolicy: corev1.PullIfNotPresent,
|
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||||
@ -1028,7 +1028,7 @@ func TestAgentController(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kubeInformers := informers.NewSharedInformerFactory(kubeClientset, 0)
|
kubeInformers := informers.NewSharedInformerFactory(kubeClientset, 0)
|
||||||
log := testlogger.NewLegacy(t) //nolint: staticcheck // old test with lots of log statements
|
log := testlogger.NewLegacy(t) // nolint: staticcheck // old test with lots of log statements
|
||||||
|
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
@ -1106,7 +1106,7 @@ func TestAgentController(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
if tt.wantAgentDeployment == nil {
|
if tt.wantAgentDeployment == nil {
|
||||||
assert.Empty(t, deployments.Items, "did not expect an agent deployment")
|
assert.Empty(t, deployments.Items, "did not expect an agent deployment")
|
||||||
} else { //nolint: gocritic
|
} else { // nolint: gocritic
|
||||||
if assert.Len(t, deployments.Items, 1, "expected a single agent deployment") {
|
if assert.Len(t, deployments.Items, 1, "expected a single agent deployment") {
|
||||||
assert.Equal(t, tt.wantAgentDeployment, &deployments.Items[0])
|
assert.Equal(t, tt.wantAgentDeployment, &deployments.Items[0])
|
||||||
}
|
}
|
||||||
|
73
internal/crypto/ptls/default.go
Normal file
73
internal/crypto/ptls/default.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
//go:build !fips_strict
|
||||||
|
// +build !fips_strict
|
||||||
|
|
||||||
|
package ptls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Default(rootCAs *x509.CertPool) *tls.Config {
|
||||||
|
return &tls.Config{
|
||||||
|
// Can't use SSLv3 because of POODLE and BEAST
|
||||||
|
// Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher
|
||||||
|
// Can't use TLSv1.1 because of RC4 cipher usage
|
||||||
|
//
|
||||||
|
// The Kubernetes API Server must use TLS 1.2, at a minimum,
|
||||||
|
// to protect the confidentiality of sensitive data during electronic dissemination.
|
||||||
|
// https://stigviewer.com/stig/kubernetes/2021-06-17/finding/V-242378
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
|
||||||
|
// the order does not matter in go 1.17+ https://go.dev/blog/tls-cipher-suites
|
||||||
|
// we match crypto/tls.cipherSuitesPreferenceOrder because it makes unit tests easier to write
|
||||||
|
// this list is ignored when TLS 1.3 is used
|
||||||
|
//
|
||||||
|
// as of 2021-10-19, Mozilla Guideline v5.6, Go 1.17.2, intermediate configuration, supports:
|
||||||
|
// - Firefox 27
|
||||||
|
// - Android 4.4.2
|
||||||
|
// - Chrome 31
|
||||||
|
// - Edge
|
||||||
|
// - IE 11 on Windows 7
|
||||||
|
// - Java 8u31
|
||||||
|
// - OpenSSL 1.0.1
|
||||||
|
// - Opera 20
|
||||||
|
// - Safari 9
|
||||||
|
// https://ssl-config.mozilla.org/#server=go&version=1.17.2&config=intermediate&guideline=5.6
|
||||||
|
//
|
||||||
|
// The Kubernetes API server must use approved cipher suites.
|
||||||
|
// https://stigviewer.com/stig/kubernetes/2021-06-17/finding/V-242418
|
||||||
|
CipherSuites: []uint16{
|
||||||
|
// these are all AEADs with ECDHE, some use ChaCha20Poly1305 while others use AES-GCM
|
||||||
|
// this provides forward secrecy, confidentiality and authenticity of data
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||||
|
},
|
||||||
|
|
||||||
|
// enable HTTP2 for go's 1.7 HTTP Server
|
||||||
|
// setting this explicitly is only required in very specific circumstances
|
||||||
|
// it is simpler to just set it here than to try and determine if we need to
|
||||||
|
NextProtos: []string{"h2", "http/1.1"},
|
||||||
|
|
||||||
|
// optional root CAs, nil means use the host's root CA set
|
||||||
|
RootCAs: rootCAs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultLDAP(rootCAs *x509.CertPool) *tls.Config {
|
||||||
|
c := Default(rootCAs)
|
||||||
|
// add less secure ciphers to support the default AWS Active Directory config
|
||||||
|
c.CipherSuites = append(c.CipherSuites,
|
||||||
|
// CBC with ECDHE
|
||||||
|
// this provides forward secrecy and confidentiality of data but not authenticity
|
||||||
|
// MAC-then-Encrypt CBC ciphers are susceptible to padding oracle attacks
|
||||||
|
// See https://crypto.stackexchange.com/a/205 and https://crypto.stackexchange.com/a/224
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||||
|
)
|
||||||
|
return c
|
||||||
|
}
|
66
internal/crypto/ptls/fips_strict.go
Normal file
66
internal/crypto/ptls/fips_strict.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
// The configurations here override the usual ptls.Secure, ptls.Default, and ptls.DefaultLDAP
|
||||||
|
// configs when Pinniped is built in fips-only mode.
|
||||||
|
// All of these are the same because FIPs is already so limited.
|
||||||
|
//go:build fips_strict
|
||||||
|
// +build fips_strict
|
||||||
|
|
||||||
|
package ptls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"C"
|
||||||
|
"crypto/tls"
|
||||||
|
_ "crypto/tls/fipsonly" // restricts all TLS configuration to FIPS-approved settings.
|
||||||
|
"crypto/x509"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/plog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Always use TLS 1.2 for FIPs
|
||||||
|
const secureServingOptionsMinTLSVersion = "VersionTLS12"
|
||||||
|
const SecureTLSConfigMinTLSVersion = tls.VersionTLS12
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
go func() {
|
||||||
|
version := runtime.Version()
|
||||||
|
plog.Debug("using boringcrypto in fips only mode.", "go version", version)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Default(rootCAs *x509.CertPool) *tls.Config {
|
||||||
|
return &tls.Config{
|
||||||
|
// goboring requires TLS 1.2 and only TLS 1.2
|
||||||
|
MinVersion: SecureTLSConfigMinTLSVersion,
|
||||||
|
MaxVersion: SecureTLSConfigMinTLSVersion,
|
||||||
|
|
||||||
|
// enable HTTP2 for go's 1.7 HTTP Server
|
||||||
|
// setting this explicitly is only required in very specific circumstances
|
||||||
|
// it is simpler to just set it here than to try and determine if we need to
|
||||||
|
NextProtos: []string{"h2", "http/1.1"},
|
||||||
|
|
||||||
|
// optional root CAs, nil means use the host's root CA set
|
||||||
|
RootCAs: rootCAs,
|
||||||
|
|
||||||
|
// This is all of the fips-approved ciphers.
|
||||||
|
// The list is hard-coded for convenience of testing.
|
||||||
|
CipherSuites: []uint16{
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||||
|
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||||||
|
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Secure(rootCAs *x509.CertPool) *tls.Config {
|
||||||
|
return Default(rootCAs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultLDAP(rootCAs *x509.CertPool) *tls.Config {
|
||||||
|
return Default(rootCAs)
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package ptls
|
package ptls
|
||||||
@ -21,92 +21,13 @@ import (
|
|||||||
|
|
||||||
// TODO decide if we need to expose the four TLS levels (secure, default, default-ldap, legacy) as config.
|
// TODO decide if we need to expose the four TLS levels (secure, default, default-ldap, legacy) as config.
|
||||||
|
|
||||||
|
// defaultServingOptionsMinTLSVersion is the minimum tls version in the format
|
||||||
|
// expected by SecureServingOptions.MinTLSVersion from
|
||||||
|
// k8s.io/apiserver/pkg/server/options.
|
||||||
|
const defaultServingOptionsMinTLSVersion = "VersionTLS12"
|
||||||
|
|
||||||
type ConfigFunc func(*x509.CertPool) *tls.Config
|
type ConfigFunc func(*x509.CertPool) *tls.Config
|
||||||
|
|
||||||
func Default(rootCAs *x509.CertPool) *tls.Config {
|
|
||||||
return &tls.Config{
|
|
||||||
// Can't use SSLv3 because of POODLE and BEAST
|
|
||||||
// Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher
|
|
||||||
// Can't use TLSv1.1 because of RC4 cipher usage
|
|
||||||
//
|
|
||||||
// The Kubernetes API Server must use TLS 1.2, at a minimum,
|
|
||||||
// to protect the confidentiality of sensitive data during electronic dissemination.
|
|
||||||
// https://stigviewer.com/stig/kubernetes/2021-06-17/finding/V-242378
|
|
||||||
MinVersion: tls.VersionTLS12,
|
|
||||||
|
|
||||||
// the order does not matter in go 1.17+ https://go.dev/blog/tls-cipher-suites
|
|
||||||
// we match crypto/tls.cipherSuitesPreferenceOrder because it makes unit tests easier to write
|
|
||||||
// this list is ignored when TLS 1.3 is used
|
|
||||||
//
|
|
||||||
// as of 2021-10-19, Mozilla Guideline v5.6, Go 1.17.2, intermediate configuration, supports:
|
|
||||||
// - Firefox 27
|
|
||||||
// - Android 4.4.2
|
|
||||||
// - Chrome 31
|
|
||||||
// - Edge
|
|
||||||
// - IE 11 on Windows 7
|
|
||||||
// - Java 8u31
|
|
||||||
// - OpenSSL 1.0.1
|
|
||||||
// - Opera 20
|
|
||||||
// - Safari 9
|
|
||||||
// https://ssl-config.mozilla.org/#server=go&version=1.17.2&config=intermediate&guideline=5.6
|
|
||||||
//
|
|
||||||
// The Kubernetes API server must use approved cipher suites.
|
|
||||||
// https://stigviewer.com/stig/kubernetes/2021-06-17/finding/V-242418
|
|
||||||
CipherSuites: []uint16{
|
|
||||||
// these are all AEADs with ECDHE, some use ChaCha20Poly1305 while others use AES-GCM
|
|
||||||
// this provides forward secrecy, confidentiality and authenticity of data
|
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
|
||||||
},
|
|
||||||
|
|
||||||
// enable HTTP2 for go's 1.7 HTTP Server
|
|
||||||
// setting this explicitly is only required in very specific circumstances
|
|
||||||
// it is simpler to just set it here than to try and determine if we need to
|
|
||||||
NextProtos: []string{"h2", "http/1.1"},
|
|
||||||
|
|
||||||
// optional root CAs, nil means use the host's root CA set
|
|
||||||
RootCAs: rootCAs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Secure(rootCAs *x509.CertPool) *tls.Config {
|
|
||||||
// as of 2021-10-19, Mozilla Guideline v5.6, Go 1.17.2, modern configuration, supports:
|
|
||||||
// - Firefox 63
|
|
||||||
// - Android 10.0
|
|
||||||
// - Chrome 70
|
|
||||||
// - Edge 75
|
|
||||||
// - Java 11
|
|
||||||
// - OpenSSL 1.1.1
|
|
||||||
// - Opera 57
|
|
||||||
// - Safari 12.1
|
|
||||||
// https://ssl-config.mozilla.org/#server=go&version=1.17.2&config=modern&guideline=5.6
|
|
||||||
c := Default(rootCAs)
|
|
||||||
c.MinVersion = tls.VersionTLS13 // max out the security
|
|
||||||
c.CipherSuites = []uint16{
|
|
||||||
// TLS 1.3 ciphers are not configurable, but we need to explicitly set them here to make our client hello behave correctly
|
|
||||||
// See https://github.com/golang/go/pull/49293
|
|
||||||
tls.TLS_AES_128_GCM_SHA256,
|
|
||||||
tls.TLS_AES_256_GCM_SHA384,
|
|
||||||
tls.TLS_CHACHA20_POLY1305_SHA256,
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultLDAP(rootCAs *x509.CertPool) *tls.Config {
|
|
||||||
c := Default(rootCAs)
|
|
||||||
// add less secure ciphers to support the default AWS Active Directory config
|
|
||||||
c.CipherSuites = append(c.CipherSuites,
|
|
||||||
// CBC with ECDHE
|
|
||||||
// this provides forward secrecy and confidentiality of data but not authenticity
|
|
||||||
// MAC-then-Encrypt CBC ciphers are susceptible to padding oracle attacks
|
|
||||||
// See https://crypto.stackexchange.com/a/205 and https://crypto.stackexchange.com/a/224
|
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
|
||||||
)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func Legacy(rootCAs *x509.CertPool) *tls.Config {
|
func Legacy(rootCAs *x509.CertPool) *tls.Config {
|
||||||
c := Default(rootCAs)
|
c := Default(rootCAs)
|
||||||
// add all the ciphers (even the crappy ones) except the ones that Go considers to be outright broken like 3DES
|
// add all the ciphers (even the crappy ones) except the ones that Go considers to be outright broken like 3DES
|
||||||
@ -158,11 +79,11 @@ func defaultServing(opts *options.SecureServingOptionsWithLoopback) {
|
|||||||
}
|
}
|
||||||
opts.CipherSuites = cipherSuites
|
opts.CipherSuites = cipherSuites
|
||||||
|
|
||||||
opts.MinTLSVersion = "VersionTLS12"
|
opts.MinTLSVersion = defaultServingOptionsMinTLSVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
func secureServing(opts *options.SecureServingOptionsWithLoopback) {
|
func secureServing(opts *options.SecureServingOptionsWithLoopback) {
|
||||||
opts.MinTLSVersion = "VersionTLS13"
|
opts.MinTLSVersion = secureServingOptionsMinTLSVersion
|
||||||
opts.CipherSuites = nil
|
opts.CipherSuites = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
44
internal/crypto/ptls/secure.go
Normal file
44
internal/crypto/ptls/secure.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
//go:build !fips_strict
|
||||||
|
// +build !fips_strict
|
||||||
|
|
||||||
|
package ptls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
)
|
||||||
|
|
||||||
|
// secureServingOptionsMinTLSVersion is the minimum tls version in the format
|
||||||
|
// expected by SecureServingOptions.MinTLSVersion from
|
||||||
|
// k8s.io/apiserver/pkg/server/options.
|
||||||
|
const secureServingOptionsMinTLSVersion = "VersionTLS13"
|
||||||
|
|
||||||
|
// SecureTLSConfigMinTLSVersion is the minimum tls version in the format expected
|
||||||
|
// by tls.Config.
|
||||||
|
const SecureTLSConfigMinTLSVersion = tls.VersionTLS13
|
||||||
|
|
||||||
|
func Secure(rootCAs *x509.CertPool) *tls.Config {
|
||||||
|
// as of 2021-10-19, Mozilla Guideline v5.6, Go 1.17.2, modern configuration, supports:
|
||||||
|
// - Firefox 63
|
||||||
|
// - Android 10.0
|
||||||
|
// - Chrome 70
|
||||||
|
// - Edge 75
|
||||||
|
// - Java 11
|
||||||
|
// - OpenSSL 1.1.1
|
||||||
|
// - Opera 57
|
||||||
|
// - Safari 12.1
|
||||||
|
// https://ssl-config.mozilla.org/#server=go&version=1.17.2&config=modern&guideline=5.6
|
||||||
|
c := Default(rootCAs)
|
||||||
|
c.MinVersion = SecureTLSConfigMinTLSVersion // max out the security
|
||||||
|
c.CipherSuites = []uint16{
|
||||||
|
// TLS 1.3 ciphers are not configurable, but we need to explicitly set them here to make our client hello behave correctly
|
||||||
|
// See https://github.com/golang/go/pull/49293
|
||||||
|
tls.TLS_AES_128_GCM_SHA256,
|
||||||
|
tls.TLS_AES_256_GCM_SHA384,
|
||||||
|
tls.TLS_CHACHA20_POLY1305_SHA256,
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package tlsserver
|
package tlsserver
|
||||||
@ -69,6 +69,14 @@ func RecordTLSHello(server *httptest.Server) {
|
|||||||
func AssertTLS(t *testing.T, r *http.Request, tlsConfigFunc ptls.ConfigFunc) {
|
func AssertTLS(t *testing.T, r *http.Request, tlsConfigFunc ptls.ConfigFunc) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
tlsConfig := tlsConfigFunc(nil)
|
||||||
|
|
||||||
|
AssertTLSConfig(t, r, tlsConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AssertTLSConfig(t *testing.T, r *http.Request, tlsConfig *tls.Config) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
m, ok := getCtxMap(r.Context())
|
m, ok := getCtxMap(r.Context())
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
@ -78,8 +86,6 @@ func AssertTLS(t *testing.T, r *http.Request, tlsConfigFunc ptls.ConfigFunc) {
|
|||||||
info, ok := h.(*tls.ClientHelloInfo)
|
info, ok := h.(*tls.ClientHelloInfo)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
tlsConfig := tlsConfigFunc(nil)
|
|
||||||
|
|
||||||
supportedVersions := []uint16{tlsConfig.MinVersion}
|
supportedVersions := []uint16{tlsConfig.MinVersion}
|
||||||
ciphers := tlsConfig.CipherSuites
|
ciphers := tlsConfig.CipherSuites
|
||||||
|
|
||||||
@ -95,7 +101,7 @@ func AssertTLS(t *testing.T, r *http.Request, tlsConfigFunc ptls.ConfigFunc) {
|
|||||||
|
|
||||||
// use assert instead of require to not break the http.Handler with a panic
|
// use assert instead of require to not break the http.Handler with a panic
|
||||||
ok1 := assert.Equal(t, supportedVersions, info.SupportedVersions)
|
ok1 := assert.Equal(t, supportedVersions, info.SupportedVersions)
|
||||||
ok2 := assert.Equal(t, ciphers, info.CipherSuites)
|
ok2 := assert.Equal(t, cipherSuiteIDsToStrings(ciphers), cipherSuiteIDsToStrings(info.CipherSuites))
|
||||||
ok3 := assert.Equal(t, protos, info.SupportedProtos)
|
ok3 := assert.Equal(t, protos, info.SupportedProtos)
|
||||||
|
|
||||||
if all := ok1 && ok2 && ok3; !all {
|
if all := ok1 && ok2 && ok3; !all {
|
||||||
@ -104,6 +110,14 @@ func AssertTLS(t *testing.T, r *http.Request, tlsConfigFunc ptls.ConfigFunc) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cipherSuiteIDsToStrings(ids []uint16) []string {
|
||||||
|
cipherSuites := make([]string, 0, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
cipherSuites = append(cipherSuites, tls.CipherSuiteName(id))
|
||||||
|
}
|
||||||
|
return cipherSuites
|
||||||
|
}
|
||||||
|
|
||||||
func getCtxMap(ctx context.Context) (*sync.Map, bool) {
|
func getCtxMap(ctx context.Context) (*sync.Map, bool) {
|
||||||
m, ok := ctx.Value(mapKey).(*sync.Map)
|
m, ok := ctx.Value(mapKey).(*sync.Map)
|
||||||
return m, ok
|
return m, ok
|
||||||
|
@ -1503,10 +1503,10 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
|
|||||||
|
|
||||||
startKubectlPortForward(cancelCtx, t, "10445", "443", env.ConciergeAppName+"-proxy", env.ConciergeNamespace)
|
startKubectlPortForward(cancelCtx, t, "10445", "443", env.ConciergeAppName+"-proxy", env.ConciergeNamespace)
|
||||||
|
|
||||||
stdout, stderr := runNmapSSLEnum(t, "127.0.0.1", 10445)
|
stdout, stderr := testlib.RunNmapSSLEnum(t, "127.0.0.1", 10445)
|
||||||
|
|
||||||
require.Empty(t, stderr)
|
require.Empty(t, stderr)
|
||||||
require.Contains(t, stdout, getExpectedCiphers(ptls.Default), "stdout:\n%s", stdout)
|
require.Contains(t, stdout, testlib.GetExpectedCiphers(ptls.Default(nil)), "stdout:\n%s", stdout)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
191
test/integration/securetls_fips_test.go
Normal file
191
test/integration/securetls_fips_test.go
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
//go:build fips_strict
|
||||||
|
// +build fips_strict
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"k8s.io/client-go/util/cert"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/crypto/ptls"
|
||||||
|
"go.pinniped.dev/internal/testutil/tlsserver"
|
||||||
|
"go.pinniped.dev/test/testlib"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This test mirrors securetls_test.go, but adapted for fips mode.
|
||||||
|
// e.g. checks for only TLS 1.2 ciphers and checks for the
|
||||||
|
// list of fips-approved ciphers above.
|
||||||
|
// TLS checks safe to run in parallel with serial tests, see main_test.go.
|
||||||
|
func TestSecureTLSPinnipedCLIToKAS_Parallel(t *testing.T) {
|
||||||
|
_ = testlib.IntegrationEnv(t)
|
||||||
|
t.Log("testing FIPs tls config")
|
||||||
|
|
||||||
|
server := tlsserver.TLSTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// pinniped CLI uses ptls.Secure when talking to KAS,
|
||||||
|
// although the distinction doesn't matter much in FIPs mode because
|
||||||
|
// each of the configs is a wrapper for the same base FIPs config.
|
||||||
|
secure := ptls.Secure(nil)
|
||||||
|
tlsserver.AssertTLSConfig(t, r, secure)
|
||||||
|
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) {
|
||||||
|
// pinniped CLI uses ptls.Default when talking to supervisor,
|
||||||
|
// although the distinction doesn't matter much in FIPs mode because
|
||||||
|
// each of the configs is a wrapper for the same base FIPs config.
|
||||||
|
defaultTLS := ptls.Default(nil)
|
||||||
|
tlsserver.AssertTLSConfig(t, r, defaultTLS)
|
||||||
|
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 := testlib.RunNmapSSLEnum(t, "127.0.0.1", 10446)
|
||||||
|
|
||||||
|
require.Empty(t, stderr)
|
||||||
|
secure := ptls.Secure(nil)
|
||||||
|
require.Contains(t, stdout, testlib.GetExpectedCiphers(secure), "stdout:\n%s", stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSecureTLSSupervisor(t *testing.T) { // does not run in parallel because of the createSupervisorDefaultTLSCertificateSecretIfNeeded call
|
||||||
|
env := testlib.IntegrationEnv(t)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
|
startKubectlPortForward(ctx, t, "10447", "443", env.SupervisorAppName+"-nodeport", env.SupervisorNamespace)
|
||||||
|
|
||||||
|
stdout, stderr := testlib.RunNmapSSLEnum(t, "127.0.0.1", 10447)
|
||||||
|
|
||||||
|
// supervisor's cert is ECDSA
|
||||||
|
defaultECDSAOnly := ptls.Default(nil)
|
||||||
|
ciphers := make([]uint16, 0, len(defaultECDSAOnly.CipherSuites)/3)
|
||||||
|
for _, id := range defaultECDSAOnly.CipherSuites {
|
||||||
|
id := id
|
||||||
|
if !strings.Contains(tls.CipherSuiteName(id), "_ECDSA_") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ciphers = append(ciphers, id)
|
||||||
|
}
|
||||||
|
defaultECDSAOnly.CipherSuites = ciphers
|
||||||
|
|
||||||
|
require.Empty(t, stderr)
|
||||||
|
require.Contains(t, stdout, testlib.GetExpectedCiphers(defaultECDSAOnly), "stdout:\n%s", stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// this test ensures that if the list of default fips cipher
|
||||||
|
// suites changes, we will know.
|
||||||
|
func TestFIPSCipherSuites_Parallel(t *testing.T) {
|
||||||
|
_ = testlib.IntegrationEnv(t)
|
||||||
|
server := tlsserver.TLSTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// use the default fips config which contains a hard coded list of cipher suites
|
||||||
|
// that should be equal to the default list of fips cipher suites.
|
||||||
|
defaultTLS := ptls.Default(nil)
|
||||||
|
// assert that the client hello response has the same tls config as this test server.
|
||||||
|
tlsserver.AssertTLSConfig(t, r, defaultTLS)
|
||||||
|
}), tlsserver.RecordTLSHello)
|
||||||
|
|
||||||
|
ca := tlsserver.TLSTestServerCA(server)
|
||||||
|
pool, err := cert.NewPoolFromBytes(ca)
|
||||||
|
require.NoError(t, err)
|
||||||
|
// create a tls config that does not explicitly set cipher suites,
|
||||||
|
// and therefore uses goboring's default fips ciphers.
|
||||||
|
defaultConfig := &tls.Config{
|
||||||
|
RootCAs: pool,
|
||||||
|
NextProtos: ptls.Default(nil).NextProtos,
|
||||||
|
}
|
||||||
|
transport := http.Transport{
|
||||||
|
TLSClientConfig: defaultConfig,
|
||||||
|
ForceAttemptHTTP2: true,
|
||||||
|
}
|
||||||
|
// make a request against the test server, which will validate that the
|
||||||
|
// tls config of the client without explicitly set ciphers
|
||||||
|
// is the same as the tls config of the test server with explicitly
|
||||||
|
// set ciphers from ptls.
|
||||||
|
request, _ := http.NewRequest("GET", server.URL, nil)
|
||||||
|
response, err := transport.RoundTrip(request)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
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...)
|
||||||
|
})
|
||||||
|
}
|
@ -1,23 +1,19 @@
|
|||||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
//go:build !fips_strict
|
||||||
|
// +build !fips_strict
|
||||||
|
|
||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os/exec"
|
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
@ -96,10 +92,10 @@ func TestSecureTLSConciergeAggregatedAPI_Parallel(t *testing.T) {
|
|||||||
|
|
||||||
startKubectlPortForward(cancelCtx, t, "10446", "443", env.ConciergeAppName+"-api", env.ConciergeNamespace)
|
startKubectlPortForward(cancelCtx, t, "10446", "443", env.ConciergeAppName+"-api", env.ConciergeNamespace)
|
||||||
|
|
||||||
stdout, stderr := runNmapSSLEnum(t, "127.0.0.1", 10446)
|
stdout, stderr := testlib.RunNmapSSLEnum(t, "127.0.0.1", 10446)
|
||||||
|
|
||||||
require.Empty(t, stderr)
|
require.Empty(t, stderr)
|
||||||
require.Contains(t, stdout, getExpectedCiphers(ptls.Secure), "stdout:\n%s", stdout)
|
require.Contains(t, stdout, testlib.GetExpectedCiphers(ptls.Secure(nil)), "stdout:\n%s", stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSecureTLSSupervisor(t *testing.T) { // does not run in parallel because of the createSupervisorDefaultTLSCertificateSecretIfNeeded call
|
func TestSecureTLSSupervisor(t *testing.T) { // does not run in parallel because of the createSupervisorDefaultTLSCertificateSecretIfNeeded call
|
||||||
@ -110,25 +106,22 @@ func TestSecureTLSSupervisor(t *testing.T) { // does not run in parallel because
|
|||||||
|
|
||||||
startKubectlPortForward(ctx, t, "10447", "443", env.SupervisorAppName+"-nodeport", env.SupervisorNamespace)
|
startKubectlPortForward(ctx, t, "10447", "443", env.SupervisorAppName+"-nodeport", env.SupervisorNamespace)
|
||||||
|
|
||||||
stdout, stderr := runNmapSSLEnum(t, "127.0.0.1", 10447)
|
stdout, stderr := testlib.RunNmapSSLEnum(t, "127.0.0.1", 10447)
|
||||||
|
|
||||||
// supervisor's cert is ECDSA
|
// supervisor's cert is ECDSA
|
||||||
defaultECDSAOnly := func(rootCAs *x509.CertPool) *tls.Config {
|
defaultECDSAOnly := ptls.Default(nil)
|
||||||
c := ptls.Default(rootCAs)
|
ciphers := make([]uint16, 0, len(defaultECDSAOnly.CipherSuites)/2)
|
||||||
ciphers := make([]uint16, 0, len(c.CipherSuites)/2)
|
for _, id := range defaultECDSAOnly.CipherSuites {
|
||||||
for _, id := range c.CipherSuites {
|
id := id
|
||||||
id := id
|
if !strings.Contains(tls.CipherSuiteName(id), "_ECDSA_") {
|
||||||
if !strings.Contains(tls.CipherSuiteName(id), "_ECDSA_") {
|
continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
ciphers = append(ciphers, id)
|
|
||||||
}
|
}
|
||||||
c.CipherSuites = ciphers
|
ciphers = append(ciphers, id)
|
||||||
return c
|
|
||||||
}
|
}
|
||||||
|
defaultECDSAOnly.CipherSuites = ciphers
|
||||||
|
|
||||||
require.Empty(t, stderr)
|
require.Empty(t, stderr)
|
||||||
require.Contains(t, stdout, getExpectedCiphers(defaultECDSAOnly), "stdout:\n%s", stdout)
|
require.Contains(t, stdout, testlib.GetExpectedCiphers(defaultECDSAOnly), "stdout:\n%s", stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeT struct {
|
type fakeT struct {
|
||||||
@ -147,107 +140,3 @@ func (t *fakeT) Errorf(format string, args ...interface{}) {
|
|||||||
t.Logf("reporting previously ignored errors since main test failed:\n"+format, args...)
|
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`
|
|
||||||
)
|
|
||||||
|
@ -28,6 +28,7 @@ import (
|
|||||||
"go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
"go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
||||||
pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
|
pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
|
||||||
"go.pinniped.dev/internal/certauthority"
|
"go.pinniped.dev/internal/certauthority"
|
||||||
|
"go.pinniped.dev/internal/crypto/ptls"
|
||||||
"go.pinniped.dev/internal/here"
|
"go.pinniped.dev/internal/here"
|
||||||
"go.pinniped.dev/test/testlib"
|
"go.pinniped.dev/test/testlib"
|
||||||
)
|
)
|
||||||
@ -660,7 +661,7 @@ func newHTTPClient(t *testing.T, caBundle string, dnsOverrides map[string]string
|
|||||||
caCertPool.AppendCertsFromPEM([]byte(caBundle))
|
caCertPool.AppendCertsFromPEM([]byte(caBundle))
|
||||||
c.Transport = &http.Transport{
|
c.Transport = &http.Transport{
|
||||||
DialContext: overrideDialContext,
|
DialContext: overrideDialContext,
|
||||||
TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS13, RootCAs: caCertPool},
|
TLSClientConfig: &tls.Config{MinVersion: ptls.SecureTLSConfigMinTLSVersion, RootCAs: caCertPool}, //nolint: gosec // this seems to be a false flag, min tls version is 1.3 in normal mode or 1.2 in fips mode
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
c.Transport = &http.Transport{
|
c.Transport = &http.Transport{
|
||||||
|
142
test/testlib/securetls.go
Normal file
142
test/testlib/securetls.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package testlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/crypto/ptls"
|
||||||
|
)
|
||||||
|
|
||||||
|
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(config *tls.Config) string {
|
||||||
|
secureConfig := ptls.Secure(nil)
|
||||||
|
|
||||||
|
skip12 := config.MinVersion == tls.VersionTLS13
|
||||||
|
|
||||||
|
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 {
|
||||||
|
name := tls.CipherSuiteName(id)
|
||||||
|
group := ""
|
||||||
|
if strings.Contains(name, "_ECDHE_") {
|
||||||
|
group = secp256r1
|
||||||
|
} else {
|
||||||
|
group = rsa2048
|
||||||
|
}
|
||||||
|
s.WriteString(fmt.Sprintf(tls12Item, name, group))
|
||||||
|
if i == len(config.CipherSuites)-1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
s.WriteString("\n")
|
||||||
|
}
|
||||||
|
tls12Bit = fmt.Sprintf(tls12Base, s.String(), getCipherSuitePreference())
|
||||||
|
}
|
||||||
|
|
||||||
|
skip13 := config.MaxVersion == tls.VersionTLS12
|
||||||
|
if !skip13 {
|
||||||
|
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`
|
||||||
|
|
||||||
|
// cipher preference is a variable because in FIPs mode it is server
|
||||||
|
// but in normal mode it is client.
|
||||||
|
tls12Base = `
|
||||||
|
| TLSv1.2:
|
||||||
|
| ciphers:
|
||||||
|
%s
|
||||||
|
| compressors:
|
||||||
|
| NULL
|
||||||
|
| cipher preference: %s`
|
||||||
|
|
||||||
|
tls12Item = `| %s (%s) - A`
|
||||||
|
|
||||||
|
tls13Base = `
|
||||||
|
| TLSv1.3:
|
||||||
|
| ciphers:
|
||||||
|
%s
|
||||||
|
| cipher preference: server`
|
||||||
|
|
||||||
|
// This curve name is part of the output for each of our elliptic curve ciphers.
|
||||||
|
// secp256r1 is also known as P-256.
|
||||||
|
secp256r1 = "secp256r1"
|
||||||
|
// For the RSA ciphers, we expect this output to be RSA 2048.
|
||||||
|
rsa2048 = "rsa 2048"
|
||||||
|
|
||||||
|
tls13Item = `| %s (ecdh_x25519) - A`
|
||||||
|
)
|
15
test/testlib/securetls_preference_fips.go
Normal file
15
test/testlib/securetls_preference_fips.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
//go:build fips_strict
|
||||||
|
// +build fips_strict
|
||||||
|
|
||||||
|
package testlib
|
||||||
|
|
||||||
|
// Because of a bug in nmap, the cipher suite preference is
|
||||||
|
// incorrectly shown as 'client' in some cases.
|
||||||
|
// in fips-only mode, it correctly shows the cipher preference
|
||||||
|
// as 'server', while in non-fips mode it shows as 'client'.
|
||||||
|
func getCipherSuitePreference() string {
|
||||||
|
return "server"
|
||||||
|
}
|
15
test/testlib/securetls_preference_nonfips.go
Normal file
15
test/testlib/securetls_preference_nonfips.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
//go:build !fips_strict
|
||||||
|
// +build !fips_strict
|
||||||
|
|
||||||
|
package testlib
|
||||||
|
|
||||||
|
// Because of a bug in nmap, the cipher suite preference is
|
||||||
|
// incorrectly shown as 'client' in some cases.
|
||||||
|
// in fips-only mode, it correctly shows the cipher preference
|
||||||
|
// as 'server', while in non-fips mode it shows as 'client'.
|
||||||
|
func getCipherSuitePreference() string {
|
||||||
|
return "client"
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user