Skip to content

Commit

Permalink
Add support for Ignition v3 Proxy and TLS
Browse files Browse the repository at this point in the history
From Ignition v3.1 there is support in the struct to setup a proxy, and
CA validation. This changeset allows AWSMachines to expose these
features when using Ignition.

Signed-off-by: Vince Prignano <vincepri@redhat.com>
  • Loading branch information
vincepri committed Mar 6, 2024
1 parent 3a00c39 commit 4073507
Show file tree
Hide file tree
Showing 8 changed files with 496 additions and 3 deletions.
2 changes: 2 additions & 0 deletions api/v1beta1/zz_generated.conversion.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 61 additions & 0 deletions api/v1beta2/awsmachine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ type CloudInit struct {
}

// Ignition defines options related to the bootstrapping systems where Ignition is used.
// For more information on Ignition configuration, see https://coreos.github.io/butane/specs/
type Ignition struct {
// Version defines which version of Ignition will be used to generate bootstrap data.
//
Expand Down Expand Up @@ -237,6 +238,66 @@ type Ignition struct {
// +kubebuilder:default="ClusterObjectStore"
// +kubebuilder:validation:Enum:="ClusterObjectStore";"UnencryptedUserData"
StorageType IgnitionStorageTypeOption `json:"storageType,omitempty"`

// Proxy defines proxy settings for Ignition.
// Only valid for Ignition versions 3.1 and above.
// +optional
Proxy *IgnitionProxy `json:"proxy,omitempty"`

// TLS defines TLS settings for Ignition.
// Only valid for Ignition versions 3.1 and above.
// +optional
TLS *IgnitionTLS `json:"tls,omitempty"`
}

// IgnitionCASource defines the source of the certificate authority to use for Ignition.
// +kubebuilder:validation:MaxLength:=65536
type IgnitionCASource string

// IgnitionTLS defines TLS settings for Ignition.
type IgnitionTLS struct {
// CASources defines the list of certificate authorities to use for Ignition.
// The value is the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates.
// Supported schemes are http, https, tftp, s3, arn, gs, and `data` (RFC 2397) URL scheme.
//
// +optional
// +kubebuilder:validation:MaxItems=64
CASources []IgnitionCASource `json:"certificateAuthorities,omitempty"`
}

// IgnitionNoProxy defines the list of domains to not proxy for Ignition.
// +kubebuilder:validation:MaxLength:=2048
type IgnitionNoProxy string

// IgnitionProxy defines proxy settings for Ignition.
type IgnitionProxy struct {
// HTTPProxy is the HTTP proxy to use for Ignition.
// A single URL that specifies the proxy server to use for HTTP and HTTPS requests,
// unless overridden by the HTTPSProxy or NoProxy options.
// +optional
HTTPProxy *string `json:"httpProxy,omitempty"`

// HTTPSProxy is the HTTPS proxy to use for Ignition.
// A single URL that specifies the proxy server to use for HTTPS requests,
// unless overridden by the NoProxy option.
// +optional
HTTPSProxy *string `json:"httpsProxy,omitempty"`

// NoProxy is the list of domains to not proxy for Ignition.
// Specifies a list of strings to hosts that should be excluded from proxying.
//
// Each value is represented by:
// - An IP address prefix (1.2.3.4)
// - An IP address prefix in CIDR notation (1.2.3.4/8)
// - A domain name
// - A domain name matches that name and all subdomains
// - A domain name with a leading . matches subdomains only
// - A special DNS label (*), indicates that no proxying should be done
//
// An IP address prefix and domain name can also include a literal port number (1.2.3.4:80).
// +optional
// +kubebuilder:validation:MaxItems=64
NoProxy []IgnitionNoProxy `json:"noProxy,omitempty"`
}

// AWSMachineStatus defines the observed state of AWSMachine.
Expand Down
126 changes: 124 additions & 2 deletions api/v1beta2/awsmachine_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,17 @@ limitations under the License.
package v1beta2

import (
"encoding/base64"
"fmt"
"net"
"net/url"
"strings"

"github.com/google/go-cmp/cmp"
"github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook"
Expand Down Expand Up @@ -171,17 +178,132 @@ func (r *AWSMachine) ignitionEnabled() bool {

func (r *AWSMachine) validateIgnitionAndCloudInit() field.ErrorList {
var allErrs field.ErrorList
if !r.ignitionEnabled() {
return allErrs
}

// Feature gate is not enabled but ignition is enabled then send a forbidden error.
if !feature.Gates.Enabled(feature.BootstrapFormatIgnition) && r.ignitionEnabled() {
if !feature.Gates.Enabled(feature.BootstrapFormatIgnition) {
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "ignition"),
"can be set only if the BootstrapFormatIgnition feature gate is enabled"))
}

if r.ignitionEnabled() && r.cloudInitConfigured() {
// If ignition is enabled, cloudInit should not be configured.
if r.cloudInitConfigured() {
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "cloudInit"), "cannot be set if spec.ignition is set"))
}

