From 486b67caca24e38eaf5fff16bdfbee2c46078e61 Mon Sep 17 00:00:00 2001 From: Zach Wasserman Date: Thu, 15 Sep 2022 09:02:17 -0700 Subject: [PATCH] Add initial support for kubequery (#6863) Configuration and fixes for the Fleet server and frontend to add support for https://github.com/Uptycs/kubequery. Co-authored-by: Michal Nicpon --- changes/kubequery | 1 + frontend/utilities/helpers.ts | 2 +- server/service/osquery_test.go | 1 + server/service/osquery_utils/queries.go | 24 ++- server/service/osquery_utils/queries_test.go | 9 +- tools/kubequery/README.md | 14 ++ tools/kubequery/kubequery-fleet.yml | 181 +++++++++++++++++++ 7 files changed, 226 insertions(+), 6 deletions(-) create mode 100644 changes/kubequery create mode 100644 tools/kubequery/README.md create mode 100644 tools/kubequery/kubequery-fleet.yml diff --git a/changes/kubequery b/changes/kubequery new file mode 100644 index 0000000000..31ac06b34d --- /dev/null +++ b/changes/kubequery @@ -0,0 +1 @@ +* Add support for [kubequery](https://github.com/Uptycs/kubequery). diff --git a/frontend/utilities/helpers.ts b/frontend/utilities/helpers.ts index d3df030d4b..fc974d2b01 100644 --- a/frontend/utilities/helpers.ts +++ b/frontend/utilities/helpers.ts @@ -584,7 +584,7 @@ export const inMilliseconds = (nanoseconds: number): number => { export const humanHostLastRestart = ( detailUpdatedAt: string, - uptime: number + uptime: number | string ): string => { if ( !detailUpdatedAt || diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go index 893fbbfd40..3fd01d17cd 100644 --- a/server/service/osquery_test.go +++ b/server/service/osquery_test.go @@ -495,6 +495,7 @@ func verifyDiscovery(t *testing.T, queries, discovery map[string]string) { hostDetailQueryPrefix + "mdm": {}, hostDetailQueryPrefix + "munki_info": {}, hostDetailQueryPrefix + "windows_update_history": {}, + hostDetailQueryPrefix + "kubequery_info": {}, } for name := range queries { require.NotEmpty(t, discovery[name]) diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go index b3d119f4d2..5680c96108 100644 --- a/server/service/osquery_utils/queries.go +++ b/server/service/osquery_utils/queries.go @@ -24,7 +24,7 @@ import ( type DetailQuery struct { // Query is the SQL query string. Query string - // Discovery is the SQL query that defines whether the query will run or the host or not. + // Discovery is the SQL query that defines whether the query will run on the host or not. // If not set, Fleet makes sure the query will always run. Discovery string // Platforms is a list of platforms to run the query on. If this value is @@ -347,6 +347,11 @@ FROM logical_drives WHERE file_system = 'NTFS' LIMIT 1;`, Platforms: []string{"windows"}, IngestFunc: ingestDiskSpace, }, + "kubequery_info": { + Query: `SELECT * from kubernetes_info`, + IngestFunc: ingestKubequeryInfo, + Discovery: discoveryTable("kubernetes_info"), + }, } // extraDetailQueries defines extra detail queries that should be run on the host, as @@ -1037,6 +1042,9 @@ func directIngestUsers(ctx context.Context, logger log.Logger, host *fleet.Host, } users = append(users, u) } + if len(users) == 0 { + return nil + } if err := ds.SaveHostUsers(ctx, host.ID, users); err != nil { return ctxerr.Wrap(ctx, err, "update host users") } @@ -1106,6 +1114,20 @@ func directIngestMunkiInfo(ctx context.Context, logger log.Logger, host *fleet.H return ds.SetOrUpdateMunkiInfo(ctx, host.ID, rows[0]["version"], errList, warnList) } +func ingestKubequeryInfo(ctx context.Context, logger log.Logger, host *fleet.Host, rows []map[string]string) error { + if len(rows) != 1 { + logger.Log("component", "service", "method", "ingestKubequeryInfo", "warn", + fmt.Sprintf("kubernetes_info expected single result got %d", len(rows))) + } + + host.Hostname = fmt.Sprintf("kubequery %s", rows[0]["cluster_name"]) + + // These values are not provided by kubequery + host.OsqueryVersion = "kubequery" + host.Platform = "kubequery" + return nil +} + func GetDetailQueries(fleetConfig config.FleetConfig, features *fleet.Features) map[string]DetailQuery { generatedMap := make(map[string]DetailQuery) for key, query := range hostDetailQueries { diff --git a/server/service/osquery_utils/queries_test.go b/server/service/osquery_utils/queries_test.go index d40b772d86..83dce6817e 100644 --- a/server/service/osquery_utils/queries_test.go +++ b/server/service/osquery_utils/queries_test.go @@ -297,7 +297,7 @@ func sortedKeysCompare(t *testing.T, m map[string]DetailQuery, expectedKeys []st func TestGetDetailQueries(t *testing.T) { queriesNoConfig := GetDetailQueries(config.FleetConfig{}, nil) - require.Len(t, queriesNoConfig, 17) + require.Len(t, queriesNoConfig, 18) baseQueries := []string{ "network_interface", @@ -317,18 +317,19 @@ func TestGetDetailQueries(t *testing.T) { "os_windows", "os_unix_like", "windows_update_history", + "kubequery_info", } sortedKeysCompare(t, queriesNoConfig, baseQueries) queriesWithoutWinOSVuln := GetDetailQueries(config.FleetConfig{Vulnerabilities: config.VulnerabilitiesConfig{DisableWinOSVulnerabilities: true}}, nil) - require.Len(t, queriesWithoutWinOSVuln, 16) + require.Len(t, queriesWithoutWinOSVuln, 17) queriesWithUsers := GetDetailQueries(config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}, &fleet.Features{EnableHostUsers: true}) - require.Len(t, queriesWithUsers, 19) + require.Len(t, queriesWithUsers, 20) sortedKeysCompare(t, queriesWithUsers, append(baseQueries, "users", "scheduled_query_stats")) queriesWithUsersAndSoftware := GetDetailQueries(config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}, &fleet.Features{EnableHostUsers: true, EnableSoftwareInventory: true}) - require.Len(t, queriesWithUsersAndSoftware, 22) + require.Len(t, queriesWithUsersAndSoftware, 23) sortedKeysCompare(t, queriesWithUsersAndSoftware, append(baseQueries, "users", "software_macos", "software_linux", "software_windows", "scheduled_query_stats")) } diff --git a/tools/kubequery/README.md b/tools/kubequery/README.md new file mode 100644 index 0000000000..ee1d98e19f --- /dev/null +++ b/tools/kubequery/README.md @@ -0,0 +1,14 @@ +# Kubequery and Fleet + +Use the provided configuration file ([kubequery-fleet.yml](kubequery-fleet.yml)) to get a [kubequery](https://github.com/Uptycs/kubequery) instance connected to Fleet. + +Before deploying, first retrieve the enroll secret from Fleet by opening a web browser to the Fleet URL, going to the Hosts page, and clicking on the "Manage enroll secret" button. +Alternatively, you can get the enroll secret using `fleetctl` using `fleetctl get enroll-secret`. +Update the `enroll.secret` in the `ConfigMap`. In production, you will also need to update the `tls_hostname` and `fleet.pem` to the appropriate values. Finally, deploy kubequery using `kubectl` + + +```sh +kubectl apply -f kubequery-fleet.yml +``` + +Kubernetes clusters will show up in Fleet with hostnames like `kubequery `. diff --git a/tools/kubequery/kubequery-fleet.yml b/tools/kubequery/kubequery-fleet.yml new file mode 100644 index 0000000000..c7268340dc --- /dev/null +++ b/tools/kubequery/kubequery-fleet.yml @@ -0,0 +1,181 @@ +# Copyright (c) 2020-present, The kubequery authors +# Copyright (c) 2022-present, Fleet +# +# This source code is licensed as defined by the LICENSE file found in the +# root directory of this source tree. +# +# SPDX-License-Identifier: (Apache-2.0 OR GPL-2.0-only) + +# Adapted from original provided by Kubequery project. + +--- +apiVersion: v1 +kind: Namespace +metadata: + name: kubequery + labels: + app.kubernetes.io/name: kubequery + app.kubernetes.io/part-of: kubequery + app.kubernetes.io/version: latest + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: kubequery-sa + namespace: kubequery + labels: + app.kubernetes.io/name: kubequery-sa + app.kubernetes.io/part-of: kubequery + app.kubernetes.io/version: latest + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kubequery-clusterrole + labels: + app.kubernetes.io/name: kubequery-clusterrole + app.kubernetes.io/part-of: kubequery + app.kubernetes.io/version: latest +rules: +- apiGroups: ["", "admissionregistration.k8s.io", "apps", "autoscaling", "batch", "events.k8s.io", "networking.k8s.io", "policy", "rbac.authorization.k8s.io", "storage.k8s.io"] + resources: ["*"] + verbs: ["get", "list", "watch"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: kubequery-clusterrolebinding + labels: + app.kubernetes.io/name: kubequery-clusterrolebinding + app.kubernetes.io/part-of: kubequery + app.kubernetes.io/version: latest +roleRef: + kind: ClusterRole + name: kubequery-clusterrole + apiGroup: rbac.authorization.k8s.io +subjects: +- kind: ServiceAccount + name: kubequery-sa + namespace: kubequery + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: kubequery-config + namespace: kubequery + labels: + app.kubernetes.io/name: kubequery-config + app.kubernetes.io/part-of: kubequery + app.kubernetes.io/version: latest +data: + enroll.secret: TODO + kubequery.flags: | + # Server + --tls_hostname=host.docker.internal:8080 + --tls_server_certs=/opt/uptycs/etc/fleet.pem + # Enrollment + --enroll_secret_path=/opt/uptycs/etc/enroll.secret + --enroll_tls_endpoint=/api/v1/osquery/enroll + # Configuration + --config_plugin=tls + --config_tls_endpoint=/api/v1/osquery/config + --config_refresh=10 + # Live query + --disable_distributed=false + --distributed_plugin=tls + --distributed_interval=10 + --distributed_tls_max_attempts=3 + --distributed_tls_read_endpoint=/api/v1/osquery/distributed/read + --distributed_tls_write_endpoint=/api/v1/osquery/distributed/write + # Logging + --logger_plugin=tls + --logger_tls_endpoint=/api/v1/osquery/log + --logger_tls_period=10 + # File carving + --disable_carver=false + --carver_start_endpoint=/api/v1/osquery/carve/begin + --carver_continue_endpoint=/api/v1/osquery/carve/block + --carver_block_size=2000000 + kubequery.conf: | + fleet.pem: | + -----BEGIN CERTIFICATE----- + MIIE5jCCAs6gAwIBAgIJAKq0+FAVArUhMA0GCSqGSIb3DQEBCwUAMBUxEzARBgNV + BAMMCkZsZWV0IFRlc3QwHhcNMjAxMDE1MTg1ODE5WhcNMzAwNzE1MTg1ODE5WjAV + MRMwEQYDVQQDDApGbGVldCBUZXN0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC + CgKCAgEAttQ62lpMq48/XjQFxYg47D2fgKgTBMjDNSfCt9VpqE3xPnybmWo8VZtk + jmrFM50IhagyYSjvl9iqdrnsl3ZV8KYbWEy6849zDYF1SudmC7/pJyH7QvpKgL7V + 4McM62jM905hyFy9KZTAlFiaeezWSWJre7kHsK2u5tsqS6ElatEZmF59sInixaRw + RqVxOhtDm7Sl1c5xKWM6phWoJfykspFpu5J6N2jRQXCfzBYoQWN76OohGouit/BQ + C1xvm+f7wgGZgbbfDjUoHAe9Yhd3XzZsYTgMDt/SRJRDnxFZwo8BAkY9yJm7f3dQ + AEhgJ66KbyoxITdgma1hgmeWibZY4hVymcRxB1B3RNN2at19RNy2J+brMxlG0KZk + nD77EqidrwLAlYcdeU3yLjt0vYPxT+RW7l1jiZlVi/oaykAmVfOhWnTnTwbsYs7O + UMyMyYHQECEs98ex7wrjThIBJScqhsSN1ipAxr5RgaDr+U5IR+tLhMewBy8So+nf + 2YuMhLfkCgoY80ELhz5F8avts5hksB0hqnNYr+Nlwm6eXqEPZSzFJmdc1IbmWzq2 + 7UH1OQmBFF2qr2j/8dcM+oPNgjrEEQjtyW0S4j2PhjSEbINgcwu0AaABssLI80Vm + Gp1TjUGA92rMwIjlAtcUUB5FOKSS8vAXb1VcDWMkybh9sHj4Z0ECAwEAAaM5MDcw + NQYDVR0RBC4wLIIJbG9jYWxob3N0ghRob3N0LmRvY2tlci5pbnRlcm5hbIIJMTI3 + LjAuMC4xMA0GCSqGSIb3DQEBCwUAA4ICAQBZOY++LNRTVG8XlQGVlOloEKA2WY3P + gXKJLSM7xWSxj2co0v+noyhoInHT7ysub8en59Et+vN53/0OobxNIdEKDUdqs38R + F++Oy6s/MhFHSo87F06t+W91/60ub4iFRHfev1qeNFV6Yzv9sFJ5LpXLFk+qVDb8 + pPyFFE1bXjctDPjD5gUj+Y34XikVKzMb7xddWCNs34v1KCaCBW7kkfefxiZiDR6g + lCEkDzp6xaLS898oCbfFakjr4bvOgBP1IqXLIDLPMhivaxNAooHTtu/3ezp7puix + TSDkjlkStDtEFw/wjyaMcEkk51Gs1ponBbADLRxQ50AHDWk/4vy8GcIVc6CdVEOA + Zw12FN06C4Jviiiv6uCXZ6iZ+V+pjGiGmSNYF+kruUs8BfrJIB89lqxpdQ4Kx01j + AuSFvjRRvIPmvApSdKEjLcY3AYRivXsB/hASMBbjh/p1f/JzSJdxoqSvONhNQJuh + +wcdNVQhGAv3kkLn/HMHTBl2Ur+9tQaJrnR1tWl1IzwLRJIi0Soyp/q5ZjQyFglj + 32xW83DZhtpQ2SI1QGy4AvWIPnGHZhMfav02KnKRhZdOMW4oekXRMrwiyXCqIazc + xXzAlCq8dHdP2Y9uvfFxVFyE+uSfkcPxX+DG/ZnpgCS27oKA/qLCybJamlqtveNs + RSjNe5qwGi0ifA== + -----END CERTIFICATE----- + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kubequery + namespace: kubequery + labels: + app.kubernetes.io/name: kubequery + app.kubernetes.io/part-of: kubequery + app.kubernetes.io/version: latest +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: kubequery + template: + metadata: + labels: + app.kubernetes.io/name: kubequery + app.kubernetes.io/part-of: kubequery + app.kubernetes.io/version: latest + spec: + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + terminationGracePeriodSeconds: 10 + serviceAccountName: kubequery-sa + containers: + - name: kubequery + image: uptycs/kubequery:latest + imagePullPolicy: Always + resources: + requests: + cpu: 200m + memory: 128Mi + limits: + cpu: 1000m + memory: 512Mi + volumeMounts: + - name: config + mountPath: /opt/uptycs/config + volumes: + - name: config + configMap: + name: kubequery-config