Skip to content

Commit c8f2254

Browse files
authored
Add Redis Storage Backend for Auth Server (#3639)
Implements a Redis Sentinel-backed storage backend for the authorization server's `Storage` interface, enabling horizontal scaling of ToolHive auth servers. Multiple instances can now share authentication state via Redis with automatic failover support. This is Phase 1 of the Redis Storage feature, providing the core implementation with comprehensive unit tests.
1 parent c9ed08e commit c8f2254

9 files changed

Lines changed: 2871 additions & 13 deletions

File tree

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.25.7
55
require (
66
dario.cat/mergo v1.0.2
77
github.com/1password/onepassword-sdk-go v0.3.1
8+
github.com/alicebob/miniredis/v2 v2.36.1
89
github.com/aws/aws-sdk-go-v2 v1.41.1
910
github.com/aws/aws-sdk-go-v2/config v1.32.7
1011
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6
@@ -40,6 +41,7 @@ require (
4041
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
4142
github.com/pressly/goose/v3 v3.26.0
4243
github.com/prometheus/client_golang v1.23.2
44+
github.com/redis/go-redis/v9 v9.17.3
4345
github.com/sigstore/protobuf-specs v0.5.0
4446
github.com/sigstore/sigstore-go v1.1.4
4547
github.com/spf13/viper v1.21.0
@@ -114,6 +116,7 @@ require (
114116
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
115117
github.com/danieljoos/wincred v1.2.2 // indirect
116118
github.com/dgraph-io/ristretto v1.0.0 // indirect
119+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
117120
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect
118121
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect
119122
github.com/docker/cli v29.0.3+incompatible // indirect
@@ -247,6 +250,7 @@ require (
247250
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
248251
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
249252
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
253+
github.com/yuin/gopher-lua v1.1.1 // indirect
250254
go.mongodb.org/mongo-driver v1.17.6 // indirect
251255
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 // indirect
252256
go.opentelemetry.io/contrib/propagators/b3 v1.21.0 // indirect

go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNx
5151
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
5252
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
5353
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
54+
github.com/alicebob/miniredis/v2 v2.36.1 h1:Dvc5oAnNOr7BIfPn7tF269U8DvRW1dBG2D5n0WrfYMI=
55+
github.com/alicebob/miniredis/v2 v2.36.1/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
5456
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
5557
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
5658
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
@@ -100,6 +102,10 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
100102
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
101103
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
102104
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
105+
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
106+
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
107+
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
108+
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
103109
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
104110
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
105111
github.com/cedar-policy/cedar-go v1.5.1 h1:xo4lJs67XKgRcsIKCTPAFhLRALLHP/Z8vi5sWwTKIzQ=
@@ -168,6 +174,8 @@ github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4
168174
github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc=
169175
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
170176
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
177+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
178+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
171179
github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc=
172180
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE=
173181
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc=
@@ -675,6 +683,8 @@ github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEo
675683
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
676684
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
677685
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
686+
github.com/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4=
687+
github.com/redis/go-redis/v9 v9.17.3/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
678688
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
679689
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
680690
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
@@ -842,6 +852,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
842852
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
843853
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
844854
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
855+
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
856+
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
845857
github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
846858
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
847859
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=

pkg/authserver/storage/config.go

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,5 @@
1-
// Copyright 2025 Stacklok, Inc.
2-
//
3-
// Licensed under the Apache License, Version 2.0 (the "License");
4-
// you may not use this file except in compliance with the License.
5-
// You may obtain a copy of the License at
6-
//
7-
// http://www.apache.org/licenses/LICENSE-2.0
8-
//
9-
// Unless required by applicable law or agreed to in writing, software
10-
// distributed under the License is distributed on an "AS IS" BASIS,
11-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12-
// See the License for the specific language governing permissions and
13-
// limitations under the License.
1+
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
143

154
package storage
165

@@ -23,6 +12,9 @@ const (
2312
// TypeMemory uses in-memory storage (default).
2413
TypeMemory Type = "memory"
2514

15+
// TypeRedis uses Redis Sentinel-backed storage for distributed deployments.
16+
TypeRedis Type = "redis"
17+
2618
// DefaultCleanupInterval is how often the background cleanup runs.
2719
DefaultCleanupInterval = 5 * time.Minute
2820

@@ -40,6 +32,10 @@ const (
4032

4133
// DefaultPKCETTL is the default TTL for PKCE requests (same as auth codes).
4234
DefaultPKCETTL = 10 * time.Minute
35+
36+
// DefaultPublicClientTTL is the TTL for dynamically registered public clients.
37+
// This prevents unbounded growth from DCR. Confidential clients don't expire.
38+
DefaultPublicClientTTL = 30 * 24 * time.Hour // 30 days
4339
)
4440

4541
// Config configures the storage backend.
@@ -61,4 +57,54 @@ func DefaultConfig() *Config {
6157
type RunConfig struct {
6258
// Type specifies the storage backend type. Defaults to "memory".
6359
Type string `json:"type,omitempty" yaml:"type,omitempty"`
60+
61+
// RedisConfig is the Redis-specific configuration when Type is "redis".
62+
RedisConfig *RedisRunConfig `json:"redisConfig,omitempty" yaml:"redisConfig,omitempty"`
63+
}
64+
65+
// RedisRunConfig is the serializable Redis configuration for RunConfig.
66+
// This is designed for Sentinel-only deployments with ACL user authentication.
67+
type RedisRunConfig struct {
68+
// SentinelConfig contains Sentinel-specific configuration.
69+
SentinelConfig *SentinelRunConfig `json:"sentinelConfig,omitempty" yaml:"sentinelConfig,omitempty"`
70+
71+
// AuthType must be "aclUser" - only ACL user authentication is supported.
72+
AuthType string `json:"authType" yaml:"authType"`
73+
74+
// ACLUserConfig contains ACL user authentication configuration.
75+
ACLUserConfig *ACLUserRunConfig `json:"aclUserConfig,omitempty" yaml:"aclUserConfig,omitempty"`
76+
77+
// KeyPrefix for multi-tenancy, typically "thv:auth:{ns}:{name}:".
78+
KeyPrefix string `json:"keyPrefix" yaml:"keyPrefix"`
79+
80+
// DialTimeout is the timeout for establishing connections (e.g., "5s").
81+
DialTimeout string `json:"dialTimeout,omitempty" yaml:"dialTimeout,omitempty"`
82+
83+
// ReadTimeout is the timeout for read operations (e.g., "3s").
84+
ReadTimeout string `json:"readTimeout,omitempty" yaml:"readTimeout,omitempty"`
85+
86+
// WriteTimeout is the timeout for write operations (e.g., "3s").
87+
WriteTimeout string `json:"writeTimeout,omitempty" yaml:"writeTimeout,omitempty"`
88+
}
89+
90+
// SentinelRunConfig contains Redis Sentinel configuration.
91+
type SentinelRunConfig struct {
92+
// MasterName is the name of the Redis Sentinel master.
93+
MasterName string `json:"masterName" yaml:"masterName"`
94+
95+
// SentinelAddrs is the list of Sentinel addresses (host:port).
96+
SentinelAddrs []string `json:"sentinelAddrs" yaml:"sentinelAddrs"`
97+
98+
// DB is the Redis database number (default: 0).
99+
DB int `json:"db,omitempty" yaml:"db,omitempty"`
100+
}
101+
102+
// ACLUserRunConfig contains Redis ACL user authentication configuration.
103+
// Credentials are read from environment variables for security.
104+
type ACLUserRunConfig struct {
105+
// UsernameEnvVar is the environment variable containing the Redis username.
106+
UsernameEnvVar string `json:"usernameEnvVar" yaml:"usernameEnvVar"`
107+
108+
// PasswordEnvVar is the environment variable containing the Redis password.
109+
PasswordEnvVar string `json:"passwordEnvVar" yaml:"passwordEnvVar"`
64110
}

pkg/authserver/storage/memory.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@ func NewMemoryStorage(opts ...MemoryStorageOption) *MemoryStorage {
139139
return s
140140
}
141141

142+
// Health is a no-op for in-memory storage since it is always available.
143+
func (*MemoryStorage) Health(_ context.Context) error {
144+
return nil
145+
}
146+
142147
// Close stops the background cleanup goroutine and waits for it to finish.
143148
// This should be called when the storage is no longer needed.
144149
func (s *MemoryStorage) Close() error {

pkg/authserver/storage/mocks/mock_storage.go

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)