Compare commits

...

7 Commits

Author SHA1 Message Date
Tanner Jones 33c73aaa7d
Merge 0f153fe35c into 6d9b57671f 2025-06-11 09:24:48 +02:00
Tanner Jones 0f153fe35c
Merge branch 'feature/insecure-flag' of https://github.com/tannerjones4075/falcoctl into feature/insecure-flag
Signed-off-by: Tanner Jones <tanner@testifysec.com>
2025-06-03 10:26:25 -07:00
Tanner Jones a50792fb2b
removed unused package and improve error messaging 2025-06-03 10:22:37 -07:00
Tanner Jones c1f13b0d55
Update internal/login/basic/basic.go
remove duplicative r.CheckConnection(ctx)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Tanner Jones <78619684+tannerjones4075@users.noreply.github.com>
2025-05-30 09:58:58 -07:00
Tanner Jones d8f439268e
fixed linting error and added .
Signed-off-by: Tanner Jones <tanner@testifysec.com>
2025-04-17 14:51:09 -07:00
Tanner Jones e00a85d0ec
test(registry/auth): add test cases for --insecure flag behavior
Add test cases to verify the behavior of the --insecure flag in registry authentication:
- Test successful HTTP connections when --insecure is used
- Test successful HTTPS connections with self-signed certificates when --insecure is used
- Test failure of HTTPS connections with invalid certificates when --insecure is not used
- Test failure of HTTP connections when --insecure is not used

These tests ensure that the --insecure flag properly handles both:
1. Allowing plain HTTP connections
2. Skipping certificate verification for HTTPS connections

The test suite uses a local test registry with both HTTP and HTTPS (self-signed) endpoints
to verify the authentication behavior in different security contexts.

Signed-off-by: Tanner Jones <tanner@testifysec.com>
2025-04-15 17:06:19 -07:00
Tanner Jones 62c2ee06a3
feat(registry): enhance --insecure flag to support both HTTP and HTTPS connections
The --insecure flag now provides dual functionality for registry authentication:
1. Allows insecure HTTPS connections by skipping certificate verification
2. Enables plain HTTP connections when HTTPS is not available

This change improves compatibility with both:
- Self-signed/invalid HTTPS certificates
- Legacy registries that only support HTTP

Technical changes:
- Updated URL handling in registry authentication
- Modified GetRegistryFromRef to properly handle HTTP/HTTPS schemes
- Enhanced login flow to support fallback to HTTP when HTTPS fails
- Improved error handling for connection attempts

Breaking changes: None

Signed-off-by: Tanner Jones <tanner@testifysec.com>
2025-04-15 17:05:59 -07:00
5 changed files with 141 additions and 21 deletions

View File

