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..2fe5cddbfe 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, + TenantField: conf.tenantField, DefaultTenantID: conf.defaultTenantID, ReplicaHeader: conf.replicaHeader, ReplicationFactor: conf.replicationFactor, @@ -708,6 +709,7 @@ type receiveConfig struct { refreshInterval *model.Duration endpoint string tenantHeader string + tenantField 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-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) cmd.Flag("receive.tenant-label-name", "Label name through which the tenant will be announced.").Default(receive.DefaultTenantLabel).StringVar(&rc.tenantLabelName) 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. diff --git a/pkg/receive/handler.go b/pkg/receive/handler.go index c35c3bea41..c2888d3f39 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,6 +79,7 @@ type Options struct { ListenAddress string Registry prometheus.Registerer TenantHeader string + TenantField string DefaultTenantID string ReplicaHeader string Endpoint string @@ -291,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() @@ -299,6 +308,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. @@ -309,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 @@ -810,3 +828,42 @@ 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 { + return "", errors.New("could not get organization field from client cert") + } + tenant = cert.Subject.Organization[0] + + case CertificateFieldOrganizationalUnit: + 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 == "" { + 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") + } + + return tenant, nil +}