Skip to content
Open
Show file tree
Hide file tree
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
15 changes: 13 additions & 2 deletions tools/oprt2/pkg/attunehooks/authenticators/authenticator.go
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")
}
}
163 changes: 163 additions & 0 deletions tools/oprt2/pkg/attunehooks/authenticators/mtls/authenticator.go
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 {
Copy link
Copy Markdown
Contributor

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 Authenticator but 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.

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
Copy link
Copy Markdown
Contributor

@doggydogworld doggydogworld Dec 2, 2025

Choose a reason for hiding this comment

The 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") // true

It will get caught anyway during the setup function when it tries to startup the server but that may end up being a red herring if caught in logs. Might just be better to handle the error here.

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
Expand Up @@ -19,10 +19,18 @@ package certprovider
import (
"context"
"crypto/tls"
"errors"

"github.com/gravitational/shared-workflows/tools/oprt2/pkg/config"
)

// Provider provides a certificate for client authentication.
type Provider interface {
// Gets a keypair for use with mTLS authentication.
GetClientCertificate(context.Context) (*tls.Certificate, error)
}

// FromConfig builds a cert provider from the provided config.
func FromConfig(config config.CertificateProvider) (Provider, error) {
return nil, errors.New("not implemented")
}
40 changes: 40 additions & 0 deletions tools/oprt2/pkg/attunehooks/authenticators/mtls/config.go
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
}
34 changes: 34 additions & 0 deletions tools/oprt2/pkg/attunehooks/authenticators/mtls/options.go
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
}
}
Loading
Loading