@ -26,6 +26,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"golang.org/x/term" "golang.org/x/term"
"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/credentials" "oras.land/oras-go/v2/registry/remote/credentials"
"github.com/falcosecurity/falcoctl/internal/config" "github.com/falcosecurity/falcoctl/internal/config"
@ -41,6 +42,7 @@ type loginOptions struct {
username string username string
password string password string
passwordFromStdin bool passwordFromStdin bool
insecure bool
} }
// NewBasicCmd returns the basic command. // NewBasicCmd returns the basic command.
@ -66,16 +68,21 @@ Example - Login with username and password from stdin:
Example - Login with username and password in an interactive prompt: Example - Login with username and password in an interactive prompt:
falcoctl registry auth basic localhost:5000 falcoctl registry auth basic localhost:5000
Example - Login to an insecure registry:
falcoctl registry auth basic --insecure localhost:5000
`, `,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error { PreRunE: func(cmd *cobra.Command, args []string) error {
_ = viper.BindPFlag("registry.auth.basic.username", cmd.Flags().Lookup("username")) _ = viper.BindPFlag("registry.auth.basic.username", cmd.Flags().Lookup("username"))
_ = viper.BindPFlag("registry.auth.basic.password", cmd.Flags().Lookup("password")) _ = viper.BindPFlag("registry.auth.basic.password", cmd.Flags().Lookup("password"))
_ = viper.BindPFlag("registry.auth.basic.password_stdin", cmd.Flags().Lookup("password-stdin")) _ = viper.BindPFlag("registry.auth.basic.password_stdin", cmd.Flags().Lookup("password-stdin"))
_ = viper.BindPFlag("registry.auth.basic.insecure", cmd.Flags().Lookup("insecure"))
o.username = viper.GetString("registry.auth.basic.username") o.username = viper.GetString("registry.auth.basic.username")
o.password = viper.GetString("registry.auth.basic.password") o.password = viper.GetString("registry.auth.basic.password")
o.passwordFromStdin = viper.GetBool("registry.auth.basic.password_stdin") o.passwordFromStdin = viper.GetBool("registry.auth.basic.password_stdin")
o.insecure = viper.GetBool("registry.auth.basic.insecure")
return nil return nil
}, },
@ -87,6 +94,7 @@ Example - Login with username and password in an interactive prompt:
cmd.Flags().StringVarP(&o.username, "username", "u", "", "registry username") cmd.Flags().StringVarP(&o.username, "username", "u", "", "registry username")
cmd.Flags().StringVarP(&o.password, "password", "p", "", "registry password") cmd.Flags().StringVarP(&o.password, "password", "p", "", "registry password")
cmd.Flags().BoolVar(&o.passwordFromStdin, "password-stdin", false, "read password from stdin") cmd.Flags().BoolVar(&o.passwordFromStdin, "password-stdin", false, "read password from stdin")
cmd.Flags().BoolVar(&o.insecure, "insecure", false, "enables plain HTTP and skips TLS verification")
return cmd return cmd
} }
@ -96,18 +104,26 @@ func (o *loginOptions) RunBasic(ctx context.Context, args []string) error {
var reg string var reg string
logger := o.Printer.Logger logger := o.Printer.Logger
// Remove scheme if present
registryArg := strings.TrimPrefix(strings.TrimPrefix(args[0], "http://"), "https://")
// Allow to have the registry expressed as a ref, but actually extract it. // Allow to have the registry expressed as a ref, but actually extract it.
reg, err := utils.GetRegistryFromRef(args[0]) reg, err := utils.GetRegistryFromRef(registryArg)
if err != nil { if err != nil {
reg = args[0] reg = registryArg
} }
if err := getCredentials(o.Printer, o); err != nil { if err := getCredentials(o.Printer, o); err != nil {
return err return err
} }
// create empty client // create empty client with insecure option if specified
client := authn.NewClient() var client *auth.Client
if o.insecure {
client = authn.NewClient(authn.WithInsecure())
} else {
client = authn.NewClient()
}
// create credential store // create credential store
credentialStore, err := credentials.NewStore(config.RegistryCredentialConfPath(), credentials.StoreOptions{ credentialStore, err := credentials.NewStore(config.RegistryCredentialConfPath(), credentials.StoreOptions{

View File

@ -73,11 +73,15 @@ Example - Login with username and password from stdin:
Example - Login with username and password in an interactive prompt: Example - Login with username and password in an interactive prompt:
falcoctl registry auth basic localhost:5000 falcoctl registry auth basic localhost:5000
Example - Login to an insecure registry:
falcoctl registry auth basic --insecure localhost:5000
Usage: Usage:
falcoctl registry auth basic [hostname] falcoctl registry auth basic [hostname]
Flags: Flags:
-h, --help help for basic -h, --help help for basic
--insecure allow connections to SSL registry without certs
-p, --password string registry password -p, --password string registry password
--password-stdin read password from stdin --password-stdin read password from stdin
-u, --username string registry username -u, --username string registry username
@ -127,8 +131,54 @@ var registryAuthBasicTests = Describe("auth", func() {
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(registryAuthBasicHelp))) Expect(output).Should(gbytes.Say(regexp.QuoteMeta(registryAuthBasicHelp)))
}) })
}) })
Context("failure", func() {
Context("insecure flag", func() {
When("using HTTP with --insecure", func() {
BeforeEach(func() {
args = []string{registryCmd, authCmd, basicCmd, "--insecure", "-u", "username", "-p", "password", "--config", configFile, registry}
})
It("should succeed with plain HTTP", func() {
Expect(err).ShouldNot(HaveOccurred())
Expect(output).Should(gbytes.Say("Login succeeded"))
})
})
When("using HTTPS with self-signed cert and --insecure", func() {
BeforeEach(func() {
// The registry is already configured for HTTPS in the test suite
args = []string{registryCmd, authCmd, basicCmd, "--insecure", "-u", "username", "-p", "password", "--config", configFile, registryBasic}
})
It("should succeed with insecure HTTPS", func() {
Expect(err).ShouldNot(HaveOccurred())
Expect(output).Should(gbytes.Say("Login succeeded"))
})
})
When("using HTTPS without --insecure", func() {
BeforeEach(func() {
args = []string{registryCmd, authCmd, basicCmd, "-u", "username", "-p", "password", "--config", configFile, registryBasic}
})
It("should fail with certificate verification error", func() {
Expect(err).Should(HaveOccurred())
Expect(output).Should(gbytes.Say("certificate"))
})
})
When("using HTTP without --insecure", func() {
BeforeEach(func() {
args = []string{registryCmd, authCmd, basicCmd, "-u", "username", "-p", "password", "--config", configFile, "http://" + registry}
})
It("should fail when trying plain HTTP without insecure flag", func() {
Expect(err).Should(HaveOccurred())
})
})
})
Context("failure", func() {
When("without hostname", func() { When("without hostname", func() {
BeforeEach(func() { BeforeEach(func() {
args = []string{registryCmd, authCmd, basicCmd} args = []string{registryCmd, authCmd, basicCmd}
@ -137,5 +187,4 @@ var registryAuthBasicTests = Describe("auth", func() {
"ERROR accepts 1 arg(s), received 0") "ERROR accepts 1 arg(s), received 0")
}) })
}) })
}) })

