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
1 change: 0 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ CDN_CLASS="default"
ENABLE_DELETION="false"
CF_DEFAULT_ORIGIN_DOMAIN="my.default.origin"
CF_PRICE_CLASS="PriceClass_All"
CF_AWS_WAF="arn:aws:wafv2:us-east-1:123456789012:global/webacl/ExampleWebACL/473e64fd-f30b-4765-81a0-62ad96dd167a"
CF_CUSTOM_SSL_CERT="arn:aws:acm:us-east-1:123456789012:certificate/473e64fd-78bc-4244-bc34-5eba7ad17fd7"
CF_SECURITY_POLICY="TLSv1.2_2021"
CF_ENABLE_LOGGING="true"
Expand Down
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,24 @@ In CloudFront, these would result in the following order:
- `/en-us/foo` -> en-us specific origin
- `/*/foo` -> catch all origin

## WebACL Associations

When multiple ingresses share the same CloudFront distribution, the controller determines which AWS WAF WebACL (if any) to associate based on the following rules:

1. **Priority to Explicit WebACLs:**
- If any ingress in the group specifies a WebACL ARN using the annotation `cdn-origin-controller.gympass.com/cf.web-acl-arn`, that WebACL will be associated with the distribution.
- If multiple ingresses specify different WebACL ARNs, the controller will return a reconciliation error for all of them as it's a conflicting configuration because a distribution can only have a single WebACL.
2. **No Annotation or Empty Value:**
- If no ingress in the group specifies a WebACL ARN, or if the annotation is present but empty, the controller will retain the current WebACL association on the distribution.
- This means the WebACL will not be removed automatically if you remove or clear the annotation from your ingresses.
3. **Manual Removal Required:**
- The controller **will not** remove a WebACL from an existing distribution during reconciliation. If you want to disassociate a WebACL, you must do so manually via the AWS Console or CLI.

**Best Practices:**
- Always specify the same WebACL ARN on all ingresses in a group to avoid ambiguity.
- To change the WebACL, update the annotation on at least one ingress in the group to the new ARN.
- To remove a WebACL from a distribution, remove the annotation from all ingresses and then manually disassociate the WebACL in AWS.

## Function Associations

In order to associate [Cloudfront Functions](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-functions.html) and [Lambda@Edge Functions](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-at-the-edge.html) to your Ingress-based origins, add the `cdn-origin-controller.gympass.com/cf.function-associations` annotation.
Expand Down Expand Up @@ -330,7 +348,6 @@ Use the following environment variables to change the controller's behavior:

