|
@@ -17,17 +17,27 @@ limitations under the License.
|
|
|
package addon
|
|
package addon
|
|
|
|
|
|
|
|
import (
|
|
import (
|
|
|
|
|
+ "context"
|
|
|
"crypto/rand"
|
|
"crypto/rand"
|
|
|
|
|
+ "crypto/rsa"
|
|
|
"crypto/x509"
|
|
"crypto/x509"
|
|
|
|
|
+ "crypto/x509/pkix"
|
|
|
"encoding/base64"
|
|
"encoding/base64"
|
|
|
"encoding/json"
|
|
"encoding/json"
|
|
|
"encoding/pem"
|
|
"encoding/pem"
|
|
|
"errors"
|
|
"errors"
|
|
|
"fmt"
|
|
"fmt"
|
|
|
|
|
+ "math/big"
|
|
|
|
|
+ "net/url"
|
|
|
"path/filepath"
|
|
"path/filepath"
|
|
|
"strings"
|
|
"strings"
|
|
|
|
|
+ "time"
|
|
|
|
|
|
|
|
|
|
+ corev1 "k8s.io/api/core/v1"
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
|
+ "k8s.io/apimachinery/pkg/util/wait"
|
|
|
|
|
+ "k8s.io/client-go/kubernetes"
|
|
|
|
|
+ "k8s.io/client-go/rest"
|
|
|
|
|
|
|
|
// nolint
|
|
// nolint
|
|
|
|
|
|
|
@@ -38,6 +48,31 @@ import (
|
|
|
"github.com/external-secrets/external-secrets-e2e/framework/util"
|
|
"github.com/external-secrets/external-secrets-e2e/framework/util"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+const (
|
|
|
|
|
+ // authenticatorServicePort is the port the authenticator-service binary listens on.
|
|
|
|
|
+ // This matches the CONJUR_AUTHENTICATOR_SERVICE_URL default of http://localhost:5681
|
|
|
|
|
+ // and must not collide with the conjur-oss container's own PORT (8080), since both
|
|
|
|
|
+ // containers share the pod's network namespace.
|
|
|
|
|
+ authenticatorServicePort = "5681"
|
|
|
|
|
+
|
|
|
|
|
+ // authenticatorServiceContainerName is the name of the sidecar container running
|
|
|
|
|
+ // the authenticator-service binary inside the conjur-oss pod.
|
|
|
|
|
+ authenticatorServiceContainerName = "authenticator-service"
|
|
|
|
|
+
|
|
|
|
|
+ // SpiffeTrustDomain is the SPIFFE trust domain used for the authn-cert authenticator
|
|
|
|
|
+ // operating in 'spiffe' host mode.
|
|
|
|
|
+ SpiffeTrustDomain = "eso-tests.example.org"
|
|
|
|
|
+
|
|
|
|
|
+ // SpiffeWorkloadID is the SPIFFE ID's workload path, embedded as a URI SAN in the
|
|
|
|
|
+ // SPIFFE client certificate (spiffe://<trust-domain>/<SpiffeWorkloadID>).
|
|
|
|
|
+ SpiffeWorkloadID = "vm-spiffe"
|
|
|
|
|
+
|
|
|
|
|
+ // SpiffeIdentityPath is the Conjur policy path that the authn-cert authenticator
|
|
|
|
|
+ // prepends to the SPIFFE workload path to derive the full host identity. The resulting
|
|
|
|
|
+ // host must exist at policy path SpiffeIdentityPath/SpiffeWorkloadID.
|
|
|
|
|
+ SpiffeIdentityPath = "eso-tests/spiffe"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
type Conjur struct {
|
|
type Conjur struct {
|
|
|
chart *HelmChart
|
|
chart *HelmChart
|
|
|
dataKey string
|
|
dataKey string
|
|
@@ -48,6 +83,10 @@ type Conjur struct {
|
|
|
|
|
|
|
|
AdminApiKey string
|
|
AdminApiKey string
|
|
|
ConjurServerCA []byte
|
|
ConjurServerCA []byte
|
|
|
|
|
+ ClientCert []byte
|
|
|
|
|
+ ClientKey []byte
|
|
|
|
|
+ SpiffeCert []byte
|
|
|
|
|
+ SpiffeKey []byte
|
|
|
portForwarder *PortForward
|
|
portForwarder *PortForward
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -60,14 +99,54 @@ func NewConjur() *Conjur {
|
|
|
Fail(err.Error())
|
|
Fail(err.Error())
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // Generate client certificates for cert-based authentication.
|
|
|
|
|
+ // The CN "vm-01" matches the host identity in the cert host policy.
|
|
|
|
|
+ rootBlock, _ := pem.Decode(rootPem)
|
|
|
|
|
+ if rootBlock == nil {
|
|
|
|
|
+ Fail("unable to decode root cert PEM")
|
|
|
|
|
+ }
|
|
|
|
|
+ rootCert, err := x509.ParseCertificate(rootBlock.Bytes)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ Fail(fmt.Sprintf("unable to parse root cert: %v", err))
|
|
|
|
|
+ }
|
|
|
|
|
+ rootKeyBlock, _ := pem.Decode(rootKeyPEM)
|
|
|
|
|
+ if rootKeyBlock == nil {
|
|
|
|
|
+ Fail("unable to decode root key PEM")
|
|
|
|
|
+ }
|
|
|
|
|
+ rootKey, err := x509.ParsePKCS1PrivateKey(rootKeyBlock.Bytes)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ Fail(fmt.Sprintf("unable to parse root key: %v", err))
|
|
|
|
|
+ }
|
|
|
|
|
+ clientCertPem, clientKeyRSA, err := genClientAuthCert(rootCert, rootKey, "vm-01", "")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ Fail(fmt.Sprintf("unable to generate client cert: %v", err))
|
|
|
|
|
+ }
|
|
|
|
|
+ clientKeyPem := pem.EncodeToMemory(&pem.Block{
|
|
|
|
|
+ Type: privatePemType,
|
|
|
|
|
+ Bytes: x509.MarshalPKCS1PrivateKey(clientKeyRSA),
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // Generate a second client certificate carrying a SPIFFE URI SAN, used by the
|
|
|
|
|
+ // authn-cert authenticator's 'spiffe' host mode, where the authenticated host's
|
|
|
|
|
+ // identity is derived from the certificate itself rather than the request path.
|
|
|
|
|
+ spiffeCertPem, spiffeKeyRSA, err := genClientAuthCert(rootCert, rootKey, "",
|
|
|
|
|
+ fmt.Sprintf("spiffe://%s/%s", SpiffeTrustDomain, SpiffeWorkloadID))
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ Fail(fmt.Sprintf("unable to generate spiffe client cert: %v", err))
|
|
|
|
|
+ }
|
|
|
|
|
+ spiffeKeyPem := pem.EncodeToMemory(&pem.Block{
|
|
|
|
|
+ Type: privatePemType,
|
|
|
|
|
+ Bytes: x509.MarshalPKCS1PrivateKey(spiffeKeyRSA),
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
return &Conjur{
|
|
return &Conjur{
|
|
|
dataKey: dataKey,
|
|
dataKey: dataKey,
|
|
|
chart: &HelmChart{
|
|
chart: &HelmChart{
|
|
|
Namespace: "conjur",
|
|
Namespace: "conjur",
|
|
|
ReleaseName: "conjur-conjur",
|
|
ReleaseName: "conjur-conjur",
|
|
|
Chart: fmt.Sprintf("%s/conjur-oss", repo),
|
|
Chart: fmt.Sprintf("%s/conjur-oss", repo),
|
|
|
- // Use latest version of Conjur OSS. To pin to a specific version, uncomment the following line.
|
|
|
|
|
- // ChartVersion: "2.0.7",
|
|
|
|
|
|
|
+ // Pinned to the current latest conjur-oss chart release. Bump alongside conjurReleaseVersion.
|
|
|
|
|
+ ChartVersion: "2.1.1",
|
|
|
Repo: ChartRepo{
|
|
Repo: ChartRepo{
|
|
|
Name: repo,
|
|
Name: repo,
|
|
|
URL: "https://cyberark.github.io/helm-charts",
|
|
URL: "https://cyberark.github.io/helm-charts",
|
|
@@ -87,27 +166,102 @@ func NewConjur() *Conjur {
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
- Namespace: "conjur",
|
|
|
|
|
|
|
+ Namespace: "conjur",
|
|
|
|
|
+ ClientCert: clientCertPem,
|
|
|
|
|
+ ClientKey: clientKeyPem,
|
|
|
|
|
+ SpiffeCert: spiffeCertPem,
|
|
|
|
|
+ SpiffeKey: spiffeKeyPem,
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// Install sets up the conjur-oss chart, the authenticator-service sidecar, and configures
|
|
|
|
|
+// the authenticators. The install-then-patch-then-exec sequence spans several kind/kubelet
|
|
|
|
|
+// eventually-consistent steps (Deployment rollout, pod scheduling, kubelet exec/attach
|
|
|
|
|
+// readiness); under load these can intermittently race even with generous waits at each
|
|
|
|
|
+// step (e.g. a pod reported Ready via the API can still be replaced by a subsequent
|
|
|
|
|
+// reconcile before it is actually exec-attachable). Rather than chase every individual
|
|
|
|
|
+// timing window, retry the whole sequence a few times, tearing down between attempts.
|
|
|
func (l *Conjur) Install() error {
|
|
func (l *Conjur) Install() error {
|
|
|
- err := l.chart.Install()
|
|
|
|
|
- if err != nil {
|
|
|
|
|
|
|
+ const maxAttempts = 3
|
|
|
|
|
+ var lastErr error
|
|
|
|
|
+ for attempt := 1; attempt <= maxAttempts; attempt++ {
|
|
|
|
|
+ if attempt > 1 {
|
|
|
|
|
+ By(fmt.Sprintf("retrying conjur install (attempt %d/%d) after: %v", attempt, maxAttempts, lastErr))
|
|
|
|
|
+ if err := l.teardown(); err != nil {
|
|
|
|
|
+ return fmt.Errorf("unable to tear down conjur before retry: %w", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if lastErr = l.installOnce(); lastErr == nil {
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return fmt.Errorf("conjur install failed after %d attempts: %w", maxAttempts, lastErr)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (l *Conjur) installOnce() error {
|
|
|
|
|
+ if err := l.chart.Install(); err != nil {
|
|
|
return err
|
|
return err
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- err = l.initConjur()
|
|
|
|
|
- if err != nil {
|
|
|
|
|
|
|
+ if err := l.patchAuthenticatorServiceSidecar(); err != nil {
|
|
|
return err
|
|
return err
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- err = l.configureConjur()
|
|
|
|
|
- if err != nil {
|
|
|
|
|
|
|
+ if err := l.initConjur(); err != nil {
|
|
|
return err
|
|
return err
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return nil
|
|
|
|
|
|
|
+ return l.configureConjur()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// teardown removes the conjur-oss release and namespace so installOnce can run again from
|
|
|
|
|
+// a clean slate. Errors are ignored: the namespace may not exist yet if chart.Install()
|
|
|
|
|
+// itself failed on a previous attempt.
|
|
|
|
|
+func (l *Conjur) teardown() error {
|
|
|
|
|
+ if l.portForwarder != nil {
|
|
|
|
|
+ l.portForwarder.Close()
|
|
|
|
|
+ l.portForwarder = nil
|
|
|
|
|
+ }
|
|
|
|
|
+ _ = l.chart.Uninstall()
|
|
|
|
|
+ err := l.chart.config.KubeClientSet.CoreV1().Namespaces().Delete(GinkgoT().Context(), l.chart.Namespace, metav1.DeleteOptions{})
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil //nolint:nilerr // namespace may already be gone; nothing to clean up.
|
|
|
|
|
+ }
|
|
|
|
|
+ return util.WaitForKubeNamespaceNotExist(l.chart.Namespace, l.chart.config.KubeClientSet)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// retryExecCmd retries util.ExecCmdWithContainer briefly to absorb the window where a
|
|
|
|
|
+// freshly-created pod reports Ready via the API before the kubelet's exec/attach endpoint
|
|
|
|
|
+// for its containers is actually available.
|
|
|
|
|
+func retryExecCmd(client kubernetes.Interface, config *rest.Config, podName, containerName, namespace, command string) error {
|
|
|
|
|
+ var lastErr error
|
|
|
|
|
+ for i := 0; i < 10; i++ {
|
|
|
|
|
+ if i > 0 {
|
|
|
|
|
+ time.Sleep(1 * time.Second)
|
|
|
|
|
+ }
|
|
|
|
|
+ if _, lastErr = util.ExecCmdWithContainer(client, config, podName, containerName, namespace, command); lastErr == nil {
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return lastErr
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// waitForConjurServiceReady polls the Conjur Service URL from inside the cluster (via exec
|
|
|
|
|
+// into the conjur-oss pod) until it gets a real HTTP response, absorbing kube-proxy
|
|
|
|
|
+// Endpoints-sync lag after the sidecar rollout. curl's own TLS verification is disabled
|
|
|
|
|
+// (-k) because this only checks that the Service routes to a live, responsive nginx; actual
|
|
|
|
|
+// certificate validation is exercised by the real authentication flows later.
|
|
|
|
|
+func (l *Conjur) waitForConjurServiceReady() error {
|
|
|
|
|
+ cmd := fmt.Sprintf("curl -sk -o /dev/null -w '%%{http_code}' %s/status", l.ConjurURL)
|
|
|
|
|
+ return wait.PollUntilContextTimeout(GinkgoT().Context(), 1*time.Second, 2*time.Minute, true, func(ctx context.Context) (bool, error) {
|
|
|
|
|
+ out, err := util.ExecCmdWithContainer(l.chart.config.KubeClientSet, l.chart.config.KubeConfig,
|
|
|
|
|
+ l.PodName, "conjur-oss", l.Namespace, cmd)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return false, nil
|
|
|
|
|
+ }
|
|
|
|
|
+ return strings.Contains(out, "200"), nil
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (l *Conjur) initConjur() error {
|
|
func (l *Conjur) initConjur() error {
|
|
@@ -128,12 +282,12 @@ func (l *Conjur) initConjur() error {
|
|
|
}
|
|
}
|
|
|
l.ConjurServerCA = caCertSecret.Data["tls.crt"]
|
|
l.ConjurServerCA = caCertSecret.Data["tls.crt"]
|
|
|
|
|
|
|
|
- // Create "default" account
|
|
|
|
|
- _, err = util.ExecCmdWithContainer(
|
|
|
|
|
- l.chart.config.KubeClientSet,
|
|
|
|
|
- l.chart.config.KubeConfig,
|
|
|
|
|
- l.PodName, "conjur-oss", l.Namespace, "conjurctl account create default")
|
|
|
|
|
- if err != nil {
|
|
|
|
|
|
|
+ // Create "default" account. A freshly-created pod can report Ready in the API before
|
|
|
|
|
+ // the kubelet's exec/attach endpoint for its containers is actually wired up, so the
|
|
|
|
|
+ // first exec attempt against it can fail with "container not found" even though the
|
|
|
|
|
+ // container is running; retry briefly to absorb that race.
|
|
|
|
|
+ if err := retryExecCmd(l.chart.config.KubeClientSet, l.chart.config.KubeConfig,
|
|
|
|
|
+ l.PodName, "conjur-oss", l.Namespace, "conjurctl account create default"); err != nil {
|
|
|
return fmt.Errorf("error initializing conjur: %w", err)
|
|
return fmt.Errorf("error initializing conjur: %w", err)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -162,6 +316,18 @@ func (l *Conjur) initConjur() error {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
l.ConjurURL = fmt.Sprintf("https://conjur-conjur-conjur-oss.%s.svc.cluster.local", l.Namespace)
|
|
l.ConjurURL = fmt.Sprintf("https://conjur-conjur-conjur-oss.%s.svc.cluster.local", l.Namespace)
|
|
|
|
|
+
|
|
|
|
|
+ // The ExternalSecret controller reaches Conjur through this Service URL, which routes
|
|
|
|
|
+ // through kube-proxy to whichever pod is currently in the Service's Endpoints. That
|
|
|
|
|
+ // Endpoints list is reconciled asynchronously and can lag behind the pod-level readiness
|
|
|
|
|
+ // checks in patchAuthenticatorServiceSidecar's rollout wait, especially right after the
|
|
|
|
|
+ // sidecar-triggered rolling update. Probe through the Service URL itself (from inside the
|
|
|
|
|
+ // cluster, exercising the same DNS/ClusterIP/kube-proxy path the controller will use)
|
|
|
|
|
+ // before proceeding, so tests don't race a stale or not-yet-synced endpoint.
|
|
|
|
|
+ if err := l.waitForConjurServiceReady(); err != nil {
|
|
|
|
|
+ return fmt.Errorf("error waiting for conjur service to be reachable: %w", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
cfg := conjurapi.Config{
|
|
cfg := conjurapi.Config{
|
|
|
Account: "default",
|
|
Account: "default",
|
|
|
ApplianceURL: fmt.Sprintf("https://localhost:%d", l.portForwarder.localPort),
|
|
ApplianceURL: fmt.Sprintf("https://localhost:%d", l.portForwarder.localPort),
|
|
@@ -179,6 +345,164 @@ func (l *Conjur) initConjur() error {
|
|
|
return nil
|
|
return nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// conjurReleaseVersion is the pinned cyberark/conjur GitHub release used to fetch the
|
|
|
|
|
+// authenticator-service binary. Bump this when a newer release is needed.
|
|
|
|
|
+const conjurReleaseVersion = "v1.27.0"
|
|
|
|
|
+
|
|
|
|
|
+// authenticatorServiceInitScript downloads the authenticator-service binary matching the
|
|
|
|
|
+// node's architecture from the pinned cyberark/conjur GitHub release (see
|
|
|
|
|
+// conjurReleaseVersion), and writes its minimal config file, into a shared emptyDir volume
|
|
|
|
|
+// mounted by the sidecar container.
|
|
|
|
|
+//
|
|
|
|
|
+// The authenticator-service is not distributed as a container image, only as raw
|
|
|
|
|
+// per-architecture binaries attached to GitHub releases (see AUTHENTICATOR_SERVICE.md in
|
|
|
|
|
+// the cyberark/conjur repo), so it must be fetched at runtime rather than referenced by tag.
|
|
|
|
|
+const authenticatorServiceInitScript = `set -e
|
|
|
|
|
+ARCH=$(uname -m)
|
|
|
|
|
+case "$ARCH" in
|
|
|
|
|
+ x86_64) BIN_ARCH=amd64 ;;
|
|
|
|
|
+ aarch64|arm64) BIN_ARCH=arm64 ;;
|
|
|
|
|
+ *) echo "unsupported architecture: $ARCH" >&2; exit 1 ;;
|
|
|
|
|
+esac
|
|
|
|
|
+DOWNLOAD_URL=$(curl -s https://api.github.com/repos/cyberark/conjur/releases/tags/` + conjurReleaseVersion + `\
|
|
|
|
|
+ | grep -o "\"browser_download_url\": *\"[^\"]*authenticator_linux_[0-9]*_${BIN_ARCH}\"" \
|
|
|
|
|
+ | head -1 | sed -E 's/.*"(https:[^"]+)".*/\1/')
|
|
|
|
|
+if [ -z "$DOWNLOAD_URL" ]; then
|
|
|
|
|
+ echo "unable to determine authenticator-service download URL for arch ${BIN_ARCH}" >&2
|
|
|
|
|
+ exit 1
|
|
|
|
|
+fi
|
|
|
|
|
+echo "downloading authenticator-service from ${DOWNLOAD_URL}"
|
|
|
|
|
+curl -sL -o /authsvc/authenticator-service "$DOWNLOAD_URL"
|
|
|
|
|
+chmod +x /authsvc/authenticator-service
|
|
|
|
|
+printf '{"port": "%s", "http_timeout": "10s"}' "` + authenticatorServicePort + `" > /authsvc/config.json`
|
|
|
|
|
+
|
|
|
|
|
+// patchAuthenticatorServiceSidecar adds an authenticator-service sidecar to the conjur-oss
|
|
|
|
|
+// deployment and enables the feature flag that lets Conjur delegate authn-cert credential
|
|
|
|
|
+// validation to it. Conjur OSS's authn-cert authenticator does not validate client
|
|
|
|
|
+// certificates itself; it calls out to this separate HTTP service (see
|
|
|
|
|
+// AUTHENTICATOR_SERVICE.md and CERTIFICATE_AUTH.md in the cyberark/conjur repo). The
|
|
|
|
|
+// conjur-oss Helm chart has no hook for adding sidecars, so the running Deployment is
|
|
|
|
|
+// patched directly after the initial chart install.
|
|
|
|
|
+func (l *Conjur) patchAuthenticatorServiceSidecar() error {
|
|
|
|
|
+ By("patching conjur-oss deployment with authenticator-service sidecar")
|
|
|
|
|
+ clientSet := l.chart.config.KubeClientSet
|
|
|
|
|
+ deploymentName := fmt.Sprintf("%s-conjur-oss", l.chart.ReleaseName)
|
|
|
|
|
+
|
|
|
|
|
+ deployment, err := clientSet.AppsV1().Deployments(l.Namespace).Get(GinkgoT().Context(), deploymentName, metav1.GetOptions{})
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return fmt.Errorf("unable to get conjur-oss deployment: %w", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ binVolume := corev1.Volume{
|
|
|
|
|
+ Name: "authsvc-bin",
|
|
|
|
|
+ VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}},
|
|
|
|
|
+ }
|
|
|
|
|
+ binVolumeMount := corev1.VolumeMount{
|
|
|
|
|
+ Name: binVolume.Name,
|
|
|
|
|
+ MountPath: "/authsvc",
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, binVolume)
|
|
|
|
|
+ deployment.Spec.Template.Spec.InitContainers = append(deployment.Spec.Template.Spec.InitContainers, corev1.Container{
|
|
|
|
|
+ Name: "authenticator-service-fetch",
|
|
|
|
|
+ Image: "curlimages/curl:8.11.0",
|
|
|
|
|
+ Command: []string{"/bin/sh", "-c"},
|
|
|
|
|
+ Args: []string{authenticatorServiceInitScript},
|
|
|
|
|
+ VolumeMounts: []corev1.VolumeMount{binVolumeMount},
|
|
|
|
|
+ })
|
|
|
|
|
+ deployment.Spec.Template.Spec.Containers = append(deployment.Spec.Template.Spec.Containers, corev1.Container{
|
|
|
|
|
+ Name: authenticatorServiceContainerName,
|
|
|
|
|
+ Image: "gcr.io/distroless/static-debian12:latest",
|
|
|
|
|
+ Command: []string{"/authsvc/authenticator-service"},
|
|
|
|
|
+ Args: []string{"-c", "/authsvc/config.json"},
|
|
|
|
|
+ VolumeMounts: []corev1.VolumeMount{binVolumeMount},
|
|
|
|
|
+ Ports: []corev1.ContainerPort{
|
|
|
|
|
+ {Name: "authsvc", ContainerPort: 5681},
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ for i := range deployment.Spec.Template.Spec.Containers {
|
|
|
|
|
+ c := &deployment.Spec.Template.Spec.Containers[i]
|
|
|
|
|
+ if c.Name != "conjur-oss" {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ c.Env = append(c.Env,
|
|
|
|
|
+ corev1.EnvVar{Name: "CONJUR_FEATURE_AUTHENTICATOR_SERVICE_ENABLED", Value: "true"},
|
|
|
|
|
+ corev1.EnvVar{Name: "CONJUR_AUTHENTICATOR_SERVICE_URL", Value: "http://localhost:" + authenticatorServicePort},
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ updated, err := clientSet.AppsV1().Deployments(l.Namespace).Update(GinkgoT().Context(), deployment, metav1.UpdateOptions{})
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return fmt.Errorf("unable to patch conjur-oss deployment with authenticator-service sidecar: %w", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Wait for the rollout to fully complete (old ReplicaSet scaled to 0) rather than just
|
|
|
|
|
+ // waiting for any pod matching the label to be ready. During the rolling update, the
|
|
|
|
|
+ // old (pre-sidecar) pod can remain Ready while terminating; a label-only wait can race
|
|
|
|
|
+ // and hand initConjur a pod whose conjur-oss container is already gone.
|
|
|
|
|
+ if err := waitForDeploymentRollout(clientSet, l.Namespace, deploymentName, updated.Generation); err != nil {
|
|
|
|
|
+ return fmt.Errorf("error waiting for conjur-oss rollout after sidecar patch: %w", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func waitForDeploymentRollout(clientSet kubernetes.Interface, namespace, name string, generation int64) error {
|
|
|
|
|
+ if err := wait.PollUntilContextTimeout(GinkgoT().Context(), 1*time.Second, 5*time.Minute, true, func(ctx context.Context) (bool, error) {
|
|
|
|
|
+ dep, err := clientSet.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{})
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return false, nil
|
|
|
|
|
+ }
|
|
|
|
|
+ if dep.Status.ObservedGeneration < generation {
|
|
|
|
|
+ return false, nil
|
|
|
|
|
+ }
|
|
|
|
|
+ replicas := int32(1)
|
|
|
|
|
+ if dep.Spec.Replicas != nil {
|
|
|
|
|
+ replicas = *dep.Spec.Replicas
|
|
|
|
|
+ }
|
|
|
|
|
+ return dep.Status.UpdatedReplicas == replicas &&
|
|
|
|
|
+ dep.Status.Replicas == replicas &&
|
|
|
|
|
+ dep.Status.AvailableReplicas == replicas, nil
|
|
|
|
|
+ }); err != nil {
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Deployment status is computed asynchronously by the deployment controller and can
|
|
|
|
|
+ // briefly report AvailableReplicas==1 while the old pre-sidecar pod is still
|
|
|
|
|
+ // Running-but-terminating (Kubelet has not removed it yet). Cross-check pod state
|
|
|
|
|
+ // directly: require exactly one pod for this Deployment that is not terminating and
|
|
|
|
|
+ // has all 3 containers (conjur-oss, nginx, authenticator-service) ready, so
|
|
|
|
|
+ // initConjur never execs into a pod that's mid-teardown.
|
|
|
|
|
+ return wait.PollUntilContextTimeout(GinkgoT().Context(), 1*time.Second, 5*time.Minute, true, func(ctx context.Context) (bool, error) {
|
|
|
|
|
+ pods, err := clientSet.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{LabelSelector: "app=conjur-oss"})
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return false, nil
|
|
|
|
|
+ }
|
|
|
|
|
+ live := 0
|
|
|
|
|
+ for i := range pods.Items {
|
|
|
|
|
+ pod := &pods.Items[i]
|
|
|
|
|
+ if pod.DeletionTimestamp != nil {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ if pod.Status.Phase != corev1.PodRunning {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ if len(pod.Status.ContainerStatuses) != 3 {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ allReady := true
|
|
|
|
|
+ for _, cs := range pod.Status.ContainerStatuses {
|
|
|
|
|
+ allReady = allReady && cs.Ready
|
|
|
|
|
+ }
|
|
|
|
|
+ if allReady {
|
|
|
|
|
+ live++
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return live == 1, nil
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
func (l *Conjur) configureConjur() error {
|
|
func (l *Conjur) configureConjur() error {
|
|
|
By("configuring conjur")
|
|
By("configuring conjur")
|
|
|
// Construct Conjur policy for authn-jwt. This uses the token-app-property "sub" to
|
|
// Construct Conjur policy for authn-jwt. This uses the token-app-property "sub" to
|
|
@@ -215,6 +539,54 @@ func (l *Conjur) configureConjur() error {
|
|
|
return fmt.Errorf("unable to load authn-jwt policy: %w", err)
|
|
return fmt.Errorf("unable to load authn-jwt policy: %w", err)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // Construct Conjur policy for two authn-cert authenticators. Both delegate client
|
|
|
|
|
+ // certificate validation to the authenticator-service sidecar (see
|
|
|
|
|
+ // patchAuthenticatorServiceSidecar). The 'ca-cert' variable name is fixed by the
|
|
|
|
|
+ // authenticator; it is not a policy-defined path segment.
|
|
|
|
|
+ //
|
|
|
|
|
+ // 'eso-tests' operates in 'spiffe' host mode: the authenticated host's identity is
|
|
|
|
|
+ // derived from the SPIFFE URI SAN in the client certificate (see genSpiffeCert), rather
|
|
|
|
|
+ // than from a role identifier in the request path. This is the mode exercised when the
|
|
|
|
|
+ // ExternalSecret store omits Auth.Cert.HostID.
|
|
|
|
|
+ //
|
|
|
|
|
+ // 'eso-tests-hostid' operates in the default 'request' host mode, mirroring the
|
|
|
|
|
+ // authn-jwt/eso-tests-hostid authenticator: the host identity is supplied explicitly via
|
|
|
|
|
+ // Auth.Cert.HostID. A single authenticator cannot serve both modes because host-mode is
|
|
|
|
|
+ // a per-authenticator setting, not a per-request one.
|
|
|
|
|
+ policy = `- !policy
|
|
|
|
|
+ id: conjur/authn-cert/eso-tests
|
|
|
|
|
+ body:
|
|
|
|
|
+ - !webservice
|
|
|
|
|
+ - !variable ca-cert
|
|
|
|
|
+ - !variable host-mode
|
|
|
|
|
+ - !variable trust-domain
|
|
|
|
|
+ - !variable identity-path
|
|
|
|
|
+
|
|
|
|
|
+- !policy
|
|
|
|
|
+ id: conjur/authn-cert/eso-tests-hostid
|
|
|
|
|
+ body:
|
|
|
|
|
+ - !webservice
|
|
|
|
|
+ - !variable ca-cert`
|
|
|
|
|
+
|
|
|
|
|
+ _, err = l.ConjurClient.LoadPolicy(conjurapi.PolicyModePost, "root", strings.NewReader(policy))
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return fmt.Errorf("unable to load authn-cert policy: %w", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Set the CA certificate and spiffe-mode variables for the authn-cert authenticators
|
|
|
|
|
+ certSecrets := map[string]string{
|
|
|
|
|
+ "conjur/authn-cert/eso-tests/ca-cert": string(l.ConjurServerCA),
|
|
|
|
|
+ "conjur/authn-cert/eso-tests/host-mode": "spiffe",
|
|
|
|
|
+ "conjur/authn-cert/eso-tests/trust-domain": SpiffeTrustDomain,
|
|
|
|
|
+ "conjur/authn-cert/eso-tests/identity-path": SpiffeIdentityPath,
|
|
|
|
|
+ "conjur/authn-cert/eso-tests-hostid/ca-cert": string(l.ConjurServerCA),
|
|
|
|
|
+ }
|
|
|
|
|
+ for secretPath, secretValue := range certSecrets {
|
|
|
|
|
+ if err := l.ConjurClient.AddSecret(secretPath, secretValue); err != nil {
|
|
|
|
|
+ return fmt.Errorf("unable to add secret %s: %w", secretPath, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// Fetch the jwks info from the k8s cluster
|
|
// Fetch the jwks info from the k8s cluster
|
|
|
pubKeysJson, issuer, err := l.fetchJWKSandIssuer()
|
|
pubKeysJson, issuer, err := l.fetchJWKSandIssuer()
|
|
|
if err != nil {
|
|
if err != nil {
|
|
@@ -303,6 +675,50 @@ func genCertificates(namespace, serviceName string) ([]byte, []byte, []byte, []b
|
|
|
return rootPem, rootKeyPEM, serverPem, serverKeyPem, err
|
|
return rootPem, rootKeyPEM, serverPem, serverKeyPem, err
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// genSpiffeCert generates a client certificate carrying the given SPIFFE ID as a URI SAN,
|
|
|
|
|
+// signed by signingCert/signingKey. This is used by the authn-cert authenticator's 'spiffe'
|
|
|
|
|
+// host mode, which derives the authenticated host's identity from the certificate's URI SAN
|
|
|
|
|
+// rather than from a role identifier in the request path.
|
|
|
|
|
+// genClientAuthCert generates a client certificate for mutual TLS (authn-cert). cn sets the
|
|
|
|
|
+// Common Name (used by 'request' host mode's cn restriction annotation); spiffeID, if
|
|
|
|
|
+// non-empty, is embedded as a URI SAN (used by 'spiffe' host mode).
|
|
|
|
|
+func genClientAuthCert(signingCert *x509.Certificate, signingKey *rsa.PrivateKey, cn, spiffeID string) ([]byte, *rsa.PrivateKey, error) {
|
|
|
|
|
+ var uris []*url.URL
|
|
|
|
|
+ if spiffeID != "" {
|
|
|
|
|
+ uri, err := url.Parse(spiffeID)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, nil, fmt.Errorf("unable to parse spiffe id: %w", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ uris = []*url.URL{uri}
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ pkey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ tpl := x509.Certificate{
|
|
|
|
|
+ Subject: pkix.Name{
|
|
|
|
|
+ Country: []string{"/dev/null"},
|
|
|
|
|
+ Organization: []string{"External Secrets ACME"},
|
|
|
|
|
+ CommonName: cn,
|
|
|
|
|
+ },
|
|
|
|
|
+ SerialNumber: big.NewInt(1),
|
|
|
|
|
+ NotBefore: time.Now(),
|
|
|
|
|
+ NotAfter: time.Now().Add(time.Hour),
|
|
|
|
|
+ // DigitalSignature is required for the client to sign the TLS handshake during
|
|
|
|
|
+ // mutual TLS; CRLSign (used by the sibling genPeerCert for server certs) does not
|
|
|
|
|
+ // cover that purpose and nginx's strict (critical) key usage check rejects the
|
|
|
|
|
+ // handshake outright if it's missing.
|
|
|
|
|
+ KeyUsage: x509.KeyUsageDigitalSignature,
|
|
|
|
|
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
|
|
|
|
+ IsCA: false,
|
|
|
|
|
+ MaxPathLenZero: true,
|
|
|
|
|
+ URIs: uris,
|
|
|
|
|
+ }
|
|
|
|
|
+ _, certPEM, err := genCert(&tpl, signingCert, &pkey.PublicKey, signingKey)
|
|
|
|
|
+ return certPEM, pkey, err
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
func (l *Conjur) Logs() error {
|
|
func (l *Conjur) Logs() error {
|
|
|
return l.chart.Logs()
|
|
return l.chart.Logs()
|
|
|
}
|
|
}
|