Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Receive: option to extract tenant from client certificate #5153

Merged
merged 7 commits into from Jun 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions cmd/thanos/receive.go
Expand Up @@ -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,
Expand Down Expand Up @@ -708,6 +709,7 @@ type receiveConfig struct {
refreshInterval *model.Duration
endpoint string
tenantHeader string
tenantField string
tenantLabelName string
defaultTenantID string
replicaHeader string
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions docs/components/receive.md
Expand Up @@ -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.
Expand Down
59 changes: 58 additions & 1 deletion pkg/receive/handler.go
Expand Up @@ -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")
Expand All @@ -72,6 +79,7 @@ type Options struct {
ListenAddress string
Registry prometheus.Registerer
TenantHeader string
TenantField string
DefaultTenantID string
ReplicaHeader string
Endpoint string
Expand Down Expand Up @@ -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()

Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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
}