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

INFOPLAT-1562 dynamic expiring auth headers #974

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a2177ac
INFOPLAT-1559 Adds auth header token expiry functionality
hendoxc Dec 4, 2024
c2e1595
INFOPLAT-1559 Removes sleep
hendoxc Dec 6, 2024
1ef77e8
INFOPLAT-1559 Updates test comments
hendoxc Dec 6, 2024
cdb4455
INFOPLAT-1560 Adds grpc.PerRPCCredentials implementation
hendoxc Dec 13, 2024
c2a542e
INFOPLAT-1560 Adds `AuthHeaderProvider` to beholder client config
hendoxc Dec 13, 2024
e40e8fd
INFOPLAT-1560 Allows `AuthHeaderProvider` to be used instead of stati…
hendoxc Dec 13, 2024
a4712db
Merge branch 'main' into INFOPLAT-1562-dynamic-expiring-auth-headers
hendoxc Dec 17, 2024
4dfa84f
INFOPLAT-1560 Update `refresh` logic
hendoxc Dec 17, 2024
88e9813
INFOPLAT-1560 Makes `refresh` thread safe
hendoxc Dec 17, 2024
83ef2a4
INFOPLAT-1560 Makes `RequireTransportSecurity` configurable
hendoxc Dec 17, 2024
5acee42
INFOPLAT-1560 Fixes tests
hendoxc Dec 17, 2024
13fd01c
INFOPLAT-1560 Runs formating and linting
hendoxc Dec 17, 2024
e6a4b35
INFOPLAT-1560 Runs formating and linting
hendoxc Dec 17, 2024
bb43e81
Merge branch 'INFOPLAT-1562-dynamic-expiring-auth-headers' of github.…
hendoxc Dec 17, 2024
2c36a16
INFOPLAT 1560 Add tests for NewAuthHeaderProvider and authHeaderPerRP…
hendoxc Dec 17, 2024
cf5dcda
INFOPLAT-1560 SImplifies caller usage
hendoxc Dec 19, 2024
da28e35
Merge branch 'main' into INFOPLAT-1562-dynamic-expiring-auth-headers
hendoxc Dec 19, 2024
d895532
INFOPLAT-1562 Handle configuration inconsistency
hendoxc Dec 20, 2024
00b67a4
Merge branch 'main' into INFOPLAT-1562-dynamic-expiring-auth-headers
hendoxc Dec 20, 2024
a809c1c
Merge branch 'main' into INFOPLAT-1562-dynamic-expiring-auth-headers
hendoxc Dec 23, 2024
5803b51
Merge branch 'INFOPLAT-1562-dynamic-expiring-auth-headers' of github.…
hendoxc Dec 23, 2024
aae9c45
INFOPLAT-1562 Reconfigure loop configs to use AuthHeaderProvider
hendoxc Dec 23, 2024
2bb5c5c
Revert "INFOPLAT-1562 Reconfigure loop configs to use AuthHeaderProvi…
hendoxc Dec 23, 2024
6cb806e
Merge branch 'main' into INFOPLAT-1562-dynamic-expiring-auth-headers
hendoxc Jan 9, 2025
fff5b5f
INFOPLAT-1562 Adjusts `authHeaderPerRPCCredentials` to have configura…
hendoxc Jan 9, 2025
d567ef5
Merge branch 'INFOPLAT-1562-dynamic-expiring-auth-headers' of github.…
hendoxc Jan 9, 2025
6f59cef
INFOPLAT-1562 Removes privkey from struct
hendoxc Jan 9, 2025
d5529df
INFPLAT-1562 Fixes linting
hendoxc Jan 9, 2025
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
159 changes: 151 additions & 8 deletions pkg/beholder/auth.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,170 @@
package beholder

import (
"context"
"crypto/ed25519"
"encoding/binary"
"fmt"
"sync"
"time"

"google.golang.org/grpc/credentials"
)

