From a7e987bf517f6404399791733eef69dc8d27641e Mon Sep 17 00:00:00 2001 From: Magnus Kaiser Date: Mon, 14 Feb 2022 09:48:09 +0100 Subject: [PATCH 1/7] added option to extract tenant from client certificate Signed-off-by: Magnus Kaiser --- CHANGELOG.md | 1 + cmd/thanos/receive.go | 4 ++++ pkg/receive/handler.go | 51 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6d611e33b..01a02e3f3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ The binaries published with this release are built with Go1.17.8 to avoid [CVE-2 ### Added +- [#5153](https://github.com/thanos-io/thanos/pull/5153) Receive: option to extract tenant from client certificate - [#5110](https://github.com/thanos-io/thanos/pull/5110) Block: Do not upload DebugMeta files to obj store. - [#4963](https://github.com/thanos-io/thanos/pull/4963) Compactor, Store, Tools: Loading block metadata now only filters out duplicates within a source (or compaction group if replica labels are configured), and does so in parallel over sources. - [#5089](https://github.com/thanos-io/thanos/pull/5089) S3: Create an empty map in the case SSE-KMS is used and no KMSEncryptionContext is passed. diff --git a/cmd/thanos/receive.go b/cmd/thanos/receive.go index 553e8303b2..301315693e 100644 --- a/cmd/thanos/receive.go +++ b/cmd/thanos/receive.go @@ -191,6 +191,7 @@ func runReceive( Registry: reg, Endpoint: conf.endpoint, TenantHeader: conf.tenantHeader, + TenantAttribute: conf.tenantAttribute, DefaultTenantID: conf.defaultTenantID, ReplicaHeader: conf.replicaHeader, ReplicationFactor: conf.replicationFactor, @@ -708,6 +709,7 @@ type receiveConfig struct { refreshInterval *model.Duration endpoint string tenantHeader string + tenantAttribute string tenantLabelName string defaultTenantID string replicaHeader string @@ -771,6 +773,8 @@ func (rc *receiveConfig) registerFlag(cmd extkingpin.FlagClause) { cmd.Flag("receive.tenant-header", "HTTP header to determine tenant for write requests.").Default(receive.DefaultTenantHeader).StringVar(&rc.tenantHeader) + cmd.Flag("receive.tenant-certificate-attribute", "Use TLS client certificate's attribute to determine tenant for write requests. Must be one of organization, organizationalUnit or commonName.").Default("").StringVar(&rc.tenantAttribute) + cmd.Flag("receive.default-tenant-id", "Default tenant ID to use when none is provided via a header.").Default(receive.DefaultTenant).StringVar(&rc.defaultTenantID) cmd.Flag("receive.tenant-label-name", "Label name through which the tenant will be announced.").Default(receive.DefaultTenantLabel).StringVar(&rc.tenantLabelName) diff --git a/pkg/receive/handler.go b/pkg/receive/handler.go index c35c3bea41..d245aadde9 100644 --- a/pkg/receive/handler.go +++ b/pkg/receive/handler.go @@ -72,6 +72,7 @@ type Options struct { ListenAddress string Registry prometheus.Registerer TenantHeader string + TenantAttribute string DefaultTenantID string ReplicaHeader string Endpoint string @@ -340,6 +341,56 @@ func (h *Handler) receiveHTTP(w http.ResponseWriter, r *http.Request) { } } + tenant := r.Header.Get(h.options.TenantHeader) + if tenant == "" { + tenant = h.options.DefaultTenantID + } + + // Checking certs for possible value. + if h.options.TenantAttribute != "" { + if len(r.TLS.PeerCertificates) > 0 { + // First cert is the leaf authenticated against. + cert := r.TLS.PeerCertificates[0] + + switch h.options.TenantAttribute { + + case "organization": + if len(cert.Subject.Organization) > 0 { + tenant = cert.Subject.Organization[0] + } else { + http.Error(w, "could not get organization attribute from client cert", http.StatusBadRequest) + return + } + + case "organizationalUnit": + if len(cert.Subject.OrganizationalUnit) > 0 { + tenant = cert.Subject.OrganizationalUnit[0] + } else { + http.Error(w, "could not get organizationalUnit attribute from client cert", http.StatusBadRequest) + return + } + + case "commonName": + if cert.Subject.CommonName != "" { + tenant = cert.Subject.CommonName + } else { + http.Error(w, "could not get commonName attribute from client cert", http.StatusBadRequest) + return + } + + default: + // Unknown/unsupported attribute requested, can't continue. + level.Error(h.logger).Log("err", err, "msg", "tls client cert attribute requested is not supported") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + } else { + http.Error(w, "could not get required certificate attribute from client cert", http.StatusBadRequest) + return + } + } + // Exit early if the request contained no data. We don't support metadata yet. We also cannot fail here, because // this would mean lack of forward compatibility for remote write proto. if len(wreq.Timeseries) == 0 { From c3ee675ededbdbbd4224d3bbd1d0c2581ed6f267 Mon Sep 17 00:00:00 2001 From: Magnus Kaiser Date: Mon, 28 Feb 2022 16:32:17 +0100 Subject: [PATCH 2/7] added suggestions from PR Signed-off-by: Magnus Kaiser --- cmd/thanos/receive.go | 6 +-- pkg/receive/handler.go | 97 ++++++++++++++++++++++++------------------ 2 files changed, 58 insertions(+), 45 deletions(-) diff --git a/cmd/thanos/receive.go b/cmd/thanos/receive.go index 301315693e..2fe5cddbfe 100644 --- a/cmd/thanos/receive.go +++ b/cmd/thanos/receive.go @@ -191,7 +191,7 @@ func runReceive( Registry: reg, Endpoint: conf.endpoint, TenantHeader: conf.tenantHeader, - TenantAttribute: conf.tenantAttribute, + TenantField: conf.tenantField, DefaultTenantID: conf.defaultTenantID, ReplicaHeader: conf.replicaHeader, ReplicationFactor: conf.replicationFactor, @@ -709,7 +709,7 @@ type receiveConfig struct { refreshInterval *model.Duration endpoint string tenantHeader string - tenantAttribute string + tenantField string tenantLabelName string defaultTenantID string replicaHeader string @@ -773,7 +773,7 @@ func (rc *receiveConfig) registerFlag(cmd extkingpin.FlagClause) { cmd.Flag("receive.tenant-header", "HTTP header to determine tenant for write requests.").Default(receive.DefaultTenantHeader).StringVar(&rc.tenantHeader) - cmd.Flag("receive.tenant-certificate-attribute", "Use TLS client certificate's attribute to determine tenant for write requests. Must be one of organization, organizationalUnit or commonName.").Default("").StringVar(&rc.tenantAttribute) + cmd.Flag("receive.tenant-certificate-field", "Use TLS client's certificate field to determine tenant for write requests. Must be one of "+receive.CertificateFieldOrganization+", "+receive.CertificateFieldOrganizationalUnit+" or "+receive.CertificateFieldCommonName+". This setting will cause the receive.tenant-header flag value to be ignored.").Default("").EnumVar(&rc.tenantField, "", receive.CertificateFieldOrganization, receive.CertificateFieldOrganizationalUnit, receive.CertificateFieldCommonName) cmd.Flag("receive.default-tenant-id", "Default tenant ID to use when none is provided via a header.").Default(receive.DefaultTenant).StringVar(&rc.defaultTenantID) diff --git a/pkg/receive/handler.go b/pkg/receive/handler.go index d245aadde9..f38598c039 100644 --- a/pkg/receive/handler.go +++ b/pkg/receive/handler.go @@ -57,6 +57,13 @@ const ( labelError = "error" ) +// Allowed fields in client certificates. +const ( + CertificateFieldOrganization = "organization" + CertificateFieldOrganizationalUnit = "organizationalUnit" + CertificateFieldCommonName = "commonName" +) + var ( // errConflict is returned whenever an operation fails due to any conflict-type error. errConflict = errors.New("conflict") @@ -72,7 +79,7 @@ type Options struct { ListenAddress string Registry prometheus.Registerer TenantHeader string - TenantAttribute string + TenantField string DefaultTenantID string ReplicaHeader string Endpoint string @@ -346,47 +353,11 @@ func (h *Handler) receiveHTTP(w http.ResponseWriter, r *http.Request) { tenant = h.options.DefaultTenantID } - // Checking certs for possible value. - if h.options.TenantAttribute != "" { - if len(r.TLS.PeerCertificates) > 0 { - // First cert is the leaf authenticated against. - cert := r.TLS.PeerCertificates[0] - - switch h.options.TenantAttribute { - - case "organization": - if len(cert.Subject.Organization) > 0 { - tenant = cert.Subject.Organization[0] - } else { - http.Error(w, "could not get organization attribute from client cert", http.StatusBadRequest) - return - } - - case "organizationalUnit": - if len(cert.Subject.OrganizationalUnit) > 0 { - tenant = cert.Subject.OrganizationalUnit[0] - } else { - http.Error(w, "could not get organizationalUnit attribute from client cert", http.StatusBadRequest) - return - } - - case "commonName": - if cert.Subject.CommonName != "" { - tenant = cert.Subject.CommonName - } else { - http.Error(w, "could not get commonName attribute from client cert", http.StatusBadRequest) - return - } - - default: - // Unknown/unsupported attribute requested, can't continue. - level.Error(h.logger).Log("err", err, "msg", "tls client cert attribute requested is not supported") - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - } else { - http.Error(w, "could not get required certificate attribute from client cert", http.StatusBadRequest) + if h.options.TenantField != "" { + tenant, err = h.getTenantFromCertificate(r) + if err != nil { + // This must hard fail to ensure hard tenancy when feature is enabled. + http.Error(w, err.Error(), http.StatusBadRequest) return } } @@ -861,3 +832,45 @@ func (p *peerGroup) get(ctx context.Context, addr string) (storepb.WriteableStor p.cache[addr] = client return client, nil } + +// GetTenantFromCertificate extracts the tenant value from a client's presented certificate. The x509 field to use as +// value can be configured with Options.TenantField. An error is returned when the extraction has not succeeded. +func (h *Handler) getTenantFromCertificate(r *http.Request) (string, error) { + var tenant string + + if len(r.TLS.PeerCertificates) == 0 { + return "", errors.New("could not get required certificate field from client cert") + } + + // First cert is the leaf authenticated against. + cert := r.TLS.PeerCertificates[0] + + switch h.options.TenantField { + + case CertificateFieldOrganization: + if len(cert.Subject.Organization) > 0 { + tenant = cert.Subject.Organization[0] + } else { + return "", errors.New("could not get organization field from client cert") + } + + case CertificateFieldOrganizationalUnit: + if len(cert.Subject.OrganizationalUnit) > 0 { + tenant = cert.Subject.OrganizationalUnit[0] + } else { + return "", errors.New("could not get organizationalUnit field from client cert") + } + + case CertificateFieldCommonName: + if cert.Subject.CommonName != "" { + tenant = cert.Subject.CommonName + } else { + return "", errors.New("could not get commonName field from client cert") + } + + default: + return "", errors.New("tls client cert field requested is not supported") + } + + return tenant, nil +} From 8c0a05f58800091e54a128686873644f5164839d Mon Sep 17 00:00:00 2001 From: Magnus Kaiser Date: Wed, 23 Mar 2022 08:42:22 +0100 Subject: [PATCH 3/7] removed else cases Signed-off-by: Magnus Kaiser --- pkg/receive/handler.go | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/pkg/receive/handler.go b/pkg/receive/handler.go index f38598c039..f88c4196ac 100644 --- a/pkg/receive/handler.go +++ b/pkg/receive/handler.go @@ -848,25 +848,22 @@ func (h *Handler) getTenantFromCertificate(r *http.Request) (string, error) { switch h.options.TenantField { case CertificateFieldOrganization: - if len(cert.Subject.Organization) > 0 { - tenant = cert.Subject.Organization[0] - } else { + if len(cert.Subject.Organization) == 0 { return "", errors.New("could not get organization field from client cert") } + tenant = cert.Subject.Organization[0] case CertificateFieldOrganizationalUnit: - if len(cert.Subject.OrganizationalUnit) > 0 { - tenant = cert.Subject.OrganizationalUnit[0] - } else { + if len(cert.Subject.OrganizationalUnit) == 0 { return "", errors.New("could not get organizationalUnit field from client cert") } + tenant = cert.Subject.OrganizationalUnit[0] case CertificateFieldCommonName: - if cert.Subject.CommonName != "" { - tenant = cert.Subject.CommonName - } else { + if cert.Subject.CommonName == "" { return "", errors.New("could not get commonName field from client cert") } + tenant = cert.Subject.CommonName default: return "", errors.New("tls client cert field requested is not supported") From f053402662d44793ed750e020fda188b9a167ce9 Mon Sep 17 00:00:00 2001 From: Magnus Kaiser Date: Tue, 12 Apr 2022 09:49:34 +0200 Subject: [PATCH 4/7] corrected location of certificate field check Signed-off-by: Magnus Kaiser --- pkg/receive/handler.go | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/pkg/receive/handler.go b/pkg/receive/handler.go index f88c4196ac..0198360ea4 100644 --- a/pkg/receive/handler.go +++ b/pkg/receive/handler.go @@ -307,6 +307,15 @@ func (h *Handler) receiveHTTP(w http.ResponseWriter, r *http.Request) { tenant = h.options.DefaultTenantID } + if h.options.TenantField != "" { + tenant, err = h.getTenantFromCertificate(r) + if err != nil { + // This must hard fail to ensure hard tenancy when feature is enabled. + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + } + tLogger := log.With(h.logger, "tenant", tenant) // ioutil.ReadAll dynamically adjust the byte slice for read data, starting from 512B. @@ -348,20 +357,6 @@ func (h *Handler) receiveHTTP(w http.ResponseWriter, r *http.Request) { } } - tenant := r.Header.Get(h.options.TenantHeader) - if tenant == "" { - tenant = h.options.DefaultTenantID - } - - if h.options.TenantField != "" { - tenant, err = h.getTenantFromCertificate(r) - if err != nil { - // This must hard fail to ensure hard tenancy when feature is enabled. - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - } - // Exit early if the request contained no data. We don't support metadata yet. We also cannot fail here, because // this would mean lack of forward compatibility for remote write proto. if len(wreq.Timeseries) == 0 { From 3aea9b3acd0195b084d52dacf011e75ccd534dde Mon Sep 17 00:00:00 2001 From: Magnus Kaiser Date: Tue, 12 Apr 2022 11:09:46 +0200 Subject: [PATCH 5/7] fixed issue with err definition Signed-off-by: Magnus Kaiser --- pkg/receive/handler.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/receive/handler.go b/pkg/receive/handler.go index 0198360ea4..c8a499ff1a 100644 --- a/pkg/receive/handler.go +++ b/pkg/receive/handler.go @@ -299,6 +299,7 @@ func (h *Handler) handleRequest(ctx context.Context, rep uint64, tenant string, } func (h *Handler) receiveHTTP(w http.ResponseWriter, r *http.Request) { + var err error span, ctx := tracing.StartSpan(r.Context(), "receive_http") defer span.Finish() @@ -326,7 +327,7 @@ func (h *Handler) receiveHTTP(w http.ResponseWriter, r *http.Request) { } else { compressed.Grow(512) } - _, err := io.Copy(&compressed, r.Body) + _, err = io.Copy(&compressed, r.Body) if err != nil { http.Error(w, errors.Wrap(err, "read compressed request body").Error(), http.StatusInternalServerError) return From 2f38495e9c5616db62581e4f681a4a144cdad28a Mon Sep 17 00:00:00 2001 From: Magnus Kaiser Date: Wed, 25 May 2022 11:23:14 +0200 Subject: [PATCH 6/7] updated docs Signed-off-by: Magnus Kaiser --- docs/components/receive.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/components/receive.md b/docs/components/receive.md index 26881703b7..709d6b4c72 100644 --- a/docs/components/receive.md +++ b/docs/components/receive.md @@ -144,6 +144,12 @@ Flags: --receive.replication-factor=1 How many times to replicate incoming write requests. + --receive.tenant-certificate-field= + Use TLS client's certificate field to determine + tenant for write requests. Must be one of + organization, organizationalUnit or commonName. + This setting will cause the + receive.tenant-header flag value to be ignored. --receive.tenant-header="THANOS-TENANT" HTTP header to determine tenant for write requests. From a8a25e35dd13498dcccf311e0ca71be45974e3f2 Mon Sep 17 00:00:00 2001 From: Magnus Kaiser Date: Wed, 25 May 2022 12:02:47 +0200 Subject: [PATCH 7/7] corrected comment Signed-off-by: Magnus Kaiser --- pkg/receive/handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/receive/handler.go b/pkg/receive/handler.go index c8a499ff1a..c2888d3f39 100644 --- a/pkg/receive/handler.go +++ b/pkg/receive/handler.go @@ -829,7 +829,7 @@ func (p *peerGroup) get(ctx context.Context, addr string) (storepb.WriteableStor return client, nil } -// GetTenantFromCertificate extracts the tenant value from a client's presented certificate. The x509 field to use as +// getTenantFromCertificate extracts the tenant value from a client's presented certificate. The x509 field to use as // value can be configured with Options.TenantField. An error is returned when the extraction has not succeeded. func (h *Handler) getTenantFromCertificate(r *http.Request) (string, error) { var tenant string