| Env var key | Required | Description | Default |
|---------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------|
| CF_AWS_WAF | No | The Web ACL which should be associated with the distributions. Use the ID for WAF v1 and the ARN for WAF v2. | "" |
| CF_CUSTOM_TAGS | No | Comma-separated list of custom tags to be added to distributions. Example: "foo=bar,bar=foo" | "" |
| CF_DEFAULT_ORIGIN_DOMAIN | Yes | Domain of the default origin each distribution must have to route traffic to in case no custom behaviors match the request. | "" |
| CF_DESCRIPTION_TEMPLATE | No | Template of the distribution's description. Currently a single field can be accessed, `{{group}}`, which matches the CDN group under which the distribution was provisioned. | "Serve contents for {{group}} group." |
Expand Down
1 change: 0 additions & 1 deletion internal/cloudfront/distribution.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@ func NewDistributionBuilder(group string, cfg config.Config) DistributionBuilder
customOrigins: make(map[string]Origin),
priceClass: cfg.CloudFrontPriceClass,
group: group,
webACLID: cfg.CloudFrontWAFARN,
cfg: cfg,
}
}
Expand Down
2 changes: 0 additions & 2 deletions internal/cloudfront/distribution_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ func (s *DistributionTestSuite) SetupTest() {
DefaultOriginDomain: "test.default.origin",
CloudFrontDescriptionTemplate: "test description: {{group}}",
CloudFrontPriceClass: "test price class",
CloudFrontWAFARN: "default-web-acl",
CloudFrontDefaultCachingPolicyID: "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
CloudFrontDefaultCacheRequestPolicyID: "216adef6-5c7f-47e4-b989-5492eafa07d3",
CloudFrontDefaultPublicOriginAccessRequestPolicyID: "216adef6-5c7f-47e4-b989-5492eafa07d3",
Expand Down Expand Up @@ -78,7 +77,6 @@ func (s *DistributionTestSuite) TestDistributionBuilder_New() {
s.Equal("test.default.origin", dist.DefaultOrigin.Host)
s.Equal("test description: test group", dist.Description)
s.Equal("test price class", dist.PriceClass)
s.Equal("default-web-acl", dist.WebACLID)
s.Equal("true", dist.Tags["cdn-origin-controller.gympass.com/owned"])
s.Equal("test group", dist.Tags["cdn-origin-controller.gympass.com/cdn.group"])
}
Expand Down
8 changes: 5 additions & 3 deletions internal/cloudfront/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ type DistributionRepository interface {
ARNByGroup(group string) (string, error)
// Create creates the given Distribution on CloudFront. Returns the created dist.
Create(Distribution) (Distribution, error)
// Gets the Distribution configuration by ID.
DistributionConfigByID(id string) (*awscloudfront.GetDistributionConfigOutput, error)
// Sync ensures the given Distribution is correctly configured on CloudFront. Returns synced dist.
Sync(Distribution) (Distribution, error)
// Delete deletes the Distribution at AWS
Expand Down Expand Up @@ -131,7 +133,7 @@ func (r DistRepository) Create(d Distribution) (Distribution, error) {

func (r DistRepository) Sync(d Distribution) (Distribution, error) {
config := newAWSDistributionConfig(d, r.CallerRef, r.Cfg)
output, err := r.distributionConfigByID(d.ID)
output, err := r.DistributionConfigByID(d.ID)
if err != nil {
return Distribution{}, fmt.Errorf("getting distribution config: %v", err)
}
Expand Down Expand Up @@ -176,7 +178,7 @@ func (r DistRepository) Sync(d Distribution) (Distribution, error) {
}

func (r DistRepository) Delete(d Distribution) error {
output, err := r.distributionConfigByID(d.ID)
output, err := r.DistributionConfigByID(d.ID)
if err != nil {
return cdnaws.IgnoreErrorCodef("getting distribution config: %v", err, awscloudfront.ErrCodeNoSuchDistribution)
}
Expand Down Expand Up @@ -251,7 +253,7 @@ func (r DistRepository) distributionTags(d Distribution) *awscloudfront.Tags {
return &awsTags
}

func (r DistRepository) distributionConfigByID(id string) (*awscloudfront.GetDistributionConfigOutput, error) {
func (r DistRepository) DistributionConfigByID(id string) (*awscloudfront.GetDistributionConfigOutput, error) {
input := &awscloudfront.GetDistributionConfigInput{
Id: aws.String(id),
}
Expand Down
1 change: 0 additions & 1 deletion internal/cloudfront/repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ func (s *DistributionRepositoryTestSuite) SetupTest() {
s.cfg = config.Config{
DefaultOriginDomain: "default.origin",
CloudFrontPriceClass: awscloudfront.PriceClassPriceClass100,
CloudFrontWAFARN: "default-web-acl",
}
}

Expand Down
22 changes: 22 additions & 0 deletions internal/cloudfront/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,11 @@ func (s *Service) newDistribution(ingresses []k8s.CDNIngress, group string, shar

if len(shared.WebACLARN) > 0 {
b = b.WithWebACL(shared.WebACLARN)
} else if len(distARN) > 0 {
b, err = s.keepCurrentWebACLConfig(b, distARN)
if err != nil {
return Distribution{}, fmt.Errorf("setting webacl config: %v", err)
}
}

if len(distARN) > 0 {
Expand All @@ -256,6 +261,23 @@ func (s *Service) newDistribution(ingresses []k8s.CDNIngress, group string, shar
return b.Build()
}

// keepCurrentWebACLConfig checks the current WebACL configuration on distribution and updates the desired state accordingly.
func (s *Service) keepCurrentWebACLConfig(b DistributionBuilder, distARN string) (DistributionBuilder, error) {
distibutionID := b.extractID(distARN)

config, err := s.DistRepo.DistributionConfigByID(distibutionID)
if err != nil {
return b, fmt.Errorf("getting distribution config by ID (%s): %v", distibutionID, err)
}

// If there's a WebACLId in the existing distribution config, set it in the builder, otherwise keep the default (no WebACL).
if config.DistributionConfig.WebACLId != nil && len(*config.DistributionConfig.WebACLId) > 0 {
b = b.WithWebACL(*config.DistributionConfig.WebACLId)
}

return b, nil
}

// discoverCert returns the first found ACM Certificate that matches any Alternate Domain Name of the input Ingresses
func (s *Service) discoverCert(ingresses []k8s.CDNIngress) (certificate.Certificate, error) {
var alternateDomains []string
Expand Down
5 changes: 0 additions & 5 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ const (
enableDeletionKey = "enable_deletion"
cfDefaultOriginDomainKey = "cf_default_origin_domain"
cfPriceClassKey = "cf_price_class"
cfWafArnKey = "cf_aws_waf"
cfSecurityPolicyKey = "cf_security_policy"
cfEnableLoggingKey = "cf_enable_logging"
cfS3BucketLogKey = "cf_s3_bucket_log"
Expand All @@ -61,7 +60,6 @@ func initDefaults() {
viper.SetDefault(enableDeletionKey, "false")
viper.SetDefault(cfDefaultOriginDomainKey, "")
viper.SetDefault(cfPriceClassKey, awscloudfront.PriceClassPriceClassAll)
viper.SetDefault(cfWafArnKey, "")
viper.SetDefault(cfSecurityPolicyKey, "")
viper.SetDefault(cfEnableLoggingKey, "false")
viper.SetDefault(cfS3BucketLogKey, "")
Expand Down Expand Up @@ -99,8 +97,6 @@ type Config struct {
// CloudFrontPriceClass determines how many edge locations CloudFront will use for your distribution.
// ref: https://docs.aws.amazon.com/sdk-for-go/api/service/cloudfront/
CloudFrontPriceClass string
// CloudFrontWAFARN the Web ACL ARN.
CloudFrontWAFARN string
// CloudFrontSecurityPolicy the minimum SSL/TLS protocol that CloudFront can use to communicate with viewers.
// ref: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-viewercertificate.html
CloudFrontSecurityPolicy string
Expand Down Expand Up @@ -175,7 +171,6 @@ func Parse() (Config, error) {
DefaultOriginDomain: viper.GetString(cfDefaultOriginDomainKey),
DeletionEnabled: viper.GetBool(enableDeletionKey),
CloudFrontPriceClass: viper.GetString(cfPriceClassKey),
CloudFrontWAFARN: viper.GetString(cfWafArnKey),
CloudFrontSecurityPolicy: viper.GetString(cfSecurityPolicyKey),
CloudFrontEnableLogging: viper.GetBool(cfEnableLoggingKey),
CloudFrontS3BucketLog: viper.GetString(cfS3BucketLogKey),
Expand Down
Loading