diff --git a/cmd/manager/main.go b/cmd/manager/main.go index b79e667b16..ca40ed0af8 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -16,9 +16,6 @@ import ( "strings" "time" - logstashv1alpha1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/logstash/v1alpha1" - "github.com/elastic/cloud-on-k8s/v2/pkg/controller/logstash" - "github.com/go-logr/logr" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -55,6 +52,7 @@ import ( entv1beta1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/enterprisesearch/v1beta1" kbv1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/kibana/v1" kbv1beta1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/kibana/v1beta1" + logstashv1alpha1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/logstash/v1alpha1" emsv1alpha1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/maps/v1alpha1" policyv1alpha1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/stackconfigpolicy/v1alpha1" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/agent" @@ -82,6 +80,7 @@ import ( "github.com/elastic/cloud-on-k8s/v2/pkg/controller/kibana" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/license" licensetrial "github.com/elastic/cloud-on-k8s/v2/pkg/controller/license/trial" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/logstash" lsvalidation "github.com/elastic/cloud-on-k8s/v2/pkg/controller/logstash/validation" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/maps" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/remoteca" diff --git a/config/crds/v1/all-crds.yaml b/config/crds/v1/all-crds.yaml index 171316ef8e..ef6f25d5d8 100644 --- a/config/crds/v1/all-crds.yaml +++ b/config/crds/v1/all-crds.yaml @@ -3826,6 +3826,10 @@ spec: description: Auth contains user authentication and authorization security settings for Elasticsearch. properties: + disableElasticUser: + description: DisableElasticUser disables the default elastic user + that is created by ECK. + type: boolean fileRealm: description: FileRealm to propagate to the Elasticsearch cluster. items: diff --git a/config/crds/v1/bases/elasticsearch.k8s.elastic.co_elasticsearches.yaml b/config/crds/v1/bases/elasticsearch.k8s.elastic.co_elasticsearches.yaml index 5b40dbd666..c6f1ed21b7 100644 --- a/config/crds/v1/bases/elasticsearch.k8s.elastic.co_elasticsearches.yaml +++ b/config/crds/v1/bases/elasticsearch.k8s.elastic.co_elasticsearches.yaml @@ -67,6 +67,10 @@ spec: description: Auth contains user authentication and authorization security settings for Elasticsearch. properties: + disableElasticUser: + description: DisableElasticUser disables the default elastic user + that is created by ECK. + type: boolean fileRealm: description: FileRealm to propagate to the Elasticsearch cluster. items: diff --git a/deploy/eck-operator/charts/eck-operator-crds/templates/all-crds.yaml b/deploy/eck-operator/charts/eck-operator-crds/templates/all-crds.yaml index 9c9aae83fe..613252bc11 100644 --- a/deploy/eck-operator/charts/eck-operator-crds/templates/all-crds.yaml +++ b/deploy/eck-operator/charts/eck-operator-crds/templates/all-crds.yaml @@ -3862,6 +3862,10 @@ spec: description: Auth contains user authentication and authorization security settings for Elasticsearch. properties: + disableElasticUser: + description: DisableElasticUser disables the default elastic user + that is created by ECK. + type: boolean fileRealm: description: FileRealm to propagate to the Elasticsearch cluster. items: diff --git a/docs/orchestrating-elastic-stack-applications/security/users-and-roles.asciidoc b/docs/orchestrating-elastic-stack-applications/security/users-and-roles.asciidoc index 82d233869d..0171dffa46 100644 --- a/docs/orchestrating-elastic-stack-applications/security/users-and-roles.asciidoc +++ b/docs/orchestrating-elastic-stack-applications/security/users-and-roles.asciidoc @@ -25,6 +25,25 @@ kubectl get secret quickstart-es-elastic-user -o go-template='{{.data.elastic | To rotate this password, refer to: <<{p}-rotate-credentials>>. +=== Disabling the default `elastic` user + +If your prefer to manage all users via SSO, for example using <<{p}-saml-authentication>> or OpenID Connect, you can disable the default `elastic` superuser by setting the `auth.disableElasticUser` field in the Elasticsearch resource to `true`: + +[source,yaml,subs="attributes"] +---- +apiVersion: elasticsearch.k8s.elastic.co/{eck_crd_version} +kind: Elasticsearch +metadata: + name: elasticsearch-sample +spec: + version: {version} + auth: + disableElasticUser: true + nodeSets: + - name: default + count: 1 +---- + == Creating custom users WARNING: Do not run the `elasticsearch-service-tokens` command inside an Elasticsearch Pod managed by the operator. This would overwrite the service account tokens used internally to authenticate the Elastic stack applications. diff --git a/docs/quickstart.asciidoc b/docs/quickstart.asciidoc index ded61d7999..23d7722ddc 100644 --- a/docs/quickstart.asciidoc +++ b/docs/quickstart.asciidoc @@ -162,13 +162,15 @@ quickstart-es-http ClusterIP 10.15.251.145 9200/TCP 34m . Get the credentials. + -A default user named `elastic` is automatically created with the password stored in a Kubernetes secret: +A default user named `elastic` is created by default with the password stored in a Kubernetes secret: + [source,sh] ---- PASSWORD=$(kubectl get secret quickstart-es-elastic-user -o go-template='{{.data.elastic | base64decode}}') ---- +NOTE: The `elastic` user creation can be disabled if desired. Check <<{p}-users-and-roles>> for more information. + . Request the Elasticsearch endpoint. + From inside the Kubernetes cluster: diff --git a/docs/reference/api-docs.asciidoc b/docs/reference/api-docs.asciidoc index 6c41f6b508..335c037412 100644 --- a/docs/reference/api-docs.asciidoc +++ b/docs/reference/api-docs.asciidoc @@ -1200,6 +1200,7 @@ Auth contains user authentication and authorization security settings for Elasti | Field | Description | *`roles`* __xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-elasticsearch-v1-rolesource[$$RoleSource$$] array__ | Roles to propagate to the Elasticsearch cluster. | *`fileRealm`* __xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-elasticsearch-v1-filerealmsource[$$FileRealmSource$$] array__ | FileRealm to propagate to the Elasticsearch cluster. +| *`disableElasticUser`* __boolean__ | DisableElasticUser disables the default elastic user that is created by ECK. |=== diff --git a/pkg/apis/elasticsearch/v1/elasticsearch_types.go b/pkg/apis/elasticsearch/v1/elasticsearch_types.go index eb9ec4ccdf..a1487dc44a 100644 --- a/pkg/apis/elasticsearch/v1/elasticsearch_types.go +++ b/pkg/apis/elasticsearch/v1/elasticsearch_types.go @@ -221,6 +221,8 @@ type Auth struct { Roles []RoleSource `json:"roles,omitempty"` // FileRealm to propagate to the Elasticsearch cluster. FileRealm []FileRealmSource `json:"fileRealm,omitempty"` + // DisableElasticUser disables the default elastic user that is created by ECK. + DisableElasticUser bool `json:"disableElasticUser,omitempty"` } // RoleSource references roles to create in the Elasticsearch cluster. diff --git a/pkg/controller/elasticsearch/user/predefined.go b/pkg/controller/elasticsearch/user/predefined.go index 6c08a6c0b1..1fecbf5de9 100644 --- a/pkg/controller/elasticsearch/user/predefined.go +++ b/pkg/controller/elasticsearch/user/predefined.go @@ -26,7 +26,6 @@ import ( const ( // ElasticUserName is the public-facing user. ElasticUserName = "elastic" - // ControllerUserName is the controller user to interact with ES. ControllerUserName = "elastic-internal" // MonitoringUserName is used for the Elasticsearch monitoring. @@ -35,6 +34,8 @@ const ( PreStopUserName = "elastic-internal-pre-stop" // ProbeUserName is used for the Elasticsearch readiness probe. ProbeUserName = "elastic-internal-probe" + // DiagnosticsUserName is used for the ECK diagnostics. + DiagnosticsUserName = "elastic-internal-diagnostics" ) // reconcileElasticUser reconciles a single secret holding the "elastic" user password. @@ -46,6 +47,9 @@ func reconcileElasticUser( userProvidedFileRealm filerealm.Realm, passwordHasher cryptutil.PasswordHasher, ) (users, error) { + if es.Spec.Auth.DisableElasticUser { + return nil, nil + } secretName := esv1.ElasticUserSecret(es.Name) // if user has set up the elastic user via the file realm do not create the operator managed secret to avoid confusion if userProvidedFileRealm.PasswordHashForUser(ElasticUserName) != nil { @@ -89,6 +93,7 @@ func reconcileInternalUsers( {Name: PreStopUserName, Roles: []string{ClusterManageRole}}, {Name: ProbeUserName, Roles: []string{ProbeUserRole}}, {Name: MonitoringUserName, Roles: []string{RemoteMonitoringCollectorBuiltinRole}}, + {Name: DiagnosticsUserName, Roles: []string{DiagnosticsUserRole}}, }, esv1.InternalUsersSecret(es.Name), true, diff --git a/pkg/controller/elasticsearch/user/predefined_test.go b/pkg/controller/elasticsearch/user/predefined_test.go index 8d99d79bea..8206797f45 100644 --- a/pkg/controller/elasticsearch/user/predefined_test.go +++ b/pkg/controller/elasticsearch/user/predefined_test.go @@ -265,7 +265,7 @@ func Test_reconcileInternalUsers(t *testing.T) { got, err := reconcileInternalUsers(context.Background(), c, es, tt.existingFileRealm, testPasswordHasher) require.NoError(t, err) // check returned users - require.Len(t, got, 4) + require.Len(t, got, 5) controllerUser := got[0] probeUser := got[2] // names and roles are always the same diff --git a/pkg/controller/elasticsearch/user/reconcile_test.go b/pkg/controller/elasticsearch/user/reconcile_test.go index 50183d4a67..00fc46f3d9 100644 --- a/pkg/controller/elasticsearch/user/reconcile_test.go +++ b/pkg/controller/elasticsearch/user/reconcile_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" @@ -84,18 +85,50 @@ func Test_ReconcileRolesFileRealmSecret(t *testing.T) { } func Test_aggregateFileRealm(t *testing.T) { - c := k8s.NewFakeClient(sampleUserProvidedFileRealmSecrets...) - fileRealm, controllerUser, err := aggregateFileRealm(context.Background(), c, sampleEsWithAuth, initDynamicWatches(), record.NewFakeRecorder(10), testPasswordHasher) - require.NoError(t, err) - require.NotEmpty(t, controllerUser.Password) - actualUsers := fileRealm.UserNames() - require.ElementsMatch(t, []string{"elastic", "elastic-internal", "elastic-internal-pre-stop", "elastic-internal-probe", "elastic-internal-monitoring", "user1", "user2", "user3"}, actualUsers) + sampleEsWithAuthAndElasticUserDisabled := sampleEsWithAuth.DeepCopy() + sampleEsWithAuthAndElasticUserDisabled.Spec.Auth.DisableElasticUser = true + tests := []struct { + name string + es esv1.Elasticsearch + expected []string + assertions func(t *testing.T, c k8s.Client, es esv1.Elasticsearch) + }{ + { + name: "file realm users with elastic user enabled", + es: sampleEsWithAuth, + expected: []string{"elastic", "elastic-internal", "elastic-internal-pre-stop", "elastic-internal-probe", "elastic-internal-diagnostics", "elastic-internal-monitoring", "user1", "user2", "user3"}, + }, + { + name: "file realm users with elastic user disabled", + es: *sampleEsWithAuthAndElasticUserDisabled, + expected: []string{"elastic-internal", "elastic-internal-pre-stop", "elastic-internal-probe", "elastic-internal-diagnostics", "elastic-internal-monitoring", "user1", "user2", "user3"}, + assertions: func(t *testing.T, c k8s.Client, es esv1.Elasticsearch) { + t.Helper() + var secret corev1.Secret + err := c.Get(context.Background(), types.NamespacedName{Namespace: es.Namespace, Name: esv1.ElasticUserSecret(es.Name)}, &secret) + require.True(t, apierrors.IsNotFound(err)) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := k8s.NewFakeClient(sampleUserProvidedFileRealmSecrets...) + fileRealm, controllerUser, err := aggregateFileRealm(context.Background(), c, tt.es, initDynamicWatches(), record.NewFakeRecorder(10), testPasswordHasher) + require.NoError(t, err) + require.NotEmpty(t, controllerUser.Password) + actualUsers := fileRealm.UserNames() + require.ElementsMatch(t, tt.expected, actualUsers) + if tt.assertions != nil { + tt.assertions(t, c, tt.es) + } + }) + } } func Test_aggregateRoles(t *testing.T) { c := k8s.NewFakeClient(sampleUserProvidedRolesSecret...) roles, err := aggregateRoles(context.Background(), c, sampleEsWithAuth, initDynamicWatches(), record.NewFakeRecorder(10)) require.NoError(t, err) - require.Len(t, roles, 55) + require.Len(t, roles, 56) require.Contains(t, roles, ProbeUserRole, ClusterManageRole, "role1", "role2") } diff --git a/pkg/controller/elasticsearch/user/roles.go b/pkg/controller/elasticsearch/user/roles.go index e84584d7b8..397108da4f 100644 --- a/pkg/controller/elasticsearch/user/roles.go +++ b/pkg/controller/elasticsearch/user/roles.go @@ -8,6 +8,7 @@ import ( "fmt" "gopkg.in/yaml.v3" + "k8s.io/utils/ptr" beatv1beta1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/beat/v1beta1" esclient "github.com/elastic/cloud-on-k8s/v2/pkg/controller/elasticsearch/client" @@ -25,6 +26,8 @@ const ( ProbeUserRole = "elastic_internal_probe_user" // RemoteMonitoringCollectorBuiltinRole is the name of the built-in remote_monitoring_collector role. RemoteMonitoringCollectorBuiltinRole = "remote_monitoring_collector" + // DiagnosticsUserRole is the name of the built-in role for ECK diagnostics use. + DiagnosticsUserRole = "elastic_internal_diagnostics" // ApmUserRoleV6 is the name of the role used by 6.8.x APMServer instances to connect to Elasticsearch. ApmUserRoleV6 = "eck_apm_user_role_v6" @@ -66,6 +69,46 @@ var ( PredefinedRoles = RolesFileContent{ ProbeUserRole: esclient.Role{Cluster: []string{"monitor"}}, ClusterManageRole: esclient.Role{Cluster: []string{"manage"}}, + DiagnosticsUserRole: esclient.Role{ + Cluster: []string{"monitor", "monitor_snapshot", "manage", "read_ilm", "read_security"}, + Indices: []esclient.IndexRole{ + { + Names: []string{"*"}, + Privileges: []string{"monitor", "read", "view_index_metadata"}, + AllowRestrictedIndices: ptr.To[bool](true), + }, + }, + Applications: []esclient.ApplicationRole{ + { + Application: "kibana-.kibana", + Resources: []string{"*"}, + Privileges: []string{ + "feature_ml.read", + "feature_siem.read", + "feature_siem.read_alerts", + "feature_siem.policy_management_read", + "feature_siem.endpoint_list_read", + "feature_siem.trusted_applications_read", + "feature_siem.event_filters_read", + "feature_siem.host_isolation_exceptions_read", + "feature_siem.blocklist_read", + "feature_siem.actions_log_management_read", + "feature_securitySolutionCases.read", + "feature_securitySolutionAssistant.read", + "feature_actions.read", + "feature_builtInAlerts.read", + "feature_fleet.all", + "feature_fleetv2.all", + "feature_osquery.read", + "feature_indexPatterns.read", + "feature_discover.read", + "feature_dashboard.read", + "feature_maps.read", + "feature_visualize.read", + }, + }, + }, + }, ApmUserRoleV6: esclient.Role{ Cluster: []string{"monitor", "manage_index_templates"}, Indices: []esclient.IndexRole{ diff --git a/test/e2e/test/elasticsearch/checks_k8s.go b/test/e2e/test/elasticsearch/checks_k8s.go index 0836b56807..f91925a51a 100644 --- a/test/e2e/test/elasticsearch/checks_k8s.go +++ b/test/e2e/test/elasticsearch/checks_k8s.go @@ -128,7 +128,7 @@ func CheckSecrets(b Builder, k *test.K8sClient) test.Step { }, { Name: esName + "-es-internal-users", - Keys: []string{"elastic-internal", "elastic-internal-monitoring", "elastic-internal-pre-stop", "elastic-internal-probe"}, + Keys: []string{"elastic-internal", "elastic-internal-monitoring", "elastic-internal-diagnostics", "elastic-internal-pre-stop", "elastic-internal-probe"}, Labels: map[string]string{ "common.k8s.elastic.co/type": "elasticsearch", "eck.k8s.elastic.co/credentials": "true",