From 07ce5fe21db55220962a010ade9404a3569ddd0a Mon Sep 17 00:00:00 2001 From: Sky Mulley Date: Wed, 5 Aug 2026 15:28:22 +0100 Subject: [PATCH 1/2] fix: master key resets through the panel now automatically propogate on the daemon --- config/config.go | 55 +++++++++++++++++++---------- remote/http.go | 24 +++++++++++-- router/router_server_backup_test.go | 2 ++ router/router_system.go | 21 +++++++++++ 4 files changed, 82 insertions(+), 20 deletions(-) diff --git a/config/config.go b/config/config.go index b1331236f..5fa96844f 100644 --- a/config/config.go +++ b/config/config.go @@ -411,6 +411,41 @@ func Set(c *Configuration) { _config = c } +// ResolveRemoteToken populates the derived Token field after the Panel has sent +// us new token values. Because the resolved token is what everything else in +// Wings authenticates against, this has to be called whenever the underlying +// AuthenticationToken values change, otherwise the previously resolved token +// stays in use until the process is restarted. +func (c *Configuration) ResolveRemoteToken() error { + return c.resolveToken(false) +} + +// resolveToken resolves the token to use, preferring values pinned through the +// environment so that a token supplied by the system running Wings is never +// replaced by one sent to us by the Panel. expandLocal controls whether the +// values held in the configuration itself are trusted enough to be passed through +// Expand. +func (c *Configuration) resolveToken(expandLocal bool) error { + resolve := func(env, local string) (string, error) { + if env != "" { + return Expand(env) + } + if expandLocal { + return Expand(local) + } + return local, nil + } + + var err error + if c.Token.ID, err = resolve(os.Getenv("WINGS_TOKEN_ID"), c.AuthenticationTokenId); err != nil { + return err + } + if c.Token.Token, err = resolve(os.Getenv("WINGS_TOKEN"), c.AuthenticationToken); err != nil { + return err + } + return nil +} + // SetDebugViaFlag tracks if the application is running in debug mode because of // a command line flag argument. If so we do not want to store that configuration // change to the disk. @@ -600,23 +635,7 @@ func FromFile(path string) error { return err } - c.Token = Token{ - ID: os.Getenv("WINGS_TOKEN_ID"), - Token: os.Getenv("WINGS_TOKEN"), - } - if c.Token.ID == "" { - c.Token.ID = c.AuthenticationTokenId - } - if c.Token.Token == "" { - c.Token.Token = c.AuthenticationToken - } - - c.Token.ID, err = Expand(c.Token.ID) - if err != nil { - return err - } - c.Token.Token, err = Expand(c.Token.Token) - if err != nil { + if err := c.resolveToken(true); err != nil { return err } @@ -860,7 +879,7 @@ func Expand(v string) (string, error) { b, err := os.ReadFile(p) if err != nil { - return "", nil + return "", err } v = string(bytes.TrimRight(bytes.TrimRight(b, "\r"), "\n")) } diff --git a/remote/http.go b/remote/http.go index da3a413af..852db52fd 100644 --- a/remote/http.go +++ b/remote/http.go @@ -9,6 +9,7 @@ import ( "net/http" "strconv" "strings" + "sync" "time" "github.com/pterodactyl/wings/internal/models" @@ -33,11 +34,13 @@ type Client interface { SetTransferStatus(ctx context.Context, uuid string, successful bool) error ValidateSftpCredentials(ctx context.Context, request SftpAuthRequest) (SftpAuthResponse, error) SendActivityLogs(ctx context.Context, activity []models.Activity) error + SetCredentials(id, token string) } type client struct { httpClient *http.Client baseUrl string + mu sync.RWMutex tokenId string token string maxAttempts int @@ -68,6 +71,22 @@ func WithCredentials(id, token string) ClientOption { } } +// SetCredentials replaces the credentials used when making requests to the +// remote API endpoint. +func (c *client) SetCredentials(id, token string) { + c.mu.Lock() + defer c.mu.Unlock() + c.tokenId = id + c.token = token +} + +// credentials returns the credentials currently in use by this client. +func (c *client) credentials() (string, string) { + c.mu.RLock() + defer c.mu.RUnlock() + return c.tokenId, c.token +} + // WithHttpClient sets the underlying HTTP client instance to use when making // requests to the Panel API. func WithHttpClient(httpClient *http.Client) ClientOption { @@ -105,10 +124,11 @@ func (c *client) requestOnce(ctx context.Context, method, path string, body io.R return nil, err } - req.Header.Set("User-Agent", fmt.Sprintf("Pterodactyl Wings/v%s (id:%s)", system.Version, c.tokenId)) + tokenId, token := c.credentials() + req.Header.Set("User-Agent", fmt.Sprintf("Pterodactyl Wings/v%s (id:%s)", system.Version, tokenId)) req.Header.Set("Accept", "application/vnd.pterodactyl.v1+json") req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s.%s", c.tokenId, c.token)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s.%s", tokenId, token)) // Call all opts functions to allow modifying the request for _, o := range opts { diff --git a/router/router_server_backup_test.go b/router/router_server_backup_test.go index 3a9f5978f..15951fc73 100644 --- a/router/router_server_backup_test.go +++ b/router/router_server_backup_test.go @@ -83,6 +83,8 @@ func (c backupTestRemoteClient) SendActivityLogs(context.Context, []models.Activ return nil } +func (c backupTestRemoteClient) SetCredentials(_, _ string) {} + type backupTestEnvironment struct{} func (backupTestEnvironment) Type() string { return "test" } diff --git a/router/router_system.go b/router/router_system.go index 75773c2f3..2034af55b 100644 --- a/router/router_system.go +++ b/router/router_system.go @@ -143,6 +143,22 @@ func postUpdateConfiguration(c *gin.Context) { cfg.Api.Ssl.CertificateFile = config.Get().Api.Ssl.CertificateFile } + // The token that everything authenticates against is a derived value that is + // not part of the payload sent by the Panel, so it has to be re-resolved from + // the new token values. + if err := cfg.ResolveRemoteToken(); err != nil { + middleware.CaptureAndAbort(c, err) + return + } + + // Refuse to go any further with a token we could never authenticate against. + if cfg.Token.ID == "" || cfg.Token.Token == "" { + middleware.CaptureAndAbort(c, errors.New("config: refusing to apply an update with an empty authentication token")) + return + } + + tokenId, token := cfg.Token.ID, cfg.Token.Token + // Try to write this new configuration to the disk before updating our global // state with it. if err := config.WriteToDisk(cfg); err != nil { @@ -152,6 +168,11 @@ func postUpdateConfiguration(c *gin.Context) { // Since we wrote it to the disk successfully now update the global configuration // state to use this new configuration struct. config.Set(cfg) + + // Requests we make back to the Panel use credentials that were captured when + // the client was created at boot, so they have to be rotated explicitly. + middleware.ExtractManager(c).Client().SetCredentials(tokenId, token) + c.JSON(http.StatusOK, postUpdateConfigurationResponse{ Applied: true, }) From 392e52ca2bf1a3a683037736f8b6128685a83687 Mon Sep 17 00:00:00 2001 From: Sky Mulley Date: Thu, 6 Aug 2026 02:33:40 +0100 Subject: [PATCH 2/2] refactor: remove duplicate ResolveRemoteToken, resolvetoken with docblock is good enough --- config/config.go | 31 ++++++++++++------------------- router/router_system.go | 2 +- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/config/config.go b/config/config.go index 5fa96844f..cbd913243 100644 --- a/config/config.go +++ b/config/config.go @@ -411,29 +411,22 @@ func Set(c *Configuration) { _config = c } -// ResolveRemoteToken populates the derived Token field after the Panel has sent -// us new token values. Because the resolved token is what everything else in -// Wings authenticates against, this has to be called whenever the underlying -// AuthenticationToken values change, otherwise the previously resolved token -// stays in use until the process is restarted. -func (c *Configuration) ResolveRemoteToken() error { - return c.resolveToken(false) -} - -// resolveToken resolves the token to use, preferring values pinned through the -// environment so that a token supplied by the system running Wings is never -// replaced by one sent to us by the Panel. expandLocal controls whether the -// values held in the configuration itself are trusted enough to be passed through -// Expand. -func (c *Configuration) resolveToken(expandLocal bool) error { +// ResolveToken populates the derived Token field, preferring values pinned +// through the environment over those in the configuration itself. +// +// Set remote when the values came from the Panel. Local values may use +// "file://" or "$VAR" indirection; expanding one sent over the network would +// leak files and environment variables back out through the token we attach to +// every request. +func (c *Configuration) ResolveToken(remote bool) error { resolve := func(env, local string) (string, error) { if env != "" { return Expand(env) } - if expandLocal { - return Expand(local) + if remote { + return local, nil } - return local, nil + return Expand(local) } var err error @@ -635,7 +628,7 @@ func FromFile(path string) error { return err } - if err := c.resolveToken(true); err != nil { + if err := c.ResolveToken(false); err != nil { return err } diff --git a/router/router_system.go b/router/router_system.go index 2034af55b..0358ca0db 100644 --- a/router/router_system.go +++ b/router/router_system.go @@ -146,7 +146,7 @@ func postUpdateConfiguration(c *gin.Context) { // The token that everything authenticates against is a derived value that is // not part of the payload sent by the Panel, so it has to be re-resolved from // the new token values. - if err := cfg.ResolveRemoteToken(); err != nil { + if err := cfg.ResolveToken(true); err != nil { middleware.CaptureAndAbort(c, err) return }