const (
// authHeaderKey is the name of the header that the node authenticator will use to send the auth token
authHeaderKey = "X-Beholder-Node-Auth-Token"
// authHeaderVersion is the version of the auth header format
authHeaderVersion1 = "1"
authHeaderVersion2 = "2"
// defaultAuthHeaderTTL is the default time before the auth header is refreshed
defaultAuthHeaderTTL = 1 * time.Minute
)

// authHeaderKey is the name of the header that the node authenticator will use to send the auth token
var authHeaderKey = "X-Beholder-Node-Auth-Token"
type AuthHeaderProvider interface {
// Credentials returns the PerRPCCredentials implementation
Credentials() credentials.PerRPCCredentials
// SetRequireTransportSecurity sets the value of requireTransportSecurity
SetRequireTransportSecurity(bool)
}

// AuthHeaderProviderConfig configures AuthHeaderProvider
type AuthHeaderProviderConfig struct {
HeaderTTL time.Duration
Version string
RequireTransportSecurity bool
}

// authHeaderPerRPCredentials is a PerRPCCredentials implementation that provides the auth headers
type authHeaderPerRPCCredentials struct {
privKey ed25519.PrivateKey
Copy link
Contributor

Choose a reason for hiding this comment

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

Passing a private key around will make eventual isolation of the keystore difficult. If possible, I'd like to see us use the keystore directly here

lastUpdated time.Time
headerTTL time.Duration
requireTransportSecurity bool
headers map[string]string
version string
mu sync.Mutex
}

func (config AuthHeaderProviderConfig) New(privKey ed25519.PrivateKey) AuthHeaderProvider {
if config.HeaderTTL <= 0 {
config.HeaderTTL = defaultAuthHeaderTTL
}
if config.Version == "" {
config.Version = authHeaderVersion2
}

creds := &authHeaderPerRPCCredentials{
privKey: privKey,
headerTTL: config.HeaderTTL,
version: config.Version,
requireTransportSecurity: config.RequireTransportSecurity,
}
// Initialize the headers ~ lastUpdated is 0 so the headers are generated on the first call
creds.refresh()
return creds
}

func NewAuthHeaderProvider(privKey ed25519.PrivateKey) AuthHeaderProvider {
return AuthHeaderProviderConfig{}.New(privKey)
}

func (a *authHeaderPerRPCCredentials) Credentials() credentials.PerRPCCredentials {
return a
}

func (a *authHeaderPerRPCCredentials) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) {
return a.getHeaders(), nil
}

func (a *authHeaderPerRPCCredentials) RequireTransportSecurity() bool {
return a.requireTransportSecurity
}

// SetRequireTransportSecurity sets the value of requireTransportSecurity
// This is to safeguard against inconsistent values between the PerRPCCredentials and the AuthHeaderProvider
func (a *authHeaderPerRPCCredentials) SetRequireTransportSecurity(newValue bool) {
a.mu.Lock()
defer a.mu.Unlock()
a.requireTransportSecurity = newValue
}

// getHeaders returns the auth headers, refreshing them if they are expired
func (a *authHeaderPerRPCCredentials) getHeaders() map[string]string {
if time.Since(a.lastUpdated) > a.headerTTL {
a.refresh()
}
hendoxc marked this conversation as resolved.
Show resolved Hide resolved
return a.headers
}

