Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions keps/sig-auth/20190116-service-account-external-signing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
---
title: Support external signing of service account keys
authors:
- "@micahhausler"
owning-sig: sig-auth
participating-sigs: []
reviewers:
- "@mikedanese"
- "@liggit"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a t in @liggitt

- "@tallclair"
approvers:
- "@mikedanese"
- "@liggit"
- "@tallclair"
editor: '@micahhausler'
creation-date: 2019-01-16
last-updated: 2019-05-17
status: implementable
see-also: []
replaces: []
superseded-by: []
---

# Support external signing of service account keys

## Table of Contents

<!-- toc -->
- [Summary](#summary)
- [Motivation](#motivation)
- [Goals](#goals)
- [Non-Goals](#non-goals)
- [Proposal](#proposal)
- [Preserve existing behavior](#preserve-existing-behavior)
- [Updates to API server token generation](#updates-to-api-server-token-generation)
- [New API](#new-api)
- [Implementation Details/Notes/Constraints](#implementation-detailsnotesconstraints)
- [Risks and Mitigations](#risks-and-mitigations)
- [Graduation Criteria](#graduation-criteria)
- [Implementation History](#implementation-history)
<!-- /toc -->

## Summary

The Kubernetes API server has always read service account keys from disk as the process starts, and kept them in memory for the duration of the server's lifetime. As the API server can now verify and issue projected volume tokens, it would be advantageous to support external signing and verifying of token data over an API, as well as reading public keys from an API.

## Motivation

For operators who want to regularly rotate the signing and verifying keys for projected volume tokens, the Kubernetes API server must be restarted in order to use a new key. To facilitate easy key rotation, this KEP includes an proposal for a grpc API to support out of process signing and listing of signing keys.

### Goals

- Support for out-of-process JWT signing
- Support for listing public verifying keys
- Preserve existing behavior and performance for keys not read over a socket

### Non-Goals

- Reading TLS serving certificates and key from a socket or reloading of the API server with new cert and key
- Reading any other certificates from a file

## Proposal

### Preserve existing behavior

The API server flags `--service-account-key-file` and `--service-account-signing-key-file` will continue be used for reading from files.

### Updates to API server token generation

As of Kubernetes v1.13.2, the API server uses the functions `JWTTokenGenerator` and `JWTTokenAuthenticator`. New types that implement the `TokenGenerator` interface and support token validation will be added to `k8s.io/kubernetes/pkg/serviceaccount/`.

### New API

I'm proposing creating a new versioned grpc API under `k8s.io/kubernetes/pkg/serviceaccount`. This will be similar to how the KMS envelope encryption has an API at `k8s.io/kubernetes/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1/service.proto`

```proto
syntax = "proto3";

package v1alpha1;

service KeyService {
// Sign an incoming payload
rpc SignPayload(SignPayloadRequest) returns (SignPayloadResponse) {}
Copy link
Member

@mikedanese mikedanese May 17, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What validation does the API server do on the response? Can the signer add additional claims to the token? Does it have to be a JWT? I'd prefer some guarantees here for the sake of portability.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically the signer could, as the API server is just sending bytes over, but this is just modeled after OpaqueSigner.SignPayload()

// List all active public keys
rpc ListPublicKeys(ListPublicKeysRequest) returns (ListPublicKeysResponse) {}
}

message SignPayloadRequest {
// payload is the content to be signed. JWT headers must be included by the caller
bytes payload = 1;
// algorithm specifies which algorithm to sign with
string algorithm = 2;
Copy link
Member

@mikedanese mikedanese May 17, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is useful. The apiserver reads this from the ListPublicKeys response than passes it back to the signer? What happens if it gets it wrong e.g. if a active key is rotated and the new key has a different algorithm?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that we are somewhat constrained by the OpaqueSigner interface.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the algorithm comment, it should be a list. A list would allow to specify algorithms and you are able to migrate, in case one algorithm is broken.

Copy link
Member

@enj enj Jun 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are limited in what we can do here because signer.Public().Algorithm is the alg value for SignPayload and JSONWebKey.Algorithm. I am trying to think if there is some clever way we can allow moving to a new algorithm for new signed payloads while verifying previously signed data with the old algorithm.

We need something like an OpaqueVerifier...

// OpaqueSigner is an interface that supports signing payloads with opaque
// private key(s). Private key operations preformed by implementors may, for
// example, occur in a hardware module. An OpaqueSigner may rotate signing keys
// transparently to the user of this interface.
type OpaqueSigner interface {
	// Public returns the public key of the current signing key.
	Public() *JSONWebKey
	// Algs returns a list of supported signing algorithms.
	Algs() []SignatureAlgorithm
	// SignPayload signs a payload with the current signing key using the given
	// algorithm.
	SignPayload(payload []byte, alg SignatureAlgorithm) ([]byte, error)
}

// SignatureAlgorithm represents a signature (or MAC) algorithm.
type SignatureAlgorithm string

// JSONWebKey represents a public or private key in JWK format.
type JSONWebKey struct {
	Key          interface{}
	Certificates []*x509.Certificate
	KeyID        string
	Algorithm    string
	Use          string
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hah I spoke too soon:

// OpaqueVerifier is an interface that supports verifying payloads with opaque
// public key(s). An OpaqueSigner may rotate signing keys transparently to the
// user of this interface.
type OpaqueVerifier interface {
	VerifyPayload(payload []byte, signature []byte, alg SignatureAlgorithm) error
}

I believe we can have signer.Public().Key return an OpaqueVerifier that allows us to have algorithm agility.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me

}
message SignPayloadResponse {
// content returns the signed payload
bytes content = 1;
}


message PublicKey {
// public_key is a PEM encoded public key
bytes public_key = 1;
// certificate is a concatenated list of PEM encoded x509 certificates
bytes certificates = 2;
// key_id is the key's ID
string key_id = 3;
// algorithm states the algorithm the key uses
string algorithm = 4;
}

message ListPublicKeysRequest {}
message ListPublicKeysResponse {
// key_id is the key's ID
string active_key_id = 1;
// public_keys is a list of public verifying keys
repeated PublicKey public_keys = 2;
}
```

### Implementation Details/Notes/Constraints

The API server flag `--service-account-key-file` can be specified multiple times for legacy SA tokens and projected tokens. Validation keys from this flag will be merged with the response of `ListPublicKeys()`. A new flag `--key-service-url` will be added to the API server specifying a unix socket where the key service will be accessible.

### Risks and Mitigations

New token generation and validation could suffer a performance difference when reading over a socket, as an external process will be signing data.

Signing and verifying tokens over a grpc API carries the risk of a server side request forgery, where a malicious client could generate tokens. To mitigate this risk, the API will only be accessible over a unix socket.

## Graduation Criteria

<!-- TODO -->

## Implementation History

* Initial PR: kubernetes/kubernetes#73110