// Proxy and TLS are only valid for Ignition versions >= 3.1.
if r.Spec.Ignition.Version == "2.3" || r.Spec.Ignition.Version == "3.0" {
if r.Spec.Ignition.Proxy != nil {
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "ignition", "proxy"), "cannot be set if spec.ignition.version is 2.3 or 3.0"))
}
if r.Spec.Ignition.TLS != nil {
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "ignition", "tls"), "cannot be set if spec.ignition.version is 2.3 or 3.0"))
}
}

allErrs = append(allErrs, r.validateIgnitionProxy()...)
allErrs = append(allErrs, r.validateIgnitionTLS()...)

return allErrs
}

func (r *AWSMachine) validateIgnitionProxy() field.ErrorList {
var allErrs field.ErrorList

if r.Spec.Ignition.Proxy == nil {
return allErrs
}

// Validate HTTPProxy.
if r.Spec.Ignition.Proxy.HTTPProxy != nil {
// Parse the url to check if it is valid.
_, err := url.Parse(*r.Spec.Ignition.Proxy.HTTPProxy)
if err != nil {
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ignition", "proxy", "httpProxy"), *r.Spec.Ignition.Proxy.HTTPProxy, "invalid URL"))
}
}

// Validate HTTPSProxy.
if r.Spec.Ignition.Proxy.HTTPSProxy != nil {
// Parse the url to check if it is valid.
_, err := url.Parse(*r.Spec.Ignition.Proxy.HTTPSProxy)
if err != nil {
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ignition", "proxy", "httpsProxy"), *r.Spec.Ignition.Proxy.HTTPSProxy, "invalid URL"))
}
}

// Validate NoProxy.
for _, noProxy := range r.Spec.Ignition.Proxy.NoProxy {
noProxy := string(noProxy)
// Validate here that the value `noProxy` is:
// - A domain name
// - A domain name matches that name and all subdomains
// - A domain name with a leading . matches subdomains only

// A special DNS label (*).
if noProxy == "*" {
continue
}
// An IP address prefix (1.2.3.4).
if ip := net.ParseIP(noProxy); ip != nil {
continue
}
// An IP address prefix in CIDR notation (1.2.3.4/8).
if _, _, err := net.ParseCIDR(noProxy); err == nil {
continue
}
// An IP or domain name with a port.
if _, _, err := net.SplitHostPort(noProxy); err == nil {
continue
}
// A domain name.
if noProxy[0] == '.' {
// If it starts with a dot, it should be a domain name.
noProxy = noProxy[1:]
}
// Validate that the value matches DNS 1123.
if errs := validation.IsDNS1123Subdomain(noProxy); len(errs) > 0 {
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ignition", "proxy", "noProxy"), noProxy, fmt.Sprintf("invalid noProxy value, please refer to the field documentation: %s", strings.Join(errs, "; "))))
}
}

return allErrs
}

func (r *AWSMachine) validateIgnitionTLS() field.ErrorList {
var allErrs field.ErrorList

if r.Spec.Ignition.TLS == nil {
return allErrs
}

for _, source := range r.Spec.Ignition.TLS.CASources {
// Validate that source is RFC 2397 data URL.
u, err := url.Parse(string(source))
if err != nil {
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ignition", "tls", "caSources"), source, "invalid URL"))
}

switch u.Scheme {
case "http", "https", "tftp", "s3", "arn", "gs":
// Valid schemes.
case "data":
// Validate that the data URL is base64 encoded.
i := strings.Index(u.Opaque, ",")
if i < 0 {
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ignition", "tls", "caSources"), source, "invalid data URL"))
}
// Validate that the data URL is base64 encoded.
if _, err := base64.StdEncoding.DecodeString(u.Opaque[i+1:]); err != nil {
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ignition", "tls", "caSources"), source, "invalid base64 encoding for data url"))
}
default:
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ignition", "tls", "caSources"), source, "unsupported URL scheme"))
}
}

return allErrs
}

