Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 2 additions & 57 deletions pkg/mattermost/database_external.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package mattermost

import (
"context"
"errors"
"fmt"
"net"
"net/url"
"strings"
"time"

mmv1beta "github.com/mattermost/mattermost-operator/apis/mattermost/v1beta1"
"github.com/mattermost/mattermost-operator/pkg/database"
Expand Down Expand Up @@ -159,23 +156,8 @@ func isAllowedDBCheckScheme(dbType, scheme string) bool {
return schemes[scheme]
}

// metadataIPBlocks contains IP ranges commonly used for cloud metadata services.
var metadataIPBlocks []*net.IPNet

func init() {
for _, cidr := range []string{
"169.254.169.254/32", // AWS, GCP, Azure metadata
"100.100.100.200/32", // Alibaba metadata
"fd00:ec2::254/128", // AWS IPv6 metadata
} {
_, block, _ := net.ParseCIDR(cidr)
metadataIPBlocks = append(metadataIPBlocks, block)
}
}

// validateDBCheckURL validates that a DB connection check URL uses an allowed
// scheme for the given database type and does not target cloud metadata endpoints.
// For hostnames, it resolves DNS and blocks any IP in metadata ranges.
// scheme for the given database type.
func validateDBCheckURL(rawURL, dbType string) error {
rawURL = strings.TrimSpace(rawURL)
if rawURL == "" {
Expand All @@ -192,46 +174,9 @@ func validateDBCheckURL(rawURL, dbType string) error {
return fmt.Errorf("scheme %q is not allowed for database type %q", scheme, dbType)
}

hostname := parsed.Hostname()
if hostname == "" {
if parsed.Hostname() == "" {
return errors.New("URL must contain a hostname")
}

ips, err := hostnameResolver(hostname)
if err != nil {
return fmt.Errorf("failed to resolve hostname %q: %w", hostname, err)
}
for _, ip := range ips {
for _, block := range metadataIPBlocks {
if block.Contains(ip) {
return fmt.Errorf("URL targets a blocked metadata IP range: %s", hostname)
}
}
}

return nil
}

// hostnameResolver is the function used to resolve hostnames to IPs.
// It defaults to defaultResolveHostnameIPs but can be replaced in tests
// to avoid real DNS lookups.
var hostnameResolver = defaultResolveHostnameIPs

// defaultResolveHostnameIPs returns IPs for a hostname. If hostname is a literal IP,
// returns it; otherwise performs DNS lookup with timeout.
func defaultResolveHostnameIPs(hostname string) ([]net.IP, error) {
if ip := net.ParseIP(hostname); ip != nil {
return []net.IP{ip}, nil
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
addrs, err := net.DefaultResolver.LookupIPAddr(ctx, hostname)
if err != nil {
return nil, err
}
ips := make([]net.IP, 0, len(addrs))
for _, a := range addrs {
ips = append(ips, a.IP)
}
return ips, nil
}
68 changes: 0 additions & 68 deletions pkg/mattermost/database_external_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package mattermost

import (
"net"
"strings"
"testing"

mmv1beta "github.com/mattermost/mattermost-operator/apis/mattermost/v1beta1"
Expand All @@ -13,26 +11,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func stubResolver(t *testing.T) {
t.Helper()

original := hostnameResolver
hostnameResolver = func(hostname string) ([]net.IP, error) {
if ip := net.ParseIP(hostname); ip != nil {
return []net.IP{ip}, nil
}

return []net.IP{net.ParseIP("10.0.0.99")}, nil
}

t.Cleanup(func() {
hostnameResolver = original
})
}

func TestNewExternalDBInfo(t *testing.T) {
stubResolver(t)

mattermost := &mmv1beta.Mattermost{
ObjectMeta: metav1.ObjectMeta{Name: "mm-test"},
Spec: mmv1beta.MattermostSpec{
Expand Down Expand Up @@ -106,8 +85,6 @@ func TestNewExternalDBInfo(t *testing.T) {
}

func TestExternalDBConfig_SeparateDatasourceKey(t *testing.T) {
stubResolver(t)

mattermost := &mmv1beta.Mattermost{
ObjectMeta: metav1.ObjectMeta{Name: "mm-test"},
Spec: mmv1beta.MattermostSpec{
Expand Down Expand Up @@ -212,8 +189,6 @@ func TestExternalDBConfig_SeparateDatasourceKey(t *testing.T) {
}

func TestValidateDBCheckURL(t *testing.T) {
stubResolver(t)

t.Run("valid URLs for MySQL", func(t *testing.T) {
validURLs := []string{
"http://my-db:3306",
Expand Down Expand Up @@ -258,43 +233,13 @@ func TestValidateDBCheckURL(t *testing.T) {
}
})

t.Run("blocked metadata IPs", func(t *testing.T) {
metadataURLs := []string{
"http://169.254.169.254/latest/meta-data",
"http://100.100.100.200/metadata",
}
for _, u := range metadataURLs {
err := validateDBCheckURL(u, database.MySQLDatabase)
assert.Error(t, err, "expected blocked: %s", u)
assert.Contains(t, err.Error(), "blocked metadata")
}
})

t.Run("blocked hostname resolving to metadata IP", func(t *testing.T) {
original := hostnameResolver
hostnameResolver = defaultResolveHostnameIPs
t.Cleanup(func() {
hostnameResolver = original
})

// 169.254.169.254.nip.io resolves to AWS metadata IP (requires network)
err := validateDBCheckURL("http://169.254.169.254.nip.io/metadata", database.MySQLDatabase)
require.Error(t, err)
if strings.Contains(err.Error(), "failed to resolve") {
t.Skip("DNS unavailable in test environment; skipping hostname resolution test")
}
assert.Contains(t, err.Error(), "blocked metadata")
})

t.Run("empty and invalid", func(t *testing.T) {
assert.Error(t, validateDBCheckURL("", database.MySQLDatabase))
assert.Error(t, validateDBCheckURL("://no-scheme", database.MySQLDatabase))
})
}

func TestNewExternalDBConfig_InvalidCheckURL(t *testing.T) {
stubResolver(t)

mattermost := &mmv1beta.Mattermost{
ObjectMeta: metav1.ObjectMeta{Name: "mm-test"},
Spec: mmv1beta.MattermostSpec{
Expand All @@ -304,19 +249,6 @@ func TestNewExternalDBConfig_InvalidCheckURL(t *testing.T) {
},
}

t.Run("rejects metadata URL", func(t *testing.T) {
secret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "secret"},
Data: map[string][]byte{
"DB_CONNECTION_STRING": []byte("postgres://my-postgres"),
"DB_CONNECTION_CHECK_URL": []byte("http://169.254.169.254/latest/meta-data"),
},
}
_, err := NewExternalDBConfig(mattermost, secret)
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid DB_CONNECTION_CHECK_URL")
})

t.Run("rejects disallowed scheme", func(t *testing.T) {
secret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "secret"},
Expand Down