atunnel: broker actor certificates through atelet - #708
atunnel: broker actor certificates through atelet#708Eitan Yarmush (EItanya) wants to merge 7 commits into
Conversation
|
Thanks Eitan Yarmush (@EItanya) ! Quick question before I review more deeply today. With the goal of "having a JWT available to atunnel to append on a CONNECT request to the egress" -- What parts of the this PR addresses? Here are the parts we needed (before this pr -- havent reviewed it yet);
From a very brief glance - I think your PR is attempting to do all three. Is that correct? Also, For (1), we likely need to sort out two things to make the JWTs useful:
See for ref - substrate/cmd/ateapi/internal/actoridentity/actoridentity.go Lines 135 to 138 in 3ed6aa0 |
|
Yes, with two clarifications: this PR renews JWTs, not actor certificates, and each worker has only one active actor.
I agree the issuer and subject contracts remain unresolved. This PR intentionally retains the existing issuer/subject behavior and does not implement PEP verification yet. Before verification lands, we need a stable OIDC issuer with discovery/JWKS reachable by the PEP. |
3306005 to
facebf3
Compare
| ActorResourceVersion: actor.GetMetadata().GetVersion(), | ||
| WorkerPodUid: req.GetWorkerPodUid(), |
There was a problem hiding this comment.
why do we need these?
| clientJWTIssuer = pflag.String("client-jwt-issuer", "", "The expected issuer URL for client JWTs.") | ||
| clientJWTAudience = pflag.String("client-jwt-audience", "", "The expected audience for client JWTs.") | ||
| actorIDJWTPoolFile = pflag.String("actor-id-jwt-pool", "", "The file that contains the serialized JWT authority pool for signing actor JWTs") | ||
| egressGatewayAddress = pflag.String("egress-gateway-address", "", "Address of the egress PEP. Empty disables tunneled egress.") |
There was a problem hiding this comment.
why do need egressgateway on ateapi?
| // control resolves the authenticated worker Pod to its current assignment. | ||
| control ateapipb.ControlClient | ||
| // identity revalidates that assignment and signs the actor certificate. | ||
| identity ateapipb.ActorIdentityClient |
There was a problem hiding this comment.
identity as "identityService"? feels weird to call a field identity that does not represent an identity
| // TODO: Before release, request an atunnel-specific MintCert purpose and | ||
| // require the egress PEP to reject generic actor certificates. | ||
| // The request deliberately carries no actor identity. The authenticated Pod | ||
| // UID is the only input used to select a worker and its current assignment. | ||
| workerUID, err := authenticatedWorkerUID(ctx) |
There was a problem hiding this comment.
not sure I am following the TODO comment. I thought we were saying that we need it to be an actor cert thats specifically for atunnel use.
There was a problem hiding this comment.
I think it would be fine to merge without that, and then add it in a follow up? Either way works.
| return identity.PodUID, nil | ||
| } | ||
|
|
||
| func restrictClientToNode(node *substratex509.PodIdentity) func(tls.ConnectionState) error { |
There was a problem hiding this comment.
can we get a more descriptive name and/or a comment that tells what you are doing in this func?
| containers: req.GetSpec().GetContainers(), | ||
| assetPaths: req.GetRuntimeAssetPaths(), | ||
|
|
||
| actorVersion: req.GetActorVersion(), |
There was a problem hiding this comment.
whats the rationale for removing the actorVersion
| // and in every ateom pod (which mounts them as overlay lowerdirs). | ||
| ImageCacheDir = filepath.Join(BasePath, "image-cache") | ||
| ImageCacheDir = filepath.Join(BasePath, "image-cache") | ||
| CredentialBrokerSocket = filepath.Join(BasePath, "credential-broker.sock") |
There was a problem hiding this comment.
can we add a comment on top of CredentialBrokerSocket?
| "google.golang.org/grpc/credentials" | ||
| ) | ||
|
|
||
| // BrokerCertificateSource owns atunnel's actor private key and obtains the |
There was a problem hiding this comment.
I could be wrong but dont we want atelet to be the one that generate the private keys?
| // Activate allows egress for one actor. There can be only one active actor per | ||
| // worker. bearerToken may be empty until actor JWT issuance is available. | ||
| func (e *Egress) Activate(dialer EgressDialer, atespace, actorName string, actorVersion int64, bearerToken string) error { | ||
| // Activate allows egress with a previously obtained actor certificate and |
There was a problem hiding this comment.
with a previously obtained actor certificate
thinking out loud - do we care about actorVersion(s)?
|
|
||
| message MintActorCertificateRequest { | ||
| // DER-encoded PKCS #10 certificate signing request. Atunnel retains the | ||
| // corresponding private key. |
There was a problem hiding this comment.
My initial thinking was that atelet is the thing we trust, and hence it generates the private keys and hands them to atunnel. Like kubelet. But your approach may be bettter, though keys would have to be regenerated across suspend/resume?
/cc Taahir Ahmed (@ahmedtd) for thoughts.
|
|
||
| // Exact worker Pod requesting the certificate. Ateapi verifies that this | ||
| // worker is still assigned to the actor before signing. | ||
| string worker_pod_uid = 5; |
There was a problem hiding this comment.
Can you add an additional note here to the effect that it's safe to trust the worker_pod_uid specified in the request body because atelet is the client, and atelet is trusted to tell us which worker pod is requesting the certificate (as long as the worker pod is associated with that particular atelet).
| }, | ||
| } | ||
|
|
||
| if err := substratex509.AddActorIdentityToCertificate(&substratex509.ActorIdentity{ |
There was a problem hiding this comment.
We need an additional indication here that this is an ateom or atunnel, not the actor using its own identity.
Maybe fields in the ActorIdentity extension for:
- SystemComponentName
- SystemComponentPodName
- SystemComponentPodUID
| // TODO: Before release, request an atunnel-specific MintCert purpose and | ||
| // require the egress PEP to reject generic actor certificates. | ||
| // The request deliberately carries no actor identity. The authenticated Pod | ||
| // UID is the only input used to select a worker and its current assignment. | ||
| workerUID, err := authenticatedWorkerUID(ctx) |
There was a problem hiding this comment.
I think it would be fine to merge without that, and then add it in a follow up? Either way works.
| return nil, err | ||
| } | ||
| var assigned *ateapipb.Worker | ||
| for pageToken := ""; ; { |
There was a problem hiding this comment.
Looping over all workers isn't going to work even in the medium term. Do we have any way to fetch the correct worker in a single request?
| ateapiAddress = pflag.String("ateapi-address", "dns:///api.ate-system.svc:443", "ateapi gRPC target used by the credential broker.") | ||
| ateapiCAFile = pflag.String("ateapi-ca-file", "/run/servicedns.podcert.ate.dev/trust-bundle.pem", "CA bundle used to verify ateapi.") | ||
| ateapiServerName = pflag.String("ateapi-server-name", "api.ate-system.svc", "DNS name expected on the ateapi certificate.") |
There was a problem hiding this comment.
Normally ate-api-server dials out to atelet. In this case, we need to dial back.
| serverboot.Fatal(ctx, "Failed to load atelet Pod identity", fmt.Errorf("credential bundle has no Pod identity")) | ||
| } | ||
| brokerTLS := tlsCfg.Clone() | ||
| brokerTLS.VerifyConnection = restrictClientToNode(ateletIdentity) |
There was a problem hiding this comment.
I think over a UDS we should be able to get this level of control just from filesystem permissions? Ie, we create a UDS socket file for each individual ateom, and make sure only that ateom can access it.
TLS over a UDS is a bit strange.
Closes #706
Summary
Testing
make verifygo test -race ./internal/atunnel