[1.12] Change injector and sentry to GET daprsystem Configuration (#7116)

* Change injector and sentry to GET daprsystem Configuration

In Kubernetes mode, when the Injector is patching a Pod to determine
whether mTLS is enabled and Sentry on startup] fetches the global
daprsystem Configuration, they do so by listing all Configurations in
all namespaces and then match on the first Configuration with the name
daprsystem. As Namespaces are sorted alphabetically when listed, the
Configuration chosen by these services may not be the one located in the
Dapr System namespace. This means that a malicious actor, or by accident
a user of a Dapr enabled Kubernetes cluster, with write permissions to
Configurations in a namespace which is alphabetically higher than the
Dapr system namespace is able to override the global config for Sentry
and Injector. We can expect that users of Dapr in Kubernertes would be
able to have permissions to Configurations in order for them to control
their Dapr deployment configuration.

PR updates the injector and sentry services to GET the daprsystem
Configuration in the Dapr control plane namespace.

Signed-off-by: joshvanl <me@joshvanl.dev>

* Fix RBAC for new daprsystem config get

Signed-off-by: joshvanl <me@joshvanl.dev>

* Return error if NAMESPACE is set but empty in `CurrentNamespaceOrError`

Signed-off-by: joshvanl <me@joshvanl.dev>

---------

Signed-off-by: joshvanl <me@joshvanl.dev>
This commit is contained in:
Josh van Leeuwen 2023-11-01 20:15:13 +00:00 committed by joshvanl
parent 63fb8d27a3
commit 6af5d4367d
12 changed files with 136 additions and 34 deletions

View File

@ -26,7 +26,7 @@ rules:
resourceNames: ["dapr-sidecar-injector"]
{{- if not .Values.global.rbac.namespaced }}
- apiGroups: ["dapr.io"]
resources: ["configurations", "components"]
resources: ["components"]
verbs: [ "get", "list"]
{{- end }}
---
@ -63,9 +63,12 @@ rules:
resourceNames: ["dapr-trust-bundle"]
{{- if eq .Values.global.rbac.namespaced true }}
- apiGroups: ["dapr.io"]
resources: ["configurations", "components"]
resources: ["components"]
verbs: [ "get", "list"]
{{- end }}
- apiGroups: ["dapr.io"]
resources: ["configurations"]
verbs: [ "get" ]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1

View File

