Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

operator: add mTLS authentication to tenants #9906

Merged
merged 15 commits into from
Jul 18, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 36 additions & 6 deletions operator/apis/loki/v1/lokistack_types.go
JoaoBraveCoding marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,22 @@ type OIDCSpec struct {
UsernameClaim string `json:"usernameClaim,omitempty"`
}

// MTLSSpec specifies mTLS configuration parameters.
type MTLSSpec struct {
// Secret defines the spec for the tls.key, tls.crt for tenant's authentication.
//
// +required
// +kubebuilder:validation:Required
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Cert Secret"
CertSecret *TenantSecretSpec `json:"certSecret"`
// CASpec defines the spec for the custom CA for tenant's authentication.
//
// +required
// +kubebuilder:validation:Required
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="CA ConfigMap"
CASpec *CASpec `json:"caSpec"`
JoaoBraveCoding marked this conversation as resolved.
Show resolved Hide resolved
}

// AuthenticationSpec defines the oidc configuration per tenant for lokiStack Gateway component.
type AuthenticationSpec struct {
// TenantName defines the name of the tenant.
Expand All @@ -199,10 +215,15 @@ type AuthenticationSpec struct {
TenantID string `json:"tenantId"`
// OIDC defines the spec for the OIDC tenant's authentication.
//
// +required
// +kubebuilder:validation:Required
// +optional
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="OIDC Configuration"
OIDC *OIDCSpec `json:"oidc"`
OIDC *OIDCSpec `json:"oidc,omitempty"`

// TLSConfig defines the spec for the mTLS tenant's authentication.
//
// +optional
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="mTLS Configuration"
MTLS *MTLSSpec `json:"mTLS,omitempty"`
}

// ModeType is the authentication/authorization mode in which LokiStack Gateway will be configured.
Expand Down Expand Up @@ -414,12 +435,11 @@ type HashRingSpec struct {
MemberList *MemberListSpec `json:"memberlist,omitempty"`
}

// ObjectStorageTLSSpec is the TLS configuration for reaching the object storage endpoint.
type ObjectStorageTLSSpec struct {
type CASpec struct {
// Key is the data key of a ConfigMap containing a CA certificate.
// It needs to be in the same namespace as the LokiStack custom resource.
// If empty, it defaults to "service-ca.crt".
//
// If empty, it defaults to "service-ca.crt".
// +optional
// +kubebuilder:validation:optional
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="CA ConfigMap Key"
Expand All @@ -433,6 +453,11 @@ type ObjectStorageTLSSpec struct {
CA string `json:"caName"`
}

// ObjectStorageTLSSpec is the TLS configuration for reaching the object storage endpoint.
type ObjectStorageTLSSpec struct {
CASpec `json:",inline"`
}

// ObjectStorageSecretType defines the type of storage which can be used with the Loki cluster.
//
// +kubebuilder:validation:Enum=azure;gcs;s3;swift;alibabacloud;
Expand Down Expand Up @@ -926,8 +951,13 @@ const (
// ReasonMissingGatewayTenantSecret when the required tenant secret
// for authentication is missing.
ReasonMissingGatewayTenantSecret LokiStackConditionReason = "MissingGatewayTenantSecret"
// ReasonMissingGatewayTenantConfigMap when the required tenant configmap
// for authentication is missing.
ReasonMissingGatewayTenantConfigMap LokiStackConditionReason = "MissingGatewayTenantConfigMap"
// ReasonInvalidGatewayTenantSecret when the format of the secret is invalid.
ReasonInvalidGatewayTenantSecret LokiStackConditionReason = "InvalidGatewayTenantSecret"
// ReasonMissingGatewayAuthenticationConfig when the config for when a tenant is missing authentication config
ReasonMissingGatewayAuthenticationConfig LokiStackConditionReason = "MissingGatewayTenantAuthenticationConfig"
// ReasonInvalidTenantsConfiguration when the tenant configuration provided is invalid.
ReasonInvalidTenantsConfiguration LokiStackConditionReason = "InvalidTenantsConfiguration"
// ReasonMissingGatewayOpenShiftBaseDomain when the reconciler cannot lookup the OpenShift DNS base domain.
Expand Down
46 changes: 46 additions & 0 deletions operator/apis/loki/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion operator/apis/loki/v1beta1/lokistack_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -866,7 +866,9 @@ func (src *LokiStack) ConvertTo(dstRaw conversion.Hub) error {
var storageTLS *v1.ObjectStorageTLSSpec
if src.Spec.Storage.TLS != nil {
storageTLS = &v1.ObjectStorageTLSSpec{
CA: src.Spec.Storage.TLS.CA,
CASpec: v1.CASpec{
CA: src.Spec.Storage.TLS.CA,
},
}
}

Expand Down
1 change: 0 additions & 1 deletion operator/controllers/loki/lokistack_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ type LokiStackReconciler struct {
// +kubebuilder:rbac:groups=loki.grafana.com,resources=lokistacks/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=loki.grafana.com,resources=lokistacks/finalizers,verbs=update
// +kubebuilder:rbac:groups="",resources=pods;nodes;services;endpoints;configmaps;secrets;serviceaccounts,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
periklis marked this conversation as resolved.
Show resolved Hide resolved
// +kubebuilder:rbac:groups=apps,resources=deployments;statefulsets,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterrolebindings;clusterroles;roles;rolebindings,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=monitoring.coreos.com,resources=servicemonitors;prometheusrules,verbs=get;list;watch;create;update;delete
Expand Down
118 changes: 101 additions & 17 deletions operator/internal/handlers/internal/gateway/tenant_secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,36 +33,101 @@ func GetTenantSecrets(
)

for _, tenant := range stack.Spec.Tenants.Authentication {
key := client.ObjectKey{Name: tenant.OIDC.Secret.Name, Namespace: req.Namespace}
if err := k.Get(ctx, key, &gatewaySecret); err != nil {
if apierrors.IsNotFound(err) {
switch {
case tenant.OIDC != nil:
key := client.ObjectKey{Name: tenant.OIDC.Secret.Name, Namespace: req.Namespace}
if err := getSecret(ctx, k, tenant.TenantName, key, &gatewaySecret); err != nil {
return nil, err
}
oidcSecret, err := extractOIDCSecret(&gatewaySecret)
if err != nil {
return nil, &status.DegradedError{
Message: fmt.Sprintf("Missing secrets for tenant %s", tenant.TenantName),
Reason: lokiv1.ReasonMissingGatewayTenantSecret,
Message: "Invalid gateway tenant secret contents",
Reason: lokiv1.ReasonInvalidGatewayTenantSecret,
Requeue: true,
}
}
tenantSecrets = append(tenantSecrets, &manifests.TenantSecrets{
OIDCSecret: oidcSecret,
})
case tenant.MTLS != nil:
key := client.ObjectKey{Name: tenant.MTLS.CertSecret.Name, Namespace: req.Namespace}
if err := getSecret(ctx, k, tenant.TenantName, key, &gatewaySecret); err != nil {
return nil, err
}
cert, err := extractCert(&gatewaySecret)
if err != nil {
return nil, &status.DegradedError{
Message: "Invalid gateway tenant secret contents",
Reason: lokiv1.ReasonInvalidGatewayTenantSecret,
Requeue: true,
}
}
return nil, kverrors.Wrap(err, "failed to lookup lokistack gateway tenant secret",
"name", key)
}

var ts *manifests.TenantSecrets
ts, err := extractSecret(&gatewaySecret, tenant.TenantName)
if err != nil {
var configMap corev1.ConfigMap
key = client.ObjectKey{Name: tenant.MTLS.CASpec.CA, Namespace: req.Namespace}
if err = getConfigMap(ctx, k, tenant.TenantName, key, &configMap); err != nil {
return nil, err
}
ca, err := extractCA(&configMap, tenant.MTLS.CASpec.CAKey)
if err != nil {
return nil, &status.DegradedError{
Message: "Invalid gateway tenant secret contents",
Reason: lokiv1.ReasonInvalidGatewayTenantSecret,
JoaoBraveCoding marked this conversation as resolved.
Show resolved Hide resolved
Requeue: true,
}
}
tenantSecrets = append(tenantSecrets, &manifests.TenantSecrets{
TenantName: tenant.TenantName,
MTLSSecret: &manifests.MTLSSecret{
Cert: cert,
CA: ca,
},
})
default:
return nil, &status.DegradedError{
Message: "Invalid gateway tenant secret contents",
Message: "No gateway tenant authentication method provided",
Reason: lokiv1.ReasonInvalidGatewayTenantSecret,
Requeue: true,
}
}
tenantSecrets = append(tenantSecrets, ts)
}

return tenantSecrets, nil
}

// extractSecret reads a k8s secret into a manifest tenant secret struct if valid.
func extractSecret(s *corev1.Secret, tenantName string) (*manifests.TenantSecrets, error) {
func getSecret(ctx context.Context, k k8s.Client, tenantName string, key client.ObjectKey, s *corev1.Secret) error {
if err := k.Get(ctx, key, s); err != nil {
if apierrors.IsNotFound(err) {
return &status.DegradedError{
Message: fmt.Sprintf("Missing secrets for tenant %s", tenantName),
Reason: lokiv1.ReasonMissingGatewayTenantSecret,
Requeue: true,
}
}
return kverrors.Wrap(err, "failed to lookup lokistack gateway tenant secret",
"name", key)
}
return nil
}

func getConfigMap(ctx context.Context, k k8s.Client, tenantName string, key client.ObjectKey, cm *corev1.ConfigMap) error {
if err := k.Get(ctx, key, cm); err != nil {
if apierrors.IsNotFound(err) {
return &status.DegradedError{
Message: fmt.Sprintf("Missing configmap for tenant %s", tenantName),
Reason: lokiv1.ReasonMissingGatewayTenantConfigMap,
Requeue: true,
}
}
return kverrors.Wrap(err, "failed to lookup lokistack gateway tenant configMap",
"name", key)
}
return nil
}

// extractOIDCSecret reads a k8s secret into a manifest tenant secret struct if valid.
func extractOIDCSecret(s *corev1.Secret) (*manifests.OIDCSecret, error) {
// Extract and validate mandatory fields
clientID := s.Data["clientID"]
if len(clientID) == 0 {
Expand All @@ -71,10 +136,29 @@ func extractSecret(s *corev1.Secret, tenantName string) (*manifests.TenantSecret
clientSecret := s.Data["clientSecret"]
issuerCAPath := s.Data["issuerCAPath"]

return &manifests.TenantSecrets{
TenantName: tenantName,
return &manifests.OIDCSecret{
ClientID: string(clientID),
ClientSecret: string(clientSecret),
IssuerCAPath: string(issuerCAPath),
}, nil
}

// extractCert reads the TLSCertKey from a k8s secret of type SecretTypeTLS
func extractCert(s *corev1.Secret) (string, error) {
switch s.Type {
case corev1.SecretTypeTLS:
return string(s.Data[corev1.TLSCertKey]), nil
default:
return "", kverrors.New("missing fields, has to contain fields \"tls.key\" and \"tls.crt\"")
}
}

// extractCA reads the CA from a k8s configmap
func extractCA(cm *corev1.ConfigMap, key string) (string, error) {
// Extract and validate mandatory fields
ca := cm.Data[key]
if len(ca) == 0 {
return "", kverrors.New(fmt.Sprintf("missing %s field", key), "field", key)
}
return string(ca), nil
}
JoaoBraveCoding marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,12 @@ func TestGetTenantSecrets_StaticMode(t *testing.T) {

expected := []*manifests.TenantSecrets{
{
TenantName: "test",
ClientID: "test",
ClientSecret: "test",
IssuerCAPath: "/path/to/ca/file",
TenantName: "test",
OIDCSecret: &manifests.OIDCSecret{
ClientID: "test",
ClientSecret: "test",
IssuerCAPath: "/path/to/ca/file",
},
},
}
require.ElementsMatch(t, ts, expected)
Expand Down Expand Up @@ -134,10 +136,12 @@ func TestGetTenantSecrets_DynamicMode(t *testing.T) {

expected := []*manifests.TenantSecrets{
{
TenantName: "test",
ClientID: "test",
ClientSecret: "test",
IssuerCAPath: "/path/to/ca/file",
TenantName: "test",
OIDCSecret: &manifests.OIDCSecret{
ClientID: "test",
ClientSecret: "test",
IssuerCAPath: "/path/to/ca/file",
},
},
}
require.ElementsMatch(t, ts, expected)
Expand Down Expand Up @@ -174,7 +178,7 @@ func TestExtractSecret(t *testing.T) {
t.Run(tst.name, func(t *testing.T) {
t.Parallel()

_, err := extractSecret(tst.secret, tst.tenantName)
_, err := extractOIDCSecret(tst.secret)
if !tst.wantErr {
require.NoError(t, err)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -944,7 +944,9 @@ func TestCreateOrUpdateLokiStack_WhenMissingCAConfigMap_SetDegraded(t *testing.T
Type: lokiv1.ObjectStorageSecretS3,
},
TLS: &lokiv1.ObjectStorageTLSSpec{
CA: "not-existing",
lokiv1.CASpec{
CA: "not-existing",
},
},
},
},
Expand Down Expand Up @@ -1014,7 +1016,9 @@ func TestCreateOrUpdateLokiStack_WhenInvalidCAConfigMap_SetDegraded(t *testing.T
Type: lokiv1.ObjectStorageSecretS3,
},
TLS: &lokiv1.ObjectStorageTLSSpec{
CA: invalidCAConfigMap.Name,
lokiv1.CASpec{
CA: invalidCAConfigMap.Name,
},
},
},
},
Expand Down
Loading