Skip to content

Commit

Permalink
Receive: option to extract tenant from client certificate (#5153)
Browse files Browse the repository at this point in the history
* added option to extract tenant from client certificate

Signed-off-by: Magnus Kaiser <magnus.kaiser@gec.io>

* added suggestions from PR

Signed-off-by: Magnus Kaiser <magnus.kaiser@gec.io>

* removed else cases

Signed-off-by: Magnus Kaiser <magnus.kaiser@gec.io>

* corrected location of certificate field check

Signed-off-by: Magnus Kaiser <magnus.kaiser@gec.io>

* fixed issue with err definition

Signed-off-by: Magnus Kaiser <magnus.kaiser@gec.io>

* updated docs

Signed-off-by: Magnus Kaiser <magnus.kaiser@gec.io>

* corrected comment

Signed-off-by: Magnus Kaiser <magnus.kaiser@gec.io>

Co-authored-by: Magnus Kaiser <magnus.kaiser@gec.io>
  • Loading branch information
4xoc and Magnus Kaiser committed Jun 21, 2022
1 parent 31ce79b commit 9e54868
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -80,6 +80,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.
Expand Down
4 changes: 4 additions & 0 deletions cmd/thanos/receive.go
Expand Up @@ -203,6 +203,7 @@ func runReceive(
Registry: reg,
Endpoint: conf.endpoint,
TenantHeader: conf.tenantHeader,
TenantField: conf.tenantField,
DefaultTenantID: conf.defaultTenantID,
ReplicaHeader: conf.replicaHeader,
ReplicationFactor: conf.replicationFactor,
Expand Down Expand Up @@ -725,6 +726,7 @@ type receiveConfig struct {
refreshInterval *model.Duration
endpoint string
tenantHeader string
tenantField string
tenantLabelName string
defaultTenantID string
replicaHeader string
Expand Down Expand Up @@ -794,6 +796,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)
Expand Down
6 changes: 6 additions & 0 deletions docs/components/receive.md
Expand Up @@ -163,6 +163,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.
Expand Down
59 changes: 58 additions & 1 deletion pkg/receive/handler.go
Expand Up @@ -62,6 +62,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")
Expand All @@ -77,6 +84,7 @@ type Options struct {
ListenAddress string
Registry *prometheus.Registry
TenantHeader string
TenantField string
DefaultTenantID string
ReplicaHeader string
Endpoint string
Expand Down Expand Up @@ -322,6 +330,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()

Expand All @@ -330,6 +339,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.
Expand All @@ -340,7 +358,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
Expand Down Expand Up @@ -865,3 +883,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
}

0 comments on commit 9e54868

Please sign in to comment.