Expand Down
122 changes: 122 additions & 0 deletions api/v1beta2/awsmachine_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ import (
"github.com/aws/aws-sdk-go/aws"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilfeature "k8s.io/component-base/featuregate/testing"
"k8s.io/utils/ptr"

"sigs.k8s.io/cluster-api-provider-aws/v2/feature"
utildefaulting "sigs.k8s.io/cluster-api/util/defaulting"
)

Expand Down Expand Up @@ -248,9 +250,129 @@ func TestAWSMachineCreate(t *testing.T) {
},
wantErr: true,
},
{
name: "ignition proxy and TLS can be from version 3.1",
machine: &AWSMachine{
Spec: AWSMachineSpec{
InstanceType: "test",
Ignition: &Ignition{
Version: "3.1",
Proxy: &IgnitionProxy{
HTTPProxy: ptr.To("http://proxy.example.com:3128"),
},
TLS: &IgnitionTLS{
CASources: []IgnitionCASource{"s3://example.com/ca.pem"},
},
},
},
},
wantErr: false,
},
{
name: "ignition tls with invalid CASources URL",
machine: &AWSMachine{
Spec: AWSMachineSpec{
InstanceType: "test",
Ignition: &Ignition{
Version: "3.1",
TLS: &IgnitionTLS{
CASources: []IgnitionCASource{"data;;"},
},
},
},
},
wantErr: true,
},
{
name: "ignition proxy with valid URLs, and noproxy",
machine: &AWSMachine{
Spec: AWSMachineSpec{
InstanceType: "test",
Ignition: &Ignition{
Version: "3.1",
Proxy: &IgnitionProxy{
HTTPProxy: ptr.To("http://proxy.example.com:3128"),
HTTPSProxy: ptr.To("https://proxy.example.com:3128"),
NoProxy: []IgnitionNoProxy{
"10.0.0.1", // single ip
"example.com", // domain
".example.com", // all subdomains
"example.com:3128", // domain with port
"10.0.0.1:3128", // ip with port
"10.0.0.0/8", // cidr block
"*", // no proxy wildcard
},
},
},
},
},
wantErr: false,
},
{
name: "ignition proxy with invalid HTTPProxy URL",
machine: &AWSMachine{
Spec: AWSMachineSpec{
InstanceType: "test",
Ignition: &Ignition{
Version: "3.1",
Proxy: &IgnitionProxy{
HTTPProxy: ptr.To("*:80"),
},
},
},
},
wantErr: true,
},
{
name: "ignition proxy with invalid HTTPSProxy URL",
machine: &AWSMachine{
Spec: AWSMachineSpec{
InstanceType: "test",
Ignition: &Ignition{
Version: "3.1",
Proxy: &IgnitionProxy{
HTTPSProxy: ptr.To("*:80"),
},
},
},
},
wantErr: true,
},
{
name: "ignition proxy with invalid noproxy URL",
machine: &AWSMachine{
Spec: AWSMachineSpec{
InstanceType: "test",
Ignition: &Ignition{
Version: "3.1",
Proxy: &IgnitionProxy{
NoProxy: []IgnitionNoProxy{"&"},
},
},
},
},
wantErr: true,
},
{
name: "cannot use ignition proxy with version 2.3",
machine: &AWSMachine{
Spec: AWSMachineSpec{
InstanceType: "test",
Ignition: &Ignition{
Version: "2.3.0",
Proxy: &IgnitionProxy{
HTTPProxy: ptr.To("http://proxy.example.com:3128"),
},
},
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.BootstrapFormatIgnition, true)()

machine := tt.machine.DeepCopy()
machine.ObjectMeta = metav1.ObjectMeta{
GenerateName: "machine-",
Expand Down

0 comments on commit 4073507

Please sign in to comment.