@ -64,11 +64,9 @@ rules:
resources: ["configmaps"]
verbs: ["get", "update", "watch", "list"]
resourceNames: ["dapr-trust-bundle"]
{{- if eq .Values.global.rbac.namespaced true }}
- apiGroups: ["dapr.io"]
resources: ["configurations"]
verbs: ["list"]
{{- end }}
verbs: ["list", "get", "watch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1

View File

@ -82,10 +82,15 @@ func main() {
log.Fatalf("Failed to get authentication uids from services accounts: %s", err)
}
namespace, err := security.CurrentNamespaceOrError()
if err != nil {
log.Fatalf("Failed to get current namespace: %s", err)
}
secProvider, err := security.New(ctx, security.Options{
SentryAddress: cfg.SentryAddress,
ControlPlaneTrustDomain: cfg.ControlPlaneTrustDomain,
ControlPlaneNamespace: security.CurrentNamespace(),
ControlPlaneNamespace: namespace,
TrustAnchorsFile: cfg.TrustAnchorsFile,
AppID: "dapr-injector",
MTLSEnabled: true,

View File

@ -21,6 +21,7 @@ import (
jsonpatch "github.com/evanphx/json-patch/v5"
admissionv1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
scheme "github.com/dapr/dapr/pkg/client/clientset/versioned"
@ -65,7 +66,7 @@ func (i *injector) getPodPatchOperations(ctx context.Context, ar *admissionv1.Ad
sidecar.GetInjectedComponentContainers = i.getInjectedComponentContainers
sidecar.Mode = injectorConsts.ModeKubernetes
sidecar.Namespace = ar.Request.Namespace
sidecar.MTLSEnabled = mTLSEnabled(i.daprClient)
sidecar.MTLSEnabled = mTLSEnabled(i.controlPlaneNamespace, i.daprClient)
sidecar.Identity = ar.Request.Namespace + ":" + pod.Spec.ServiceAccountName
sidecar.IgnoreEntrypointTolerations = i.config.GetIgnoreEntrypointTolerations()
sidecar.ImagePullPolicy = i.config.GetPullPolicy()
@ -103,20 +104,19 @@ func (i *injector) getPodPatchOperations(ctx context.Context, ar *admissionv1.Ad
return patch, nil
}
func mTLSEnabled(daprClient scheme.Interface) bool {
func mTLSEnabled(controlPlaneNamespace string, daprClient scheme.Interface) bool {
resp, err := daprClient.ConfigurationV1alpha1().
Configurations(metav1.NamespaceAll).
List(metav1.ListOptions{})
Configurations(controlPlaneNamespace).
Get(defaultConfig, metav1.GetOptions{})
if !apierrors.IsNotFound(err) {
log.Infof("Dapr system configuration '%s' does not exist; using default value %t for mTLSEnabled", defaultConfig, defaultMtlsEnabled)
return defaultMtlsEnabled
}
if err != nil {
log.Errorf("Failed to load dapr configuration from k8s, use default value %t for mTLSEnabled: %s", defaultMtlsEnabled, err)
return defaultMtlsEnabled
}
for _, c := range resp.Items {
if c.GetName() == defaultConfig {
return c.Spec.MTLSSpec.GetEnabled()
}
}
log.Infof("Dapr system configuration '%s' does not exist; using default value %t for mTLSEnabled", defaultConfig, defaultMtlsEnabled)
return defaultMtlsEnabled
return resp.Spec.MTLSSpec.GetEnabled()
}

View File

@ -31,9 +31,14 @@ func LoadConfiguration(ctx context.Context, name string, restConfig *rest.Config
return nil, fmt.Errorf("could not get Kubernetes API client: %w", err)
}
namespace, err := security.CurrentNamespaceOrError()
if err != nil {
return nil, err
}
var conf v1alpha1.Configuration
key := types.NamespacedName{
Namespace: security.CurrentNamespace(),
Namespace: namespace,
Name: name,
}
if err := client.Get(ctx, key, &conf); err != nil {

View File

@ -408,6 +408,21 @@ func CurrentNamespace() string {
return namespace
}
// CurrentNamespaceOrError returns the namespace of this workload. If current
// Namespace is not found, error.
func CurrentNamespaceOrError() (string, error) {
namespace, ok := os.LookupEnv("NAMESPACE")
if !ok {
return "", errors.New("'NAMESPACE' environment variable not set")
}
if len(namespace) == 0 {
return "", errors.New("'NAMESPACE' environment variable is empty")
}
return namespace, nil
}
// SentryID returns the SPIFFE ID of the sentry server.
func SentryID(sentryTrustDomain spiffeid.TrustDomain, sentryNamespace string) (spiffeid.ID, error) {
sentryID, err := spiffeid.FromSegments(sentryTrustDomain, "ns", sentryNamespace, "dapr-sentry")

View File

@ -169,3 +169,32 @@ func Test_Start(t *testing.T) {
}
})
}
func TestCurrentNamespace(t *testing.T) {
t.Run("error is namespace is not set", func(t *testing.T) {
osns, ok := os.LookupEnv("NAMESPACE")
os.Unsetenv("NAMESPACE")
t.Cleanup(func() {
if ok {
os.Setenv("NAMESPACE", osns)
}
})
ns, err := CurrentNamespaceOrError()
assert.Error(t, err)
assert.Empty(t, ns)
})
t.Run("error if namespace is set but empty", func(t *testing.T) {
t.Setenv("NAMESPACE", "")
ns, err := CurrentNamespaceOrError()
assert.Error(t, err)
assert.Empty(t, ns)
})
t.Run("returns namespace if set", func(t *testing.T) {
t.Setenv("NAMESPACE", "foo")
ns, err := CurrentNamespaceOrError()
assert.NoError(t, err)
assert.Equal(t, "foo", ns)
})
}

View File

@ -21,11 +21,13 @@ import (
"strings"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
scheme "github.com/dapr/dapr/pkg/client/clientset/versioned"
daprGlobalConfig "github.com/dapr/dapr/pkg/config"
sentryv1pb "github.com/dapr/dapr/pkg/proto/sentry/v1"
"github.com/dapr/dapr/pkg/security"
"github.com/dapr/dapr/utils"
)
@ -106,7 +108,7 @@ func getKubernetesConfig(configName string) (Config, error) {
return defaultConfig, err
}
list, err := daprClient.ConfigurationV1alpha1().Configurations(metaV1.NamespaceAll).List(metaV1.ListOptions{})
namespace, err := security.CurrentNamespaceOrError()
if err != nil {
return defaultConfig, err
}
@ -115,20 +117,28 @@ func getKubernetesConfig(configName string) (Config, error) {
configName = defaultDaprSystemConfigName
}
for _, i := range list.Items {
if i.GetName() == configName {
spec, _ := json.Marshal(i.Spec)
var configSpec daprGlobalConfig.ConfigurationSpec
json.Unmarshal(spec, &configSpec)
conf := daprGlobalConfig.Configuration{
Spec: configSpec,
}
return parseConfiguration(defaultConfig, &conf)
}
cfg, err := daprClient.ConfigurationV1alpha1().Configurations(namespace).Get(configName, metaV1.GetOptions{})
if apierrors.IsNotFound(err) {
return defaultConfig, errors.New("config CRD not found")
}
return defaultConfig, errors.New("config CRD not found")
if err != nil {
return defaultConfig, err
}
spec, err := json.Marshal(cfg.Spec)
if err != nil {
return defaultConfig, err
}
var configSpec daprGlobalConfig.ConfigurationSpec
if err := json.Unmarshal(spec, &configSpec); err != nil {
return defaultConfig, err
}
return parseConfiguration(defaultConfig, &daprGlobalConfig.Configuration{
Spec: configSpec,
})
}
func getSelfhostedConfig(configName string) (Config, error) {

View File

@ -0,0 +1,21 @@
apiVersion: v1
kind: Namespace
metadata:
name: aa
---
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: daprsystem
namespace: aa
spec:
metric:
enabled: true
metrics:
enabled: true
mtls:
allowedClockSkew: 0m
controlPlaneTrustDomain: cluster.local
enabled: false
sentryAddress: bad-address:1234
workloadCertTTL: 1ms

View File

@ -253,7 +253,7 @@ create-test-namespace:
kubectl create namespace $(DAPR_TEST_NAMESPACE)
delete-test-namespace:
kubectl delete namespace $(DAPR_TEST_NAMESPACE)
kubectl delete namespace $(DAPR_TEST_NAMESPACE) aa
setup-3rd-party: setup-helm-init setup-test-env-redis setup-test-env-kafka setup-test-env-zipkin setup-test-env-postgres
@ -584,6 +584,8 @@ setup-test-components: setup-app-configurations
$(KUBECTL) apply -f ./tests/config/externalinvocationcrd.yaml --namespace $(DAPR_TEST_NAMESPACE)
$(KUBECTL) apply -f ./tests/config/omithealthchecks_config.yaml --namespace $(DAPR_TEST_NAMESPACE)
$(KUBECTL) apply -f ./tests/config/external_invocation_http_endpoint_tls.yaml --namespace $(DAPR_TEST_NAMESPACE)
# Don't set namespace as Namespace is defind in the yaml.
$(KUBECTL) apply -f ./tests/config/ignore_daprsystem_config.yaml
# Show the installed components
$(KUBECTL) get components --namespace $(DAPR_TEST_NAMESPACE)

View File

@ -80,6 +80,20 @@ func WithDaprConfigurationGet(t *testing.T, ns, name string, config *configapi.C
return handleGetResource(t, "/apis/dapr.io/v1alpha1", "configurations", ns, name, config)
}
func WithDaprConfigurationGet(t *testing.T, config *configapi.Configuration) Option {
return func(o *options) {
obj, err := json.Marshal(config)
require.NoError(t, err)
o.handlers = append(o.handlers, handleRoute{
path: "/apis/dapr.io/v1alpha1/namespaces/" + config.Namespace + "/configurations/" + config.Name,
handler: func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
w.Write(obj)
},
})
}
}
func WithSecretGet(t *testing.T, ns, name string, secret *corev1.Secret) Option {
return handleGetResource(t, "/api/v1", "secrets", ns, name, secret)
}

View File

@ -42,7 +42,7 @@ func kubeAPI(t *testing.T, bundle ca.Bundle, namespace, serviceaccount string) *
MTLSSpec: &configapi.MTLSSpec{ControlPlaneTrustDomain: "integration.test.dapr.io"},
},
},
}}),
}),
prockube.WithSecretGet(t, "sentrynamespace", "dapr-trust-bundle", &corev1.Secret{
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Secret"},
ObjectMeta: metav1.ObjectMeta{Name: "dapr-trust-bundle"},