View File

@ -18,6 +18,8 @@ package basic
import ( import (
"context" "context"
"fmt" "fmt"
"net/http"
"strings"
"oras.land/oras-go/v2/registry/remote/auth" "oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/credentials" "oras.land/oras-go/v2/registry/remote/credentials"
@ -34,9 +36,42 @@ func Login(ctx context.Context, client *auth.Client, credStore credentials.Store
client.Credential = auth.StaticCredential(reg, cred) client.Credential = auth.StaticCredential(reg, cred)
r, err := registry.NewRegistry(reg, registry.WithClient(client)) // Check if client is configured for insecure connections
transport, ok := client.Client.Transport.(*http.Transport)
insecure := ok && transport.TLSClientConfig != nil && transport.TLSClientConfig.InsecureSkipVerify
// If the registry URL starts with https://, force HTTPS
forceHTTPS := strings.HasPrefix(reg, "https://")
// If the registry URL starts with http://, force HTTP
forceHTTP := strings.HasPrefix(reg, "http://")
// Strip scheme if present
reg = strings.TrimPrefix(strings.TrimPrefix(reg, "http://"), "https://")
// Create registry client with appropriate settings
var r *registry.Registry
var err error
switch {
case forceHTTPS:
// For explicit HTTPS URLs, use HTTPS with insecure setting from client
r, err = registry.NewRegistry(reg, registry.WithClient(client), registry.WithPlainHTTP(false))
case forceHTTP:
// For explicit HTTP URLs, use HTTP if insecure is enabled
if !insecure {
return fmt.Errorf("cannot use plain HTTP for %q without --insecure flag", reg)
}
r, err = registry.NewRegistry(reg, registry.WithClient(client), registry.WithPlainHTTP(true))
default:
// For URLs without scheme, try HTTPS first, then fall back to HTTP if insecure is enabled
r, err = registry.NewRegistry(reg, registry.WithClient(client), registry.WithPlainHTTP(false))
if err != nil && insecure {
// If HTTPS failed and insecure is enabled, try HTTP
r, err = registry.NewRegistry(reg, registry.WithClient(client), registry.WithPlainHTTP(true))
}
}
if err != nil { if err != nil {
return err return fmt.Errorf("unable to connect to registry %q: %w", reg, err)
} }
if err := r.CheckConnection(ctx); err != nil { if err := r.CheckConnection(ctx); err != nil {

View File

@ -22,6 +22,9 @@ import (
// GetRegistryFromRef extracts the registry from a ref string. // GetRegistryFromRef extracts the registry from a ref string.
func GetRegistryFromRef(ref string) (string, error) { func GetRegistryFromRef(ref string) (string, error) {
// Remove scheme if present
ref = strings.TrimPrefix(strings.TrimPrefix(ref, "http://"), "https://")
index := strings.Index(ref, "/") index := strings.Index(ref, "/")
if index <= 0 { if index <= 0 {
return "", fmt.Errorf("cannot extract registry name from ref %q", ref) return "", fmt.Errorf("cannot extract registry name from ref %q", ref)

View File

@ -17,6 +17,7 @@ package authn
import ( import (
"context" "context"
"crypto/tls"
"net" "net"
"net/http" "net/http"
"time" "time"
@ -36,6 +37,7 @@ type Options struct {
CredentialsFuncs []func(context.Context, string) (auth.Credential, error) CredentialsFuncs []func(context.Context, string) (auth.Credential, error)
AutoLoginHandler *AutoLoginHandler AutoLoginHandler *AutoLoginHandler
ClientTokenCache auth.Cache ClientTokenCache auth.Cache
Insecure bool
} }
// NewClient creates a new authenticated client to interact with a remote registry. // NewClient creates a new authenticated client to interact with a remote registry.
@ -48,21 +50,29 @@ func NewClient(options ...func(*Options)) *auth.Client {
o(opt) o(opt)
} }
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
if opt.Insecure {
//nolint:gosec // InsecureSkipVerify is intentionally set to true when --insecure flag is used
transport.TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
}
authClient := auth.Client{ authClient := auth.Client{
Client: &http.Client{ Client: &http.Client{
Transport: &http.Transport{ Transport: transport,
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
// TODO(loresuso, alacuku): tls config.
},
}, },
Cache: opt.ClientTokenCache, Cache: opt.ClientTokenCache,
Credential: func(ctx context.Context, reg string) (auth.Credential, error) { Credential: func(ctx context.Context, reg string) (auth.Credential, error) {
@ -151,3 +161,10 @@ func WithClientTokenCache(cache auth.Cache) func(c *Options) {
c.ClientTokenCache = cache c.ClientTokenCache = cache
} }
} }
// WithInsecure configures the client to skip TLS verification.
func WithInsecure() func(c *Options) {
return func(c *Options) {
c.Insecure = true
}
}