-
Notifications
You must be signed in to change notification settings - Fork 4
Add Attune authenticators (token, mTLS) #421
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,24 @@ | ||
| package authenticators | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "log/slog" | ||
|
|
||
| "github.com/gravitational/shared-workflows/tools/oprt2/pkg/attunehooks/authenticators/mtls" | ||
| "github.com/gravitational/shared-workflows/tools/oprt2/pkg/attunehooks/authenticators/token" | ||
| "github.com/gravitational/shared-workflows/tools/oprt2/pkg/commandrunner" | ||
| "github.com/gravitational/shared-workflows/tools/oprt2/pkg/config" | ||
| ) | ||
|
|
||
| // FromConfig builds an Attune authenticator hook from the provided config. | ||
| func FromConfig(config config.Authenticator) (commandrunner.Hook, error) { | ||
| return nil, errors.New("not implemented") | ||
| func FromConfig(ctx context.Context, config config.Authenticator, logger *slog.Logger) (commandrunner.Hook, error) { | ||
| switch { | ||
| case config.MTLS != nil: | ||
| return mtls.FromConfig(ctx, config.MTLS, logger) | ||
| case config.Token != nil: | ||
| return token.FromConfig(config.Token) | ||
| default: | ||
| return nil, errors.New("no or unknown Attune authenticator specified") | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| /* | ||
| * Copyright 2025 Gravitational, Inc | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package mtls | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "log/slog" | ||
| "net" | ||
| "net/url" | ||
| "os/exec" | ||
|
|
||
| "github.com/gravitational/shared-workflows/tools/oprt2/pkg/attunehooks/authenticators/mtls/certprovider" | ||
| "github.com/gravitational/shared-workflows/tools/oprt2/pkg/attunehooks/authenticators/mtls/proxy" | ||
| "github.com/gravitational/shared-workflows/tools/oprt2/pkg/commandrunner" | ||
| "github.com/gravitational/shared-workflows/tools/oprt2/pkg/logging" | ||
| ) | ||
|
|
||
| // Authenticator authenticates with the Attune control plane with mTLS authentication. | ||
| // It does this by creating a local TCP proxy that wraps the connection in TLS, forwarding | ||
| // it to a reverse proxy in front of Attune. The reverse proxy handles authentication, and | ||
| // strips the TLS layer. | ||
| type Authenticator struct { | ||
| attuneEndpointHost string | ||
| attuneEndpointPort string | ||
| certprovider certprovider.Provider | ||
| logger *slog.Logger | ||
|
|
||
| // State vars | ||
| stopProxy func() error | ||
| proxyAddress string | ||
| } | ||
|
|
||
| var _ commandrunner.Hook = (*Authenticator)(nil) | ||
|
|
||
| // Authenticator creates a new Authenticator. | ||
| func NewAuthenticator(ctx context.Context, attuneEndpoint string, certprovider certprovider.Provider, opts ...AuthenticatorOption) (a *Authenticator, err error) { | ||
| a = &Authenticator{ | ||
| certprovider: certprovider, | ||
| logger: logging.DiscardLogger, | ||
| } | ||
|
|
||
| if err := a.setHostPort(attuneEndpoint); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| for _, opt := range opts { | ||
| opt(a) | ||
| } | ||
|
|
||
| if err := a.setup(ctx); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return a, nil | ||
| } | ||
|
|
||
| // Name is the name of the authenticator. | ||
| func (a *Authenticator) Name() string { | ||
| return "mTLS" | ||
| } | ||
|
|
||
| func (a *Authenticator) setHostPort(attuneEndpoint string) error { | ||
| attuneEndpointURL, err := url.Parse(attuneEndpoint) | ||
| if err == nil { | ||
| a.attuneEndpointHost = attuneEndpointURL.Hostname() | ||
| if a.attuneEndpointHost == "" { | ||
| return fmt.Errorf("the Attune endpoint does not contain a hostname: %q", attuneEndpoint) | ||
| } | ||
|
|
||
| a.attuneEndpointPort = attuneEndpointURL.Port() | ||
| if a.attuneEndpointPort != "" { | ||
| return nil | ||
| } | ||
|
|
||
| switch attuneEndpointURL.Scheme { | ||
| case "https": | ||
| a.attuneEndpointPort = "443" | ||
| return nil | ||
| case "http": | ||
| a.attuneEndpointPort = "80" | ||
| return nil | ||
| } | ||
| } | ||
|
|
||
| if host, port, err := net.SplitHostPort(attuneEndpoint); err == nil { | ||
| a.attuneEndpointHost = host | ||
| a.attuneEndpointPort = port | ||
| return nil | ||
| } | ||
|
|
||
|
Comment on lines
+100
to
+105
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Not sure if this function is meant to be exhaustive but I did notice that unhandled schemes could fall-through to here and it would return a nil error. a := &Authenticator{}
// i.e. with a typo
if err := a.setHostPort("httpss://attune-endpoint.sh"); err != nil {
log.Fatal(err) // skipped: above will return nil
}
fmt.Println(a.attuneEndpointHost == "httpss") // true
fmt.Println(a.attuneEndpointPort == "//attune-endpoint.sh") // trueIt will get caught anyway during the |
||
| return fmt.Errorf("failed to parse Attune endpoint: %q", attuneEndpoint) | ||
| } | ||
|
|
||
| func (a *Authenticator) setup(ctx context.Context) error { | ||
| // Start a TCP proxy | ||
| proxyServeCtx, cancelProxyServe := context.WithCancel(ctx) | ||
| t2tp := proxy.NewTCP2TLS(a.attuneEndpointHost, a.attuneEndpointPort, proxy.WithLogger(a.logger), proxy.WithClientCertificateProvider(a.certprovider)) | ||
| proxyServeErr := make(chan error) | ||
| go func() { | ||
| defer close(proxyServeErr) | ||
| proxyServeErr <- t2tp.ListenAndServe(proxyServeCtx) | ||
| }() | ||
|
|
||
| a.stopProxy = func() error { | ||
| cancelProxyServe() | ||
| actualProxyServeErr := <-proxyServeErr | ||
| if actualProxyServeErr != nil { | ||
| actualProxyServeErr = fmt.Errorf("the TLS2TCP proxy failed while serving: %w", actualProxyServeErr) | ||
| } | ||
| return actualProxyServeErr | ||
| } | ||
|
|
||
| proxyAddress, err := t2tp.GetAddress(ctx) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get TLS2TCP listening address: %w", err) | ||
| } | ||
| a.proxyAddress = proxyAddress.String() | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // Command adds mTLS authentication to the Attune command. | ||
| func (a *Authenticator) Command(_ context.Context, cmd *exec.Cmd) error { | ||
| cmd.Env = append( | ||
| cmd.Env, | ||
| // This value is only here because the Attune CLI requires it to be set. It is | ||
| // meaningless, and the ingres gateway replaces on backend requests. | ||
| "ATTUNE_API_TOKEN=dummy-value", | ||
| // This must be HTTP to avoid dealing with trust issues, and because the proxy is | ||
| // only aware of TCP, downwards. The proxy always binds to localhost anyway, so | ||
| // there isn't a security risk here that we are concerned about. | ||
| "ATTUNE_API_ENDPOINT=http://"+a.proxyAddress, | ||
| ) | ||
| return nil | ||
| } | ||
|
|
||
| // Close closes the authenticator. | ||
| func (a *Authenticator) Close(ctx context.Context) error { | ||
| if a.stopProxy == nil { | ||
| return nil | ||
| } | ||
|
|
||
| if err := a.stopProxy(); err != nil { | ||
| return fmt.Errorf("failed to stop TCP2TLS proxy: %w", err) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| /* | ||
| * Copyright 2025 Gravitational, Inc | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package mtls | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "log/slog" | ||
|
|
||
| "github.com/gravitational/shared-workflows/tools/oprt2/pkg/attunehooks/authenticators/mtls/certprovider" | ||
| "github.com/gravitational/shared-workflows/tools/oprt2/pkg/config" | ||
| ) | ||
|
|
||
| func FromConfig(ctx context.Context, config *config.MTLSAuthenticator, logger *slog.Logger) (*Authenticator, error) { | ||
| certProvider, err := certprovider.FromConfig(config.CertificateSource) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to get mTLS certificate source: %w", err) | ||
| } | ||
|
|
||
| authenticator, err := NewAuthenticator(ctx, config.Endpoint, certProvider, WithLogger(logger)) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to create mTLS authenticator: %w", err) | ||
| } | ||
|
|
||
| return authenticator, nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| /* | ||
| * Copyright 2025 Gravitational, Inc | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package mtls | ||
|
|
||
| import ( | ||
| "log/slog" | ||
|
|
||
| "github.com/gravitational/shared-workflows/tools/oprt2/pkg/logging" | ||
| ) | ||
|
|
||
| type AuthenticatorOption func(a *Authenticator) | ||
|
|
||
| func WithLogger(logger *slog.Logger) AuthenticatorOption { | ||
| return func(a *Authenticator) { | ||
| if logger == nil { | ||
| logger = logging.DiscardLogger | ||
| } | ||
| a.logger = logger | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| /* | ||
| * Copyright 2025 Gravitational, Inc | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package proxy | ||
|
|
||
| import ( | ||
| "log/slog" | ||
|
|
||
| "github.com/gravitational/shared-workflows/tools/oprt2/pkg/attunehooks/authenticators/mtls/certprovider" | ||
| "github.com/gravitational/shared-workflows/tools/oprt2/pkg/logging" | ||
| ) | ||
|
|
||
| type TCP2TLSOption func(*TCP2TLS) | ||
|
|
||
| // WithClientCertificateProvider configures the proxy to use mTLS authentication via certs provided by the provider. | ||
| func WithClientCertificateProvider(provider certprovider.Provider) TCP2TLSOption { | ||
| return func(t2t *TCP2TLS) { | ||
| t2t.clientCertProvider = provider | ||
| } | ||
| } | ||
|
|
||
| // WithLogger configures the proxy with the provided logger. | ||
| func WithLogger(logger *slog.Logger) TCP2TLSOption { | ||
| return func(t2t *TCP2TLS) { | ||
| if logger == nil { | ||
| logger = logging.DiscardLogger | ||
| } | ||
| t2t.logger = logger | ||
| } | ||
| } |
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 believe I understand why we need this
Authenticatorbut I'm not entirely sure. A little bit more context in the commentary would be helpful.From what I can imagine, this is necessary because Attune does not support mTLS natively? Or we have certain implementation requirements for it that Attune does not meet? In that case this implements a custom authentication middleware between the Attune client and control plane.