mirror of
https://github.com/fosrl/newt.git
synced 2026-03-27 13:06:38 +00:00
Provisioning key working
This commit is contained in:
13
main.go
13
main.go
@@ -159,6 +159,9 @@ var (
|
|||||||
|
|
||||||
// Legacy PKCS12 support (deprecated)
|
// Legacy PKCS12 support (deprecated)
|
||||||
tlsPrivateKey string
|
tlsPrivateKey string
|
||||||
|
|
||||||
|
// Provisioning key – exchanged once for a permanent newt ID + secret
|
||||||
|
provisioningKey string
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -264,6 +267,7 @@ func runNewtMain(ctx context.Context) {
|
|||||||
blueprintFile = os.Getenv("BLUEPRINT_FILE")
|
blueprintFile = os.Getenv("BLUEPRINT_FILE")
|
||||||
noCloudEnv := os.Getenv("NO_CLOUD")
|
noCloudEnv := os.Getenv("NO_CLOUD")
|
||||||
noCloud = noCloudEnv == "true"
|
noCloud = noCloudEnv == "true"
|
||||||
|
provisioningKey = os.Getenv("NEWT_PROVISIONING_KEY")
|
||||||
|
|
||||||
if endpoint == "" {
|
if endpoint == "" {
|
||||||
flag.StringVar(&endpoint, "endpoint", "", "Endpoint of your pangolin server")
|
flag.StringVar(&endpoint, "endpoint", "", "Endpoint of your pangolin server")
|
||||||
@@ -312,6 +316,9 @@ func runNewtMain(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
// load the prefer endpoint just as a flag
|
// load the prefer endpoint just as a flag
|
||||||
flag.StringVar(&preferEndpoint, "prefer-endpoint", "", "Prefer this endpoint for the connection (if set, will override the endpoint from the server)")
|
flag.StringVar(&preferEndpoint, "prefer-endpoint", "", "Prefer this endpoint for the connection (if set, will override the endpoint from the server)")
|
||||||
|
if provisioningKey == "" {
|
||||||
|
flag.StringVar(&provisioningKey, "provisioning-key", "", "One-time provisioning key used to obtain a newt ID and secret from the server")
|
||||||
|
}
|
||||||
|
|
||||||
// Add new mTLS flags
|
// Add new mTLS flags
|
||||||
if tlsClientCert == "" {
|
if tlsClientCert == "" {
|
||||||
@@ -590,6 +597,12 @@ func runNewtMain(ctx context.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal("Failed to create client: %v", err)
|
logger.Fatal("Failed to create client: %v", err)
|
||||||
}
|
}
|
||||||
|
// If a provisioning key was supplied via CLI / env and the config file did
|
||||||
|
// not already carry one, inject it now so provisionIfNeeded() can use it.
|
||||||
|
if provisioningKey != "" && client.GetConfig().ProvisioningKey == "" {
|
||||||
|
client.GetConfig().ProvisioningKey = provisioningKey
|
||||||
|
}
|
||||||
|
|
||||||
endpoint = client.GetConfig().Endpoint // Update endpoint from config
|
endpoint = client.GetConfig().Endpoint // Update endpoint from config
|
||||||
id = client.GetConfig().ID // Update ID from config
|
id = client.GetConfig().ID // Update ID from config
|
||||||
// Update site labels for metrics with the resolved ID
|
// Update site labels for metrics with the resolved ID
|
||||||
|
|||||||
@@ -481,6 +481,11 @@ func (c *Client) connectWithRetry() {
|
|||||||
func (c *Client) establishConnection() error {
|
func (c *Client) establishConnection() error {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Exchange provisioning key for permanent credentials if needed.
|
||||||
|
if err := c.provisionIfNeeded(); err != nil {
|
||||||
|
return fmt.Errorf("failed to provision newt credentials: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Get token for authentication
|
// Get token for authentication
|
||||||
token, err := c.getToken()
|
token, err := c.getToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
package websocket
|
package websocket
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fosrl/newt/logger"
|
"github.com/fosrl/newt/logger"
|
||||||
)
|
)
|
||||||
@@ -83,6 +92,10 @@ func (c *Client) loadConfig() error {
|
|||||||
c.config.Endpoint = config.Endpoint
|
c.config.Endpoint = config.Endpoint
|
||||||
c.baseURL = config.Endpoint
|
c.baseURL = config.Endpoint
|
||||||
}
|
}
|
||||||
|
// Always load the provisioning key from the file if not already set
|
||||||
|
if c.config.ProvisioningKey == "" {
|
||||||
|
c.config.ProvisioningKey = config.ProvisioningKey
|
||||||
|
}
|
||||||
|
|
||||||
// Check if CLI args provided values that override file values
|
// Check if CLI args provided values that override file values
|
||||||
if (!fileHadID && originalConfig.ID != "") ||
|
if (!fileHadID && originalConfig.ID != "") ||
|
||||||
@@ -118,3 +131,116 @@ func (c *Client) saveConfig() error {
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// provisionIfNeeded checks whether a provisioning key is present and, if so,
|
||||||
|
// exchanges it for a newt ID and secret by calling the registration endpoint.
|
||||||
|
// On success the config is updated in-place and flagged for saving so that
|
||||||
|
// subsequent runs use the permanent credentials directly.
|
||||||
|
func (c *Client) provisionIfNeeded() error {
|
||||||
|
if c.config.ProvisioningKey == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we already have both credentials there is nothing to provision.
|
||||||
|
if c.config.ID != "" && c.config.Secret != "" {
|
||||||
|
logger.Debug("Credentials already present, skipping provisioning")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Provisioning key found – exchanging for newt credentials...")
|
||||||
|
|
||||||
|
baseURL, err := url.Parse(c.baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse base URL for provisioning: %w", err)
|
||||||
|
}
|
||||||
|
baseEndpoint := strings.TrimRight(baseURL.String(), "/")
|
||||||
|
|
||||||
|
reqBody := map[string]interface{}{
|
||||||
|
"provisioningKey": c.config.ProvisioningKey,
|
||||||
|
}
|
||||||
|
jsonData, err := json.Marshal(reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal provisioning request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
ctx,
|
||||||
|
"POST",
|
||||||
|
baseEndpoint+"/api/v1/auth/newt/register",
|
||||||
|
bytes.NewBuffer(jsonData),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create provisioning request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("X-CSRF-Token", "x-csrf-protection")
|
||||||
|
|
||||||
|
// Mirror the TLS setup used by getToken so mTLS / self-signed CAs work.
|
||||||
|
var tlsCfg *tls.Config
|
||||||
|
if c.tlsConfig.ClientCertFile != "" || c.tlsConfig.ClientKeyFile != "" ||
|
||||||
|
len(c.tlsConfig.CAFiles) > 0 || c.tlsConfig.PKCS12File != "" {
|
||||||
|
tlsCfg, err = c.setupTLS()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to setup TLS for provisioning: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if os.Getenv("SKIP_TLS_VERIFY") == "true" {
|
||||||
|
if tlsCfg == nil {
|
||||||
|
tlsCfg = &tls.Config{}
|
||||||
|
}
|
||||||
|
tlsCfg.InsecureSkipVerify = true
|
||||||
|
logger.Debug("TLS certificate verification disabled for provisioning via SKIP_TLS_VERIFY")
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient := &http.Client{}
|
||||||
|
if tlsCfg != nil {
|
||||||
|
httpClient.Transport = &http.Transport{TLSClientConfig: tlsCfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("provisioning request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
logger.Debug("Provisioning response body: %s", string(body))
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return fmt.Errorf("provisioning endpoint returned status %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var provResp ProvisioningResponse
|
||||||
|
if err := json.Unmarshal(body, &provResp); err != nil {
|
||||||
|
return fmt.Errorf("failed to decode provisioning response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !provResp.Success {
|
||||||
|
return fmt.Errorf("provisioning failed: %s", provResp.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if provResp.Data.NewtID == "" || provResp.Data.Secret == "" {
|
||||||
|
return fmt.Errorf("provisioning response is missing newt ID or secret")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Successfully provisioned – newt ID: %s", provResp.Data.NewtID)
|
||||||
|
|
||||||
|
// Persist the returned credentials and clear the one-time provisioning key
|
||||||
|
// so subsequent runs authenticate normally.
|
||||||
|
c.config.ID = provResp.Data.NewtID
|
||||||
|
c.config.Secret = provResp.Data.Secret
|
||||||
|
c.config.ProvisioningKey = ""
|
||||||
|
c.configNeedsSave = true
|
||||||
|
|
||||||
|
// Save immediately so that if the subsequent connection attempt fails the
|
||||||
|
// provisioning key is already gone from disk and the next retry uses the
|
||||||
|
// permanent credentials instead of trying to provision again.
|
||||||
|
if err := c.saveConfig(); err != nil {
|
||||||
|
logger.Error("Failed to save config after provisioning: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
package websocket
|
package websocket
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Secret string `json:"secret"`
|
Secret string `json:"secret"`
|
||||||
Endpoint string `json:"endpoint"`
|
Endpoint string `json:"endpoint"`
|
||||||
TlsClientCert string `json:"tlsClientCert"`
|
TlsClientCert string `json:"tlsClientCert"`
|
||||||
|
ProvisioningKey string `json:"provisioningKey,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TokenResponse struct {
|
type TokenResponse struct {
|
||||||
@@ -16,6 +17,15 @@ type TokenResponse struct {
|
|||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProvisioningResponse struct {
|
||||||
|
Data struct {
|
||||||
|
NewtID string `json:"newtId"`
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
} `json:"data"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
type WSMessage struct {
|
type WSMessage struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Data interface{} `json:"data"`
|
Data interface{} `json:"data"`
|
||||||
|
|||||||
Reference in New Issue
Block a user