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

Attenuate tokens for remote builds #3443

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft

Conversation

saleemrashid
Copy link
Contributor

This attenuates tokens sent to remote builders and registry to have the minimum permissions required:

  • To use the remote builders
  • To push images to registry for apps in that organization

This depends on superfly/fly-go#36.

Copy link
Contributor

@timflyio timflyio left a comment

Choose a reason for hiding this comment

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

I looked this over and didn't notice anything or have any comments.

Copy link
Member

@btoews btoews left a comment

Choose a reason for hiding this comment

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

Looks great! I left a few small questions/comments/suggestions.

// We need the build context to get a build token
buildCtx, cancel := context.WithCancelCause(ap.ctx)
// Cancel the build token request when the gRPC context is cancelled
stop := context.AfterFunc(ctx, func() {
Copy link
Member

Choose a reason for hiding this comment

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

TIL context.AfterFunc. This is a neat way to combine two contexts.

var (
client = fly.ClientFromContext(ctx)
io = iostreams.FromContext(ctx)
cfg = appconfig.ConfigFromContext(ctx)
)

daemonType := imgsrc.NewDockerDaemonType(!flag.GetBool(ctx, "build-remote-only"), !flag.GetBool(ctx, "build-local-only"), env.IsCI(), flag.GetBool(ctx, "build-nixpacks"))
resolver := imgsrc.NewResolver(daemonType, client, appName, io, flag.GetWireguard(ctx))
resolver := imgsrc.NewResolver(daemonType, client, app, io, flag.GetWireguard(ctx))
defer imgsrc.RevokeBuildTokens(context.WithoutCancel(ctx), app)
Copy link
Member

Choose a reason for hiding this comment

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

Instead of having to know when/where to call RevokeBuildTokens, we could do

task.FromContext(ctx).RunFinalizer(func(ctx){
  revokeToken(...)
})

as a part of getBuildToken().

Also TIL context.WithoutCancel(). You know all the cool new APIs!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh! I looked for an API like that and couldn't find it :) Thank you!

Comment on lines +33 to +38
var token string
if len(tokens.UserTokens) > 0 {
token, err = getBuildTokenFromUser(ctx, orgID, app.Organization)
} else {
token, err = getBuildTokenFromMacaroons(orgID, tokens.Macaroons())
}
Copy link
Member

Choose a reason for hiding this comment

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

I guess when we have oauth+macaroons, we can't easily make a build token from the macaroons because the discharge tokens are short lived?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I'm favouring user tokens to copy the existing behaviour from registry, and because the temporary deploy token has fewer moving parts.

That being said, this should be fairly resilient to token expiry? We should be calling getBuildToken right before we use the token, and I think tokens.Macaroons() will return tokens that won't expire for at least a minute (from what I understand of the pruning/fetching logic). There's maybe some contrived edge case if you disable BuildKit and have a long multi-stage build and use FROM registry.fly.io.

Copy link
Member

Choose a reason for hiding this comment

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

I don't have the whole flow in my head. Flyctl call rchab and then rchab uses the tokens to talk to registry, right? I was worried about tokens expiring before rchab has a chance to use them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

rchab is basically just a proxy to dockerd. So "build and push" is flyctl asking rchab to do a build, then flyctl asking rchab to push the image it just created (so the build and push will each do a fresh getBuildToken call). And in a BuildKit world, we don't send tokens with the initial build request, BuildKit makes a request back to flyctl when it needs them (6af24e1). So rchab should be using the tokens ~immediately after getting them.

The edge case is a non-BuildKit, legacy multi-stage build where flyctl sends tokens at the start of the build, but Docker doesn't pull the images for later stages until much later, because I think legacy builds are fully sequential.

Copy link
Member

Choose a reason for hiding this comment

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

Makes sense. Thanks for the context. There's a lot more going on than I realized.

delete(cfg.CachedBuildTokens, orgID)

apiClient := fly.ClientFromContext(ctx)
return apiClient.RevokeLimitedAccessToken(ctx, cachedToken.ID)
Copy link
Member

Choose a reason for hiding this comment

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

Another option that wouldn't require keeping track of the ID is the logOut mutation.


// Append all the relevant discharge tokens
for _, cav := range macaroon.GetCaveats[*macaroon.Caveat3P](&m.UnsafeCaveats) {
toks = append(toks, dischargeByTicket[hex.EncodeToString(cav.Ticket)])
Copy link
Member

Choose a reason for hiding this comment

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

Should we return an error here if we don't have the discharge?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think we want to return an error, just skip this permission token entirely so we don't fail if there was another one which we did have all the discharges for. (If there weren't, we'd hit the len(toks) == 0 below).

I was putting off refactoring that to whenever I rip out the discharge collection code into a macaroon helper function. That being said, it should probably be wrapped in if dis, ok := dischargeByTicket[hex.EncodeToString(cav.Ticket)]; ok.

Copy link
Member

Choose a reason for hiding this comment

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

Makes sense. Right now, we're adding the permission token even if we don't have the discharges for it though, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's right. Does that cause issues, apart from being a bit wasteful?

Copy link
Member

Choose a reason for hiding this comment

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

I don't think it's harmful

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants