-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Added service account external signer KEP #704
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
Added service account external signer KEP #704
Conversation
justaugustus
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove any references to NEXT_KEP_NUMBER and rename the KEP to just be the draft date and KEP title.
KEP numbers will be obsolete once #703 merges.
|
|
||
| message SigningKeyReply { | ||
| // The PEM encoded bytes containing a private key | ||
| bytes private_key = 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you point to docs of an AWS API that would back this? The GCP APIs that offer managed key rotation do not offer key download.
https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys.cryptoKeyVersions/asymmetricSign
https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/signJwt
A TPM can treat a key as sealed data or an external key, but ideally the key is fully owned and non-exportable from the TPM (which can be verified by a key certification and attestation). cc @awly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This wasn't intended for an AWS API, but I've since refactored it to basically be a grpc wrapper for jose.OpaqueSigner
|
|
||
| message VerifyingKeysReply { | ||
| // A list of public signing keys | ||
| repeated PublicKey public_keys = 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it make sense to differentiate the active key?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've since broken this out into 2 calls, GetPublicKey() and ListPublicKeys()
|
|
||
| ### New API | ||
|
|
||
| I'm proposing creating a new versioned grpc API under `k8s.io/kubernetes/staging/src/k8s.io/apiserver/pkg/util/flag/keyreader`. 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` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Until this needs to be generalized, somewhere in k8s.io/kubernetes/serviceaccount is probably preferable. k8s.io/apiserver/pkg/util/flag is for mapping generic apiserver configuration to the command line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll update to be under k8s.io/kubernetes/serviceaccount
|
|
||
| - 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 | ||
| - Supporting out-of-process JWT signing and verifying |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is very desirable in my opinion. A plugin that supports out of process signing can support downloaded keys (where the plugin driver does the signing) but not the other way around.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll move this to be a goal. I had originally though of keeping this scoped down where out of process operations could be in a later KEP, but then it wasn't that hard to add support for it.
6a5b5a8 to
941e4ff
Compare
6718cef to
f2a8c8a
Compare
|
|
||
| ### Implementation Details/Notes/Constraints | ||
|
|
||
| The API server flag `--service-account-key-file` can be specified multiple times. If all files are regular files, the existing behavior will be unchanged. If both a unix socket and files are provided, the APIserver will exit with an error. If the file is a unix socket, all verifying keys for projected tokens will be read over the socket. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can be specified multiple times
This was probably a mistake in retrospect...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was probably a mistake in retrospect...
how so? it supports rotation methods where the old and new key are in different files
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What value does the flexibility provide vs requiring a single file with multiple pem blocks? The downside now is the ambiguity. I would be curious to see how many clusters are taking advantage of this.
|
|
||
| service KeyService { | ||
| // Sign an incoming payload | ||
| rpc SignPayload(SignPayloadRequest) returns (SignPayloadResponse) {} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()
|
I'm LGTM on this for alpha |
| // payload is the content to be signed | ||
| bytes payload = 1; | ||
| // algorithm specifies which algorithm to sign with | ||
| string algorithm = 2; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
}There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good to me
|
(Re-posting what I said on the ML thread) Have we considered a simple starting point of just reloading the data from disk (that may be in the non-goals section though I do not remember why)? I do think having external signers is valuable but will require investment in terms of some GRPC proxy bridge to an external signing service's API. Writing an updated file to disk is much easier and far less opinionated on how the rotation is done. So I have been thinking of this KEP and the TPM talk from last KC [1]. Part of me is wondering why we do not just use PKCS11 via [2] (with some minimal bridge code to implement The advantage of a custom protobuf API is that it gives folks the flexibility to implement it in whatever way they desire. The negative is that everyone has to go write this glue code because this API is something that we made up and does not line up with anything that exists today. My guess is that using a HSM as the external signer is likely to be the most desired use case (I welcome evidence for or against this claim). PKCS11 happens to be the industry standard for HSMs. [1] https://www.youtube.com/watch?v=_kxmkI8Kc8Y |
| participating-sigs: [] | ||
| reviewers: | ||
| - "@mikedanese" | ||
| - "@liggit" |
There was a problem hiding this comment.
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
|
pkcs#11 requires cgo and dynamically loading of shared libraries which poses challenges. The extension point would be significantly different than what we tend to do in this project (gRPC over UDS) for similar extension points. The KMS plugin is pretty close to this and we made the choice to go with gRPC there. The interface of pkcs#11 is also massive which adds to the complexity of the implementation, but it doesn't entirely cover the use case specific API that's proposed here. Out the door, it's not obvious how we would derive keyid to bridge a key provider that might use names, incrementing integers, hashes, etc... for keyids. We would also would not be able to add JWT specific features to this API such as kubernetes/kubernetes#61795 if we decide that we want to later on. |
|
Our RPC extension points are complex to write, poorly adopted by anyone except major vendor players, and in general don't align with our Kube patterns that have been successful. I am not in love with the KMS plugin although I understand why it exists and some of the tradeoffs. It would be helpful to have this include an explicit list of features that cannot be implemented via file on disk so that we can argue about them. I love that people want to go build complex plugins. I die a little bit on the inside when we make all solutions require complex plugins. |
@smarterclayton, sig-auth meeting, 2019-09-18 |
From zoom chat:
|
|
Are there any alternatives to GRPC other than pkcs#11 being proposed? I've mentioned the problems with pkcs#11 above. I would really like to land this feature as alpha in the next cycle. |
|
@smarterclayton any final thoughts? |
|
lgtm |
|
/lgtm |
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: enj, micahhausler, mikedanese The full list of commands accepted by this bot can be found here. The pull request process is described here
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
|
|
||
| ## Summary | ||
|
|
||
| The Kubernetes API server has always read service account keys from disk as the process starts, and kept them in memory for the duraiton 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: typo
|
No concerns from me /hold cancel |
b1d8549 to
8d8a01c
Compare
|
New changes are detected. LGTM label has been removed. |
This is my first KEP, any input on structure or details is appreciated.
Initial implementation PR is kubernetes/kubernetes#73110
/sig auth
/cc @mikedanese @liggitt @tallclair