// refresh creates a new signed auth header token and sets the lastUpdated time to now
func (a *authHeaderPerRPCCredentials) refresh() {
a.mu.Lock()
defer a.mu.Unlock()

// authHeaderVersion is the version of the auth header format
var authHeaderVersion = "1"
timeNow := time.Now()

// BuildAuthHeaders creates the auth header value to be included on requests.
// The current format for the header is:
switch a.version {
// refresh doesn't actually do anything for version 1 since we are only signing the public key
// this for backwards compatibility and smooth transition to version 2
case authHeaderVersion1:
a.headers = BuildAuthHeaders(a.privKey)
case authHeaderVersion2:
a.headers = buildAuthHeadersV2(a.privKey, &AuthHeaderConfig{timestamp: timeNow.UnixMilli()})
default:
Copy link
Contributor

Choose a reason for hiding this comment

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

Given we want backwards compatibility, is a better default authHeadersVersion1? What's the migration period for the gateway server?

Copy link
Author

Choose a reason for hiding this comment

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

This change shouldn't effect current users, but v2 should be our default going forward.

a.headers = buildAuthHeadersV2(a.privKey, &AuthHeaderConfig{timestamp: timeNow.UnixMilli()})
}
// Set the lastUpdated time to now
a.lastUpdated = timeNow
}

// AuthHeaderConfig configures buildAuthHeadersV2
type AuthHeaderConfig struct {
timestamp int64
version string
}

// BuildAuthHeaders creates the auth headers to be included on requests.
// There are two formats for the header. Version `1` is:
//
// <version>:<public_key_hex>:<signature_hex>
//
// where the byte value of <public_key_hex> is what's being signed
// and <signature_hex> is the signature of the public key.
func BuildAuthHeaders(privKey ed25519.PrivateKey) map[string]string {
pubKey := privKey.Public().(ed25519.PublicKey)
messageBytes := pubKey
signature := ed25519.Sign(privKey, messageBytes)
headerValue := fmt.Sprintf("%s:%x:%x", authHeaderVersion, messageBytes, signature)

return map[string]string{authHeaderKey: headerValue}
return map[string]string{authHeaderKey: fmt.Sprintf("%s:%x:%x", authHeaderVersion1, messageBytes, signature)}
}

// buildAuthHeadersV2 creates the auth headers to be included on requests.
// Version `2` is:
//
// <version>:<public_key_hex>:<timestamp>:<signature_hex>
//
// where the concatenated byte value of <public_key_hex> & <timestamp> is what's being signed
Copy link
Contributor

Choose a reason for hiding this comment

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

If a malicious actor steals a v2 token or v1 token, changes the version to 1, 2, or 3 and then submits it to the gateway, how would the gateway currently respond?

Copy link
Author

Choose a reason for hiding this comment

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

If version of the token is not matching token scheme we expect, we consider the token malformed and reject the data

e.g we get a token that is version 1, we'd try to assert we can see a string that looks exactly like <version>:<public_key_hex>:<signature_hex>

func buildAuthHeadersV2(privKey ed25519.PrivateKey, config *AuthHeaderConfig) map[string]string {
hendoxc marked this conversation as resolved.
Show resolved Hide resolved
if config == nil {
config = &AuthHeaderConfig{}
}
if config.version == "" {
config.version = authHeaderVersion2
}
// If timestamp is negative or 0, set it to current timestamp.
// negative values cause overflow on conversion to uint64
Copy link
Contributor

Choose a reason for hiding this comment

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

are there production use cases for setting this timestamp? If not, a comment may be helpful to call out the foot gun with setting the timestamp outside of buildAuthHeadersV2

Copy link
Author

Choose a reason for hiding this comment

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

No production use-cases, any invalid values are corrected for

if config.timestamp <= 0 {
		config.timestamp = time.Now().UnixMilli()
	}

if config.timestamp <= 0 {
config.timestamp = time.Now().UnixMilli()
}

pubKey := privKey.Public().(ed25519.PublicKey)

timestampUnixMsBytes := make([]byte, 8)
binary.BigEndian.PutUint64(timestampUnixMsBytes, uint64(config.timestamp))

messageBytes := append(pubKey, timestampUnixMsBytes...)
signature := ed25519.Sign(privKey, messageBytes)

return map[string]string{authHeaderKey: fmt.Sprintf("%s:%x:%d:%x", config.version, pubKey, config.timestamp, signature)}
}
Loading
Loading