mirror of
https://github.com/versity/versitygw.git
synced 2026-09-22 07:54:14 +00:00
`AssumeRoleWithWebIdentity` always fetched a provider's discovery document from `<provider url>/.well-known/openid-configuration`, so an identity provider that issues tokens naming a public issuer while serving its metadata and keys on a cluster-internal path could not be used: reaching it meant relaxing the endpoint checks for every registered provider. `--oidc-discovery-url` moves that one fetch to an operator-named endpoint, which is how keys can be looked up over an optimized private path while the tokens themselves stay verifiable from the public internet against the issuer alone, as the JWT spec requires. The flag takes `<provider url>=<discovery url>` pairs, can be repeated once per provider, and is also read from `VGW_IAM_OIDC_DISCOVERY_URLS` as a comma-separated list; the Helm chart exposes the same list as `iamServer.oidc.discoveryUrls`. The discovery URL is fetched exactly as written, so it must carry the `/.well-known/openid-configuration` path when the provider serves it there. A malformed pair is rejected at startup rather than at the first assume-role call. Only the fetch moves. The provider URL is still what a token's `iss` claim is matched against, the fetched document's own `issuer` field must still equal it, and the key set still comes from the `jwks_uri` that document publishes. A configured discovery endpoint is named by the operator at startup rather than by a request, so it and the `jwks_uri` it publishes waive the private-address check for that provider's fetch chain only, without `--oidc-allow-private-endpoints` and its far broader effect on every other provider. Transport rules are unchanged: a plaintext discovery URL still requires `--oidc-allow-insecure-transport`. Thumbprint auto-fetch follows the override and pins the discovery endpoint's certificate chain, since that is the host every later fetch is verified against.
720 lines
25 KiB
Go
720 lines
25 KiB
Go
// Copyright 2026 Versity Software
|
|
// This file is 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 embedgw
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
|
|
"github.com/versity/versitygw/debuglogger"
|
|
"github.com/versity/versitygw/iamapi"
|
|
"github.com/versity/versitygw/iamapi/private"
|
|
"github.com/versity/versitygw/iamapi/storage"
|
|
"github.com/versity/versitygw/internal/netutil"
|
|
"github.com/versity/versitygw/webui"
|
|
)
|
|
|
|
const iamTitle = "VersityGW IAM API"
|
|
|
|
// IAMConfig holds all configuration options for running the VersityGW IAM API.
|
|
type IAMConfig struct {
|
|
// RootUserAccess is the access key ID used to authenticate IAM API
|
|
// requests. Required.
|
|
RootUserAccess string
|
|
// RootUserSecret is the secret access key used to authenticate IAM API
|
|
// requests. Required.
|
|
RootUserSecret string
|
|
|
|
// Ports is the list of IAM API listening addresses. Each entry accepts
|
|
// the same formats as Config.Ports: "host:port", ":port", file-backed
|
|
// UNIX socket paths, or Linux abstract namespace sockets prefixed with
|
|
// "@". Required.
|
|
Ports []string
|
|
|
|
// MaxConnections is the maximum number of concurrent TCP connections
|
|
// accepted by the IAM API server.
|
|
MaxConnections int
|
|
// MaxRequests is the maximum number of concurrent in-flight IAM API
|
|
// requests. Should not exceed MaxConnections.
|
|
MaxRequests int
|
|
|
|
// CertFile is the path to the TLS certificate file for the IAM API server.
|
|
// Both CertFile and KeyFile must be provided together to enable TLS.
|
|
CertFile string
|
|
// KeyFile is the path to the TLS private key file for the IAM API server.
|
|
KeyFile string
|
|
|
|
// LogLevel controls the debug logger: LevelSilent (default) prints
|
|
// nothing, LevelDebug prints full request/response details with
|
|
// secrets and tokens masked, and LevelUnsafe prints them unmasked.
|
|
// Never use LevelUnsafe in production.
|
|
LogLevel debuglogger.Level
|
|
// Quiet suppresses per-request summary logging and startup output.
|
|
Quiet bool
|
|
// KeepAlive enables HTTP keep-alive on IAM API connections.
|
|
KeepAlive bool
|
|
|
|
// HealthPath is the URL path for unauthenticated health-check requests
|
|
// (e.g. "/healthz"). The endpoint returns HTTP 200 for GET requests.
|
|
HealthPath string
|
|
|
|
// SocketPerm is the octal file-mode string for file-backed UNIX domain
|
|
// socket permissions. It has no effect on TCP/IP addresses or Linux
|
|
// abstract namespace sockets.
|
|
SocketPerm string
|
|
|
|
// PrivatePorts is the list of listening addresses for the standalone
|
|
// IAM service's private endpoints (derive-signing-key, evaluate-policy, resolve-identity)
|
|
// — see private.PrivateAPI. Each address must be a unix socket, or a TCP
|
|
// address with PrivateCertFile/PrivateKeyFile/PrivateClientCAFile all
|
|
// set (mTLS with mandatory client-certificate verification); anything
|
|
// else fails startup rather than serving these endpoints in the clear.
|
|
// Empty disables the private endpoints entirely.
|
|
PrivatePorts []string
|
|
// PrivateCertFile/PrivateKeyFile are the private listener's own TLS
|
|
// server certificate, distinct from CertFile/KeyFile (the public
|
|
// control-plane listener's certificate) since the two listeners have
|
|
// different security requirements.
|
|
PrivateCertFile string
|
|
PrivateKeyFile string
|
|
// PrivateClientCAFile verifies the S3 gateway's client certificate on
|
|
// the private listener. Required, together with PrivateCertFile/
|
|
// PrivateKeyFile, for any non-unix-socket PrivatePorts address.
|
|
PrivateClientCAFile string
|
|
// PrivateSocketPerm is the octal file-mode string for a file-backed
|
|
// unix-socket PrivatePorts address.
|
|
PrivateSocketPerm string
|
|
|
|
// IAMDir enables local file-backed IAM API storage. Set to the directory
|
|
// path where the IAM API user database is stored.
|
|
IAMDir string
|
|
|
|
// VaultEndpointURL enables Vault-backed IAM API storage.
|
|
VaultEndpointURL string
|
|
// VaultNamespace is the fallback Vault namespace used when the specific
|
|
// auth or secret-storage namespace is not set.
|
|
VaultNamespace string
|
|
// VaultSecretStoragePath is the KV v2 path prefix under which IAM users
|
|
// are stored (defaults to "iam").
|
|
VaultSecretStoragePath string
|
|
// VaultSecretStorageNamespace overrides VaultNamespace for KV operations.
|
|
VaultSecretStorageNamespace string
|
|
// VaultAuthMethod is the AppRole mount path (defaults to "approle").
|
|
VaultAuthMethod string
|
|
// VaultAuthNamespace overrides VaultNamespace for AppRole login.
|
|
VaultAuthNamespace string
|
|
// VaultMountPath is the KV v2 engine mount path (defaults to "kv-v2").
|
|
VaultMountPath string
|
|
// VaultRootToken authenticates with a root token instead of AppRole.
|
|
VaultRootToken string
|
|
// VaultRoleID is the AppRole role ID.
|
|
VaultRoleID string
|
|
// VaultRoleSecret is the AppRole secret ID.
|
|
VaultRoleSecret string
|
|
// VaultServerCert is the PEM-encoded Vault server TLS certificate for
|
|
// verification.
|
|
VaultServerCert string
|
|
// VaultClientCert is the PEM-encoded client TLS certificate presented to
|
|
// Vault.
|
|
VaultClientCert string
|
|
// VaultClientCertKey is the PEM-encoded private key for VaultClientCert.
|
|
VaultClientCertKey string
|
|
|
|
// CORSAllowOrigin is the Access-Control-Allow-Origin value the IAM API
|
|
// returns to browsers, and the switch that enables preflight handling.
|
|
// No browser can reach this API without it, so leaving it empty while
|
|
// WebuiPorts is set logs a warning and falls back to "*".
|
|
CORSAllowOrigin string
|
|
|
|
// The Webui* fields host the WebUI from the IAM service process, for
|
|
// deployments with no S3 gateway behind it. They mirror Config's Webui*
|
|
// fields, except that here the IAM gateway URLs are the auto-detected
|
|
// ones (from Ports) and the S3/admin URLs can only come from a flag.
|
|
//
|
|
// WebuiPorts is the list of listening addresses for the WebUI server.
|
|
// Empty disables the WebUI entirely.
|
|
WebuiPorts []string
|
|
// WebuiCertFile/WebuiKeyFile are the WebUI server's TLS certificate. When
|
|
// both are empty and WebuiNoTLS is not set, the WebUI inherits
|
|
// CertFile/KeyFile.
|
|
WebuiCertFile string
|
|
WebuiKeyFile string
|
|
// WebuiNoTLS forces the WebUI to plain HTTP even when TLS is configured
|
|
// for the IAM API.
|
|
WebuiNoTLS bool
|
|
// WebuiPathPrefix mounts the WebUI under a single-segment path prefix
|
|
// (e.g. "/ui").
|
|
WebuiPathPrefix string
|
|
// WebuiIAMGateways overrides the IAM service URLs auto-detected from
|
|
// Ports, for when the browser reaches the IAM API through a name this
|
|
// process cannot see, such as an ingress hostname.
|
|
WebuiIAMGateways []string
|
|
// WebuiGateways and WebuiAdminGateways are the S3 and admin gateway URLs
|
|
// offered on the login page. Neither is auto-detected here, so leaving
|
|
// both empty produces an IAM-only dashboard.
|
|
WebuiGateways []string
|
|
WebuiAdminGateways []string
|
|
// Region seeds the WebUI's default region selector. IAM's own signing
|
|
// region is fixed, so this only matters when WebuiGateways points the
|
|
// dashboard at an S3 gateway as well.
|
|
Region string
|
|
|
|
// SigHup is an optional channel that signals the IAM API to reload TLS
|
|
// certificates. When nil, this feature is disabled.
|
|
SigHup <-chan struct{}
|
|
|
|
// Version, Build, and BuildTime are displayed in the startup banner.
|
|
// All three are optional.
|
|
Version string
|
|
Build string
|
|
BuildTime string
|
|
|
|
// DisableOIDCThumbprintAutoFetch disables CreateOpenIDConnectProvider's
|
|
// TLS auto-fetch fallback for when ThumbprintList is omitted. When set,
|
|
// an omitted ThumbprintList is rejected instead of the IAM API making an
|
|
// outbound TLS connection to the caller-supplied URL — for restricted
|
|
// or air-gapped deployments.
|
|
DisableOIDCThumbprintAutoFetch bool
|
|
|
|
// OIDCAllowPrivateEndpoints permits OIDC provider URLs that resolve to
|
|
// loopback/private/link-local addresses and that carry an explicit port,
|
|
// both refused by default. Required to use an IdP that exists only on an
|
|
// internal network, such as a SPIFFE/SPIRE OIDC discovery provider on a
|
|
// cluster-internal Service. Transport stays https and fully verified.
|
|
OIDCAllowPrivateEndpoints bool
|
|
|
|
// OIDCAllowInsecureTransport permits plaintext http OIDC provider URLs
|
|
// and drops TLS certificate verification (ThumbprintList pinning
|
|
// included) for https ones, leaving the network path as the only thing
|
|
// authenticating the IdP. For an IdP reachable only over a path that is
|
|
// itself trusted, such as a discovery provider bound to loopback as a
|
|
// sidecar in this process's own pod.
|
|
OIDCAllowInsecureTransport bool
|
|
|
|
// OIDCDiscoveryURLs redirects individual providers' discovery-document
|
|
// fetches, as "<provider url>=<discovery url>" pairs. The discovery URL
|
|
// is fetched exactly as given, path included. Only the fetch moves: the
|
|
// provider Url stays what a token's iss claim and the fetched document's
|
|
// own issuer field must match, so an IdP can hand out tokens naming its
|
|
// public issuer while this process reads its keys over a private,
|
|
// in-cluster path.
|
|
OIDCDiscoveryURLs []string
|
|
}
|
|
|
|
// privateAPIServer is the standalone IAM service's private endpoint set
|
|
// together with everything RunIAMAPI needs to serve and maintain it: the
|
|
// TLS options ServeMultiPort will enforce, and the cert storage backing
|
|
// them so a SIGHUP can swap in a rotated certificate.
|
|
type privateAPIServer struct {
|
|
api *private.PrivateAPI
|
|
tlsOpts netutil.TLSOptions
|
|
certStorage *netutil.CertStorage
|
|
}
|
|
|
|
// newPrivateAPI builds the standalone IAM service's private endpoint set
|
|
// and the TLS options ServeMultiPort will enforce (mTLS, or nothing at all
|
|
// for a unix-socket-only deployment — see netutil.RequireSecureTransport).
|
|
func newPrivateAPI(store storage.Storer, cfg *IAMConfig) (*privateAPIServer, error) {
|
|
allSet := cfg.PrivateCertFile != "" && cfg.PrivateKeyFile != "" && cfg.PrivateClientCAFile != ""
|
|
noneSet := cfg.PrivateCertFile == "" && cfg.PrivateKeyFile == "" && cfg.PrivateClientCAFile == ""
|
|
if !allSet && !noneSet {
|
|
return nil, fmt.Errorf("--private-cert, --private-cert-key, and --private-client-ca must all be set together, or all left empty for a unix-socket-only private listener")
|
|
}
|
|
|
|
var tlsOpts netutil.TLSOptions
|
|
var certStorage *netutil.CertStorage
|
|
if allSet {
|
|
certStorage = netutil.NewCertStorage()
|
|
if err := certStorage.SetCertificate(cfg.PrivateCertFile, cfg.PrivateKeyFile); err != nil {
|
|
return nil, fmt.Errorf("private listener: load certs: %w", err)
|
|
}
|
|
pool, err := netutil.LoadCACertPool(cfg.PrivateClientCAFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("private listener: %w", err)
|
|
}
|
|
tlsOpts = netutil.TLSOptions{
|
|
GetCertificate: certStorage.GetCertificate,
|
|
ClientCAs: pool,
|
|
RequireClientCert: true,
|
|
}
|
|
}
|
|
|
|
var privOpts []private.PrivateAPIOption
|
|
if cfg.PrivateSocketPerm != "" {
|
|
perm, err := strconv.ParseUint(cfg.PrivateSocketPerm, 8, 32)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid PrivateSocketPerm value %q: must be an octal integer (e.g. '0660'): %w", cfg.PrivateSocketPerm, err)
|
|
}
|
|
privOpts = append(privOpts, private.WithPrivateSocketPerm(os.FileMode(perm)))
|
|
}
|
|
if cfg.Quiet {
|
|
privOpts = append(privOpts, private.WithPrivateQuiet())
|
|
}
|
|
if cfg.Version != "" {
|
|
privOpts = append(privOpts, private.WithPrivateServerVersion(cfg.Version))
|
|
}
|
|
|
|
p, err := private.New(store, iamapi.RootCredentials{
|
|
Access: cfg.RootUserAccess,
|
|
Secret: cfg.RootUserSecret,
|
|
}, privOpts...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("init private IAM API: %w", err)
|
|
}
|
|
|
|
return &privateAPIServer{api: p, tlsOpts: tlsOpts, certStorage: certStorage}, nil
|
|
}
|
|
|
|
// iamWebUIGateways resolves the IAM service URLs the WebUI login page offers.
|
|
// This process is the IAM service, so its own listening addresses are the
|
|
// auto-detected answer unless the operator overrode them.
|
|
func iamWebUIGateways(cfg *IAMConfig) ([]string, error) {
|
|
if len(cfg.WebuiIAMGateways) > 0 {
|
|
return validateGatewayURLs(cfg.WebuiIAMGateways, "WebuiIAMGateways")
|
|
}
|
|
|
|
var gateways []string
|
|
for _, p := range cfg.Ports {
|
|
urls, err := buildServiceURLs(p, cfg.CertFile != "")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("webui: build IAM gateway URLs: %w", err)
|
|
}
|
|
gateways = append(gateways, urls...)
|
|
}
|
|
sortGatewayURLs(gateways)
|
|
return gateways, nil
|
|
}
|
|
|
|
// newIAMWebUI builds the WebUI server hosted by the IAM service process. It
|
|
// returns nil when no WebuiPorts are configured.
|
|
func newIAMWebUI(cfg *IAMConfig) (*webui.Server, error) {
|
|
if len(cfg.WebuiPorts) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
if err := validateWebUIPathPrefix("WebuiPathPrefix", cfg.WebuiPathPrefix); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
iamGateways, err := iamWebUIGateways(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
gateways, err := validateGatewayURLs(cfg.WebuiGateways, "WebuiGateways")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
adminGateways, err := validateGatewayURLs(cfg.WebuiAdminGateways, "WebuiAdminGateways")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var webOpts []webui.Option
|
|
if !cfg.WebuiNoTLS {
|
|
webTLSCert, webTLSKey := cfg.WebuiCertFile, cfg.WebuiKeyFile
|
|
if webTLSCert == "" && webTLSKey == "" {
|
|
webTLSCert, webTLSKey = cfg.CertFile, cfg.KeyFile
|
|
}
|
|
if webTLSCert != "" || webTLSKey != "" {
|
|
if webTLSCert == "" {
|
|
return nil, fmt.Errorf("webui TLS key specified without cert file")
|
|
}
|
|
if webTLSKey == "" {
|
|
return nil, fmt.Errorf("webui TLS cert specified without key file")
|
|
}
|
|
cs := netutil.NewCertStorage()
|
|
if err := cs.SetCertificate(webTLSCert, webTLSKey); err != nil {
|
|
return nil, fmt.Errorf("tls: load certs: %v", err)
|
|
}
|
|
webOpts = append(webOpts, webui.WithTLS(cs))
|
|
}
|
|
}
|
|
if cfg.Quiet {
|
|
webOpts = append(webOpts, webui.WithQuiet())
|
|
}
|
|
if cfg.WebuiPathPrefix != "" {
|
|
webOpts = append(webOpts, webui.WithPathPrefix(cfg.WebuiPathPrefix))
|
|
}
|
|
if cfg.SocketPerm != "" {
|
|
perm, err := strconv.ParseUint(cfg.SocketPerm, 8, 32)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid SocketPerm value %q: must be an octal integer (e.g. '0660'): %w", cfg.SocketPerm, err)
|
|
}
|
|
webOpts = append(webOpts, webui.WithSocketPerm(os.FileMode(perm)))
|
|
}
|
|
|
|
return webui.NewServer(&webui.ServerConfig{
|
|
Gateways: gateways,
|
|
AdminGateways: adminGateways,
|
|
IAMGateways: iamGateways,
|
|
Region: cfg.Region,
|
|
}, webOpts...)
|
|
}
|
|
|
|
var iamAPIRunning atomic.Bool
|
|
|
|
// RunIAMAPI starts the VersityGW IAM API with the supplied configuration. It
|
|
// blocks until ctx is cancelled, or an error occurs. The server is gracefully
|
|
// shut down before the function returns.
|
|
//
|
|
// Only one IAM API instance may run per process at a time. Calling RunIAMAPI
|
|
// concurrently or a second time before the first call returns will return an
|
|
// error.
|
|
func RunIAMAPI(ctx context.Context, cfg *IAMConfig) error {
|
|
if cfg == nil {
|
|
return fmt.Errorf("iam config is required")
|
|
}
|
|
if !iamAPIRunning.CompareAndSwap(false, true) {
|
|
return fmt.Errorf("embedgw: RunIAMAPI is already running; only one instance per process is supported")
|
|
}
|
|
defer iamAPIRunning.Store(false)
|
|
|
|
if cfg.MaxConnections < 1 {
|
|
return fmt.Errorf("max-connections must be positive")
|
|
}
|
|
if cfg.MaxRequests < 1 {
|
|
return fmt.Errorf("max-requests must be positive")
|
|
}
|
|
if cfg.MaxRequests > cfg.MaxConnections {
|
|
log.Printf("WARNING: max-requests (%d) exceeds max-connections (%d) which could allow for IAM API to panic before throttling requests",
|
|
cfg.MaxRequests, cfg.MaxConnections)
|
|
}
|
|
if len(cfg.Ports) == 0 {
|
|
return fmt.Errorf("no ports specified")
|
|
}
|
|
if cfg.RootUserAccess == "" {
|
|
return fmt.Errorf("root access key is required for IAM API authentication")
|
|
}
|
|
if cfg.RootUserSecret == "" {
|
|
return fmt.Errorf("root secret key is required for IAM API authentication")
|
|
}
|
|
|
|
store, err := storage.New(storage.Config{
|
|
Dir: cfg.IAMDir,
|
|
Vault: storage.VaultConfig{
|
|
EndpointURL: cfg.VaultEndpointURL,
|
|
Namespace: cfg.VaultNamespace,
|
|
SecretStoragePath: cfg.VaultSecretStoragePath,
|
|
SecretStorageNamespace: cfg.VaultSecretStorageNamespace,
|
|
AuthMethod: cfg.VaultAuthMethod,
|
|
AuthNamespace: cfg.VaultAuthNamespace,
|
|
MountPath: cfg.VaultMountPath,
|
|
RootToken: cfg.VaultRootToken,
|
|
RoleID: cfg.VaultRoleID,
|
|
RoleSecret: cfg.VaultRoleSecret,
|
|
ServerCert: cfg.VaultServerCert,
|
|
ClientCert: cfg.VaultClientCert,
|
|
ClientCertKey: cfg.VaultClientCertKey,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
opts := []iamapi.Option{
|
|
iamapi.WithConcurrencyLimiter(cfg.MaxConnections, cfg.MaxRequests),
|
|
}
|
|
if cfg.HealthPath != "" {
|
|
opts = append(opts, iamapi.WithHealth(cfg.HealthPath))
|
|
}
|
|
if cfg.KeepAlive {
|
|
opts = append(opts, iamapi.WithKeepAlive())
|
|
}
|
|
if cfg.Quiet {
|
|
opts = append(opts, iamapi.WithQuiet())
|
|
}
|
|
if cfg.DisableOIDCThumbprintAutoFetch {
|
|
opts = append(opts, iamapi.WithOIDCThumbprintAutoFetchDisabled())
|
|
}
|
|
if cfg.OIDCAllowPrivateEndpoints {
|
|
opts = append(opts, iamapi.WithOIDCAllowPrivateEndpoints())
|
|
}
|
|
if cfg.OIDCAllowInsecureTransport {
|
|
opts = append(opts, iamapi.WithOIDCAllowInsecureTransport())
|
|
}
|
|
if len(cfg.OIDCDiscoveryURLs) > 0 {
|
|
opts = append(opts, iamapi.WithOIDCDiscoveryURLs(cfg.OIDCDiscoveryURLs))
|
|
}
|
|
corsAllowOrigin := strings.TrimSpace(cfg.CORSAllowOrigin)
|
|
if len(cfg.WebuiPorts) > 0 && corsAllowOrigin == "" {
|
|
// Every WebUI call to this API is cross-origin, so without an allowed
|
|
// origin the dashboard this process serves cannot talk to it at all.
|
|
corsAllowOrigin = "*"
|
|
fmt.Fprintf(os.Stderr, "WARNING: WebuiPorts is set but CORSAllowOrigin is not; defaulting to '*'; consider setting it to the WebUI's own origin\n")
|
|
}
|
|
if corsAllowOrigin != "" {
|
|
opts = append(opts, iamapi.WithCORSAllowOrigin(corsAllowOrigin))
|
|
}
|
|
debuglogger.SetLevel(cfg.LogLevel)
|
|
if cfg.SocketPerm != "" {
|
|
perm, err := strconv.ParseUint(cfg.SocketPerm, 8, 32)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid SocketPerm value %q: must be an octal integer (e.g. '0660'): %w", cfg.SocketPerm, err)
|
|
}
|
|
opts = append(opts, iamapi.WithSocketPerm(os.FileMode(perm)))
|
|
}
|
|
if cfg.CertFile != "" || cfg.KeyFile != "" {
|
|
if cfg.CertFile == "" {
|
|
return fmt.Errorf("TLS key specified without cert file")
|
|
}
|
|
if cfg.KeyFile == "" {
|
|
return fmt.Errorf("TLS cert specified without key file")
|
|
}
|
|
cs := iamapi.NewCertStorage()
|
|
if err := cs.SetCertificate(cfg.CertFile, cfg.KeyFile); err != nil {
|
|
return fmt.Errorf("tls: load certs: %v", err)
|
|
}
|
|
opts = append(opts, iamapi.WithTLS(cs))
|
|
}
|
|
|
|
server, err := iamapi.New(store, iamapi.RootCredentials{
|
|
Access: cfg.RootUserAccess,
|
|
Secret: cfg.RootUserSecret,
|
|
}, opts...)
|
|
if err != nil {
|
|
return fmt.Errorf("init IAM API server: %w", err)
|
|
}
|
|
|
|
var privateAPI *privateAPIServer
|
|
if len(cfg.PrivatePorts) > 0 {
|
|
privateAPI, err = newPrivateAPI(store, cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
webSrv, err := newIAMWebUI(cfg)
|
|
if err != nil {
|
|
return fmt.Errorf("init webui: %w", err)
|
|
}
|
|
|
|
if !cfg.Quiet {
|
|
cfg.printBanner()
|
|
}
|
|
|
|
errCh := make(chan error, 3)
|
|
go func() {
|
|
errCh <- server.ServeMultiPort(cfg.Ports)
|
|
}()
|
|
|
|
if privateAPI != nil {
|
|
go func() {
|
|
errCh <- privateAPI.api.ServeMultiPort(cfg.PrivatePorts, privateAPI.tlsOpts)
|
|
}()
|
|
}
|
|
|
|
if webSrv != nil {
|
|
go func() {
|
|
errCh <- webSrv.ServeMultiPort(cfg.WebuiPorts)
|
|
}()
|
|
}
|
|
|
|
var sigHup <-chan struct{}
|
|
if cfg.SigHup != nil {
|
|
sigHup = cfg.SigHup
|
|
} else {
|
|
sigHup = make(chan struct{})
|
|
}
|
|
|
|
Loop:
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
break Loop
|
|
case err = <-errCh:
|
|
break Loop
|
|
case <-sigHup:
|
|
if cfg.CertFile != "" && cfg.KeyFile != "" && server.CertStorage != nil {
|
|
reloadErr := server.CertStorage.SetCertificate(cfg.CertFile, cfg.KeyFile)
|
|
if reloadErr != nil {
|
|
debuglogger.InternalError(fmt.Errorf("iam api cert reload failed: %w", reloadErr))
|
|
} else {
|
|
fmt.Printf("iam api cert reloaded (cert: %s, key: %s)\n", cfg.CertFile, cfg.KeyFile)
|
|
}
|
|
}
|
|
// the private listener has its own certificate, so it needs
|
|
// its own reload: without this, new gateway-to-IAM TLS
|
|
// connections would keep getting the pre-rotation cert until
|
|
// the IAM service restarts.
|
|
if privateAPI != nil && privateAPI.certStorage != nil {
|
|
reloadErr := privateAPI.certStorage.SetCertificate(cfg.PrivateCertFile, cfg.PrivateKeyFile)
|
|
if reloadErr != nil {
|
|
debuglogger.InternalError(fmt.Errorf("private iam api cert reload failed: %w", reloadErr))
|
|
} else {
|
|
fmt.Printf("private iam api cert reloaded (cert: %s, key: %s)\n", cfg.PrivateCertFile, cfg.PrivateKeyFile)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
saveErr := err
|
|
|
|
if err := server.Shutdown(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "shutdown IAM API server: %v\n", err)
|
|
}
|
|
if privateAPI != nil {
|
|
if err := privateAPI.api.Shutdown(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "shutdown private IAM API server: %v\n", err)
|
|
}
|
|
}
|
|
if webSrv != nil {
|
|
if err := webSrv.Shutdown(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "shutdown webui server: %v\n", err)
|
|
}
|
|
}
|
|
|
|
return saveErr
|
|
}
|
|
|
|
func (cfg IAMConfig) printBanner() {
|
|
if len(cfg.Ports) == 0 {
|
|
fmt.Fprintf(os.Stderr, "No ports specified\n")
|
|
return
|
|
}
|
|
|
|
allInterfaces, allPorts := resolveIAMBannerInterfaces(cfg.Ports)
|
|
if len(allInterfaces) == 0 {
|
|
fmt.Fprintf(os.Stderr, "Failed to resolve any listening addresses\n")
|
|
return
|
|
}
|
|
|
|
versionStr := fmt.Sprintf("Version %v, Build %v", cfg.Version, cfg.Build)
|
|
if cfg.BuildTime != "" {
|
|
versionStr += fmt.Sprintf(", BuildTime %v", cfg.BuildTime)
|
|
}
|
|
|
|
lines := []string{
|
|
centerText(iamTitle),
|
|
centerText(versionStr),
|
|
centerText(formatIAMBannerBoundHost(cfg.Ports, allPorts)),
|
|
centerText(""),
|
|
leftText("IAM API service listening on:"),
|
|
}
|
|
|
|
for _, u := range buildIAMBannerURLs(allInterfaces, cfg.CertFile != "" || cfg.KeyFile != "") {
|
|
lines = append(lines, leftText(" "+u))
|
|
}
|
|
|
|
if len(cfg.PrivatePorts) > 0 {
|
|
privateInterfaces, _ := resolveIAMBannerInterfaces(cfg.PrivatePorts)
|
|
if len(privateInterfaces) > 0 {
|
|
lines = append(lines, centerText(""), leftText("IAM private service listening on:"))
|
|
for _, u := range buildIAMBannerURLs(privateInterfaces, cfg.PrivateCertFile != "" || cfg.PrivateKeyFile != "") {
|
|
lines = append(lines, leftText(" "+u))
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(cfg.WebuiPorts) > 0 {
|
|
webuiInterfaces, _ := resolveIAMBannerInterfaces(cfg.WebuiPorts)
|
|
if len(webuiInterfaces) > 0 {
|
|
webuiTLS := !cfg.WebuiNoTLS &&
|
|
(cfg.WebuiCertFile != "" || cfg.WebuiKeyFile != "" || cfg.CertFile != "" || cfg.KeyFile != "")
|
|
lines = append(lines, centerText(""), leftText("Web dashboard listening on:"))
|
|
for _, u := range buildIAMBannerURLs(webuiInterfaces, webuiTLS) {
|
|
lines = append(lines, leftText(" "+u+cfg.WebuiPathPrefix))
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt.Println("┌" + strings.Repeat("─", columnWidth-2) + "┐")
|
|
for _, line := range lines {
|
|
fmt.Printf("│%-*s│\n", columnWidth-2, line)
|
|
}
|
|
fmt.Println("└" + strings.Repeat("─", columnWidth-2) + "┘")
|
|
}
|
|
|
|
func resolveIAMBannerInterfaces(ports []string) ([]string, []string) {
|
|
var allInterfaces []string
|
|
var allPorts []string
|
|
interfaceMap := make(map[string]bool)
|
|
|
|
for _, portSpec := range ports {
|
|
if netutil.IsUnixSocketPath(portSpec) {
|
|
allPorts = append(allPorts, portSpec)
|
|
if !interfaceMap[portSpec] {
|
|
interfaceMap[portSpec] = true
|
|
allInterfaces = append(allInterfaces, portSpec)
|
|
}
|
|
continue
|
|
}
|
|
|
|
interfaces, err := getMatchingIPs(portSpec)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to match local IP addresses for %s: %v\n", portSpec, err)
|
|
continue
|
|
}
|
|
_, prt, err := net.SplitHostPort(portSpec)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to parse port %s: %v\n", portSpec, err)
|
|
continue
|
|
}
|
|
allPorts = append(allPorts, prt)
|
|
|
|
for _, ip := range interfaces {
|
|
key := net.JoinHostPort(ip, prt)
|
|
if !interfaceMap[key] {
|
|
interfaceMap[key] = true
|
|
allInterfaces = append(allInterfaces, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
return allInterfaces, allPorts
|
|
}
|
|
|
|
func formatIAMBannerBoundHost(ports, allPorts []string) string {
|
|
if len(ports) == 1 {
|
|
if netutil.IsUnixSocketPath(ports[0]) {
|
|
return fmt.Sprintf("(unix socket: %s)", ports[0])
|
|
}
|
|
hst, prt, _ := net.SplitHostPort(ports[0])
|
|
if hst == "" {
|
|
hst = "0.0.0.0"
|
|
}
|
|
return fmt.Sprintf("(bound on host %s and port %s)", hst, prt)
|
|
}
|
|
|
|
return fmt.Sprintf("(bound on ports: %s)", strings.Join(allPorts, ", "))
|
|
}
|
|
|
|
func buildIAMBannerURLs(interfaces []string, tls bool) []string {
|
|
var urls []string
|
|
scheme := "http"
|
|
if tls {
|
|
scheme = "https"
|
|
}
|
|
|
|
for _, addrPort := range interfaces {
|
|
if netutil.IsUnixSocketPath(addrPort) {
|
|
urls = append(urls, "unix:"+addrPort)
|
|
continue
|
|
}
|
|
|
|
ip, prt, err := net.SplitHostPort(addrPort)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
urls = append(urls, fmt.Sprintf("%s://%s", scheme, net.JoinHostPort(ip, prt)))
|
|
}
|
|
|
|
return urls
|
|
}
|