mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-20 19:25:52 +00:00
feat(elbv2): HTTPS listener certificates + SSL policies (Phase A) (#283)
* feat(elbv2): HTTPS listener certificates + SSL policies Add Certificates[]/SslPolicy to ListenerRecord. CreateListener and ModifyListener validate secure protocols (HTTPS/TLS) require >=1 certificate with exactly one default, non-secure protocols reject certs/policy, and default+validate the SSL policy. DescribeListeners echoes certificates and SslPolicy. New endpoints: AddListenerCertificates, RemoveListenerCertificates, DescribeListenerCertificates, DescribeSSLPolicies (gateway ops, NATS subjects, daemon handlers, service-interfaces.yaml). No TLS termination yet. * test(elbv2): cover listener-cert gateway ops + handler error paths
This commit is contained in:
@@ -95,6 +95,10 @@ services:
|
||||
- "elbv2.CreateListener"
|
||||
- "elbv2.DeleteListener"
|
||||
- "elbv2.DescribeListeners"
|
||||
- "elbv2.AddListenerCertificates"
|
||||
- "elbv2.RemoveListenerCertificates"
|
||||
- "elbv2.DescribeListenerCertificates"
|
||||
- "elbv2.DescribeSSLPolicies"
|
||||
publishes:
|
||||
# → vpcd
|
||||
- "vpc.create"
|
||||
|
||||
@@ -474,6 +474,8 @@ var (
|
||||
ErrorELBv2TooManyActions = "TooManyActions"
|
||||
ErrorELBv2InvalidRulePriority = "InvalidRulePriority"
|
||||
ErrorELBv2IncompatibleProtocols = "IncompatibleProtocols"
|
||||
ErrorELBv2CertificateNotFound = "CertificateNotFound"
|
||||
ErrorELBv2SSLPolicyNotFound = "SSLPolicyNotFound"
|
||||
)
|
||||
|
||||
// ValidErrorCode returns the error code if it exists in ErrorLookup,
|
||||
@@ -961,4 +963,6 @@ var ErrorLookup = map[string]ErrorMessage{
|
||||
ErrorELBv2TooManyActions: {HTTPCode: 400, Message: "You've reached the limit on the number of actions per rule."},
|
||||
ErrorELBv2InvalidRulePriority: {HTTPCode: 400, Message: "The specified rule priority is not valid. Priority must be between 1 and 50000."},
|
||||
ErrorELBv2IncompatibleProtocols: {HTTPCode: 400, Message: "The listener protocol is incompatible with the target group protocol."},
|
||||
ErrorELBv2CertificateNotFound: {HTTPCode: 400, Message: "One or more specified certificates do not exist."},
|
||||
ErrorELBv2SSLPolicyNotFound: {HTTPCode: 400, Message: "The specified SSL policy does not exist."},
|
||||
}
|
||||
|
||||
@@ -850,6 +850,10 @@ func (d *Daemon) subscribeAll() error {
|
||||
natsSub{"elbv2.DescribeLoadBalancerAttributes", d.handleELBv2DescribeLoadBalancerAttributes, "spinifex-workers"},
|
||||
natsSub{"elbv2.SetSecurityGroups", d.handleELBv2SetSecurityGroups, "spinifex-workers"},
|
||||
natsSub{"elbv2.SetIpAddressType", d.handleELBv2SetIpAddressType, "spinifex-workers"},
|
||||
natsSub{"elbv2.AddListenerCertificates", d.handleELBv2AddListenerCertificates, "spinifex-workers"},
|
||||
natsSub{"elbv2.RemoveListenerCertificates", d.handleELBv2RemoveListenerCertificates, "spinifex-workers"},
|
||||
natsSub{"elbv2.DescribeListenerCertificates", d.handleELBv2DescribeListenerCertificates, "spinifex-workers"},
|
||||
natsSub{"elbv2.DescribeSSLPolicies", d.handleELBv2DescribeSSLPolicies, "spinifex-workers"},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -121,3 +121,19 @@ func (d *Daemon) handleELBv2SetSecurityGroups(msg *nats.Msg) {
|
||||
func (d *Daemon) handleELBv2SetIpAddressType(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.elbv2Service.SetIpAddressType)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleELBv2AddListenerCertificates(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.elbv2Service.AddListenerCertificates)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleELBv2RemoveListenerCertificates(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.elbv2Service.RemoveListenerCertificates)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleELBv2DescribeListenerCertificates(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.elbv2Service.DescribeListenerCertificates)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleELBv2DescribeSSLPolicies(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.elbv2Service.DescribeSSLPolicies)
|
||||
}
|
||||
|
||||
@@ -132,6 +132,18 @@ var elbv2Actions = map[string]ELBv2Handler{
|
||||
"SetIpAddressType": elbv2Handler(func(input *elbv2.SetIpAddressTypeInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_elbv2.SetIpAddressType(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"AddListenerCertificates": elbv2Handler(func(input *elbv2.AddListenerCertificatesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_elbv2.AddListenerCertificates(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"RemoveListenerCertificates": elbv2Handler(func(input *elbv2.RemoveListenerCertificatesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_elbv2.RemoveListenerCertificates(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DescribeListenerCertificates": elbv2Handler(func(input *elbv2.DescribeListenerCertificatesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_elbv2.DescribeListenerCertificates(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DescribeSSLPolicies": elbv2Handler(func(input *elbv2.DescribeSSLPoliciesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_elbv2.DescribeSSLPolicies(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DescribeListenerAttributes": elbv2Handler(func(input *gateway_elbv2.DescribeListenerAttributesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_elbv2.DescribeListenerAttributes(input, accountID)
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package gateway_elbv2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/elbv2"
|
||||
"github.com/mulgadc/spinifex/spinifex/awserrors"
|
||||
handlers_elbv2 "github.com/mulgadc/spinifex/spinifex/handlers/elbv2"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// AddListenerCertificates handles the ELBv2 AddListenerCertificates API call. It
|
||||
// attaches additional SNI certificates to a secure listener.
|
||||
func AddListenerCertificates(input *elbv2.AddListenerCertificatesInput, natsConn *nats.Conn, accountID string) (elbv2.AddListenerCertificatesOutput, error) {
|
||||
var output elbv2.AddListenerCertificatesOutput
|
||||
|
||||
if input == nil || input.ListenerArn == nil || *input.ListenerArn == "" || len(input.Certificates) == 0 {
|
||||
return output, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
svc := handlers_elbv2.NewNATSELBv2Service(natsConn)
|
||||
result, err := svc.AddListenerCertificates(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
return *result, nil
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package gateway_elbv2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/elbv2"
|
||||
"github.com/mulgadc/spinifex/spinifex/awserrors"
|
||||
handlers_elbv2 "github.com/mulgadc/spinifex/spinifex/handlers/elbv2"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// DescribeListenerCertificates handles the ELBv2 DescribeListenerCertificates
|
||||
// API call. It returns the certificates attached to a listener.
|
||||
func DescribeListenerCertificates(input *elbv2.DescribeListenerCertificatesInput, natsConn *nats.Conn, accountID string) (elbv2.DescribeListenerCertificatesOutput, error) {
|
||||
var output elbv2.DescribeListenerCertificatesOutput
|
||||
|
||||
if input == nil || input.ListenerArn == nil || *input.ListenerArn == "" {
|
||||
return output, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
svc := handlers_elbv2.NewNATSELBv2Service(natsConn)
|
||||
result, err := svc.DescribeListenerCertificates(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
return *result, nil
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package gateway_elbv2
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go/service/elbv2"
|
||||
handlers_elbv2 "github.com/mulgadc/spinifex/spinifex/handlers/elbv2"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// DescribeSSLPolicies handles the ELBv2 DescribeSSLPolicies API call. It serves
|
||||
// the fixed catalog of supported security policies.
|
||||
func DescribeSSLPolicies(input *elbv2.DescribeSSLPoliciesInput, natsConn *nats.Conn, accountID string) (elbv2.DescribeSSLPoliciesOutput, error) {
|
||||
var output elbv2.DescribeSSLPoliciesOutput
|
||||
|
||||
svc := handlers_elbv2.NewNATSELBv2Service(natsConn)
|
||||
result, err := svc.DescribeSSLPolicies(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
return *result, nil
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package gateway_elbv2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/elbv2"
|
||||
"github.com/mulgadc/spinifex/spinifex/awserrors"
|
||||
handlers_elbv2 "github.com/mulgadc/spinifex/spinifex/handlers/elbv2"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// RemoveListenerCertificates handles the ELBv2 RemoveListenerCertificates API
|
||||
// call. It detaches the named certificates from a listener.
|
||||
func RemoveListenerCertificates(input *elbv2.RemoveListenerCertificatesInput, natsConn *nats.Conn, accountID string) (elbv2.RemoveListenerCertificatesOutput, error) {
|
||||
var output elbv2.RemoveListenerCertificatesOutput
|
||||
|
||||
if input == nil || input.ListenerArn == nil || *input.ListenerArn == "" || len(input.Certificates) == 0 {
|
||||
return output, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
svc := handlers_elbv2.NewNATSELBv2Service(natsConn)
|
||||
result, err := svc.RemoveListenerCertificates(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
return *result, nil
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package gateway_elbv2
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/elbv2"
|
||||
"github.com/mulgadc/spinifex/spinifex/awserrors"
|
||||
"github.com/mulgadc/spinifex/spinifex/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testListenerArn = "arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/lb/abc/def"
|
||||
const testCertArn = "arn:aws:acm:us-east-1:123456789012:certificate/aaaa-1111"
|
||||
|
||||
// Validation guards (no NATS call reached).
|
||||
|
||||
func TestAddListenerCertificates_NilInput(t *testing.T) {
|
||||
_, err := AddListenerCertificates(nil, nil, "123456789012")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestAddListenerCertificates_MissingArn(t *testing.T) {
|
||||
_, err := AddListenerCertificates(&elbv2.AddListenerCertificatesInput{
|
||||
Certificates: []*elbv2.Certificate{{CertificateArn: aws.String(testCertArn)}},
|
||||
}, nil, "123456789012")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestAddListenerCertificates_MissingCerts(t *testing.T) {
|
||||
_, err := AddListenerCertificates(&elbv2.AddListenerCertificatesInput{
|
||||
ListenerArn: aws.String(testListenerArn),
|
||||
}, nil, "123456789012")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestRemoveListenerCertificates_NilInput(t *testing.T) {
|
||||
_, err := RemoveListenerCertificates(nil, nil, "123456789012")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestRemoveListenerCertificates_MissingArn(t *testing.T) {
|
||||
_, err := RemoveListenerCertificates(&elbv2.RemoveListenerCertificatesInput{
|
||||
Certificates: []*elbv2.Certificate{{CertificateArn: aws.String(testCertArn)}},
|
||||
}, nil, "123456789012")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestRemoveListenerCertificates_MissingCerts(t *testing.T) {
|
||||
_, err := RemoveListenerCertificates(&elbv2.RemoveListenerCertificatesInput{
|
||||
ListenerArn: aws.String(testListenerArn),
|
||||
}, nil, "123456789012")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDescribeListenerCertificates_NilInput(t *testing.T) {
|
||||
_, err := DescribeListenerCertificates(nil, nil, "123456789012")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDescribeListenerCertificates_MissingArn(t *testing.T) {
|
||||
_, err := DescribeListenerCertificates(&elbv2.DescribeListenerCertificatesInput{}, nil, "123456789012")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
// Delegation paths: a valid input with no daemon listening fast-fails on the
|
||||
// NATS request, exercising the svc.X call + error return in each gateway op.
|
||||
|
||||
func TestAddListenerCertificates_DelegatesNoResponder(t *testing.T) {
|
||||
_, nc, _ := testutil.StartTestJetStream(t)
|
||||
_, err := AddListenerCertificates(&elbv2.AddListenerCertificatesInput{
|
||||
ListenerArn: aws.String(testListenerArn),
|
||||
Certificates: []*elbv2.Certificate{{CertificateArn: aws.String(testCertArn)}},
|
||||
}, nc, "123456789012")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestRemoveListenerCertificates_DelegatesNoResponder(t *testing.T) {
|
||||
_, nc, _ := testutil.StartTestJetStream(t)
|
||||
_, err := RemoveListenerCertificates(&elbv2.RemoveListenerCertificatesInput{
|
||||
ListenerArn: aws.String(testListenerArn),
|
||||
Certificates: []*elbv2.Certificate{{CertificateArn: aws.String(testCertArn)}},
|
||||
}, nc, "123456789012")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDescribeListenerCertificates_DelegatesNoResponder(t *testing.T) {
|
||||
_, nc, _ := testutil.StartTestJetStream(t)
|
||||
_, err := DescribeListenerCertificates(&elbv2.DescribeListenerCertificatesInput{
|
||||
ListenerArn: aws.String(testListenerArn),
|
||||
}, nc, "123456789012")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDescribeSSLPolicies_DelegatesNoResponder(t *testing.T) {
|
||||
_, nc, _ := testutil.StartTestJetStream(t)
|
||||
_, err := DescribeSSLPolicies(&elbv2.DescribeSSLPoliciesInput{}, nc, "123456789012")
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -69,6 +69,10 @@ func TestELBv2ActionsMap_AllActionsRegistered(t *testing.T) {
|
||||
"DeleteRule",
|
||||
"DescribeRules",
|
||||
"SetRulePriorities",
|
||||
"AddListenerCertificates",
|
||||
"RemoveListenerCertificates",
|
||||
"DescribeListenerCertificates",
|
||||
"DescribeSSLPolicies",
|
||||
}
|
||||
|
||||
for _, action := range expectedActions {
|
||||
|
||||
@@ -28,6 +28,11 @@ type ELBv2Service interface {
|
||||
ModifyListener(input *elbv2.ModifyListenerInput, accountID string) (*elbv2.ModifyListenerOutput, error)
|
||||
DescribeListeners(input *elbv2.DescribeListenersInput, accountID string) (*elbv2.DescribeListenersOutput, error)
|
||||
|
||||
AddListenerCertificates(input *elbv2.AddListenerCertificatesInput, accountID string) (*elbv2.AddListenerCertificatesOutput, error)
|
||||
RemoveListenerCertificates(input *elbv2.RemoveListenerCertificatesInput, accountID string) (*elbv2.RemoveListenerCertificatesOutput, error)
|
||||
DescribeListenerCertificates(input *elbv2.DescribeListenerCertificatesInput, accountID string) (*elbv2.DescribeListenerCertificatesOutput, error)
|
||||
DescribeSSLPolicies(input *elbv2.DescribeSSLPoliciesInput, accountID string) (*elbv2.DescribeSSLPoliciesOutput, error)
|
||||
|
||||
CreateRule(input *elbv2.CreateRuleInput, accountID string) (*elbv2.CreateRuleOutput, error)
|
||||
ModifyRule(input *elbv2.ModifyRuleInput, accountID string) (*elbv2.ModifyRuleOutput, error)
|
||||
DeleteRule(input *elbv2.DeleteRuleInput, accountID string) (*elbv2.DeleteRuleOutput, error)
|
||||
|
||||
@@ -1772,6 +1772,60 @@ func validateRedirectAction(rd *RedirectAction) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildListenerCertificates validates and normalises the certificate set and
|
||||
// SSL policy for a listener of the given protocol. Secure protocols (HTTPS,
|
||||
// TLS) require at least one certificate and receive a default SslPolicy when
|
||||
// unset; non-secure protocols must carry neither certificates nor an SslPolicy.
|
||||
// Exactly one certificate is marked default.
|
||||
func buildListenerCertificates(protocol string, in []*elbv2.Certificate, sslPolicy *string) ([]ListenerCertificate, string, error) {
|
||||
policy := ""
|
||||
if sslPolicy != nil {
|
||||
policy = *sslPolicy
|
||||
}
|
||||
|
||||
if !protocolRequiresCert(protocol) {
|
||||
if len(in) > 0 || policy != "" {
|
||||
return nil, "", errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
if len(in) == 0 {
|
||||
return nil, "", errors.New(awserrors.ErrorELBv2CertificateNotFound)
|
||||
}
|
||||
|
||||
certs := make([]ListenerCertificate, 0, len(in))
|
||||
defaults := 0
|
||||
for _, c := range in {
|
||||
if c == nil || c.CertificateArn == nil || *c.CertificateArn == "" {
|
||||
return nil, "", errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
isDefault := c.IsDefault != nil && *c.IsDefault
|
||||
if isDefault {
|
||||
defaults++
|
||||
}
|
||||
certs = append(certs, ListenerCertificate{
|
||||
CertificateArn: *c.CertificateArn,
|
||||
IsDefault: isDefault,
|
||||
})
|
||||
}
|
||||
switch defaults {
|
||||
case 0:
|
||||
certs[0].IsDefault = true
|
||||
case 1:
|
||||
default:
|
||||
return nil, "", errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
if policy == "" {
|
||||
policy = DefaultSslPolicy
|
||||
} else if !isKnownSslPolicy(policy) {
|
||||
return nil, "", errors.New(awserrors.ErrorELBv2SSLPolicyNotFound)
|
||||
}
|
||||
|
||||
return certs, policy, nil
|
||||
}
|
||||
|
||||
func (s *ELBv2ServiceImpl) CreateListener(input *elbv2.CreateListenerInput, accountID string) (*elbv2.CreateListenerOutput, error) {
|
||||
if input.LoadBalancerArn == nil || *input.LoadBalancerArn == "" {
|
||||
return nil, errors.New(awserrors.ErrorMissingParameter)
|
||||
@@ -1846,6 +1900,11 @@ func (s *ELBv2ServiceImpl) CreateListener(input *elbv2.CreateListenerInput, acco
|
||||
}
|
||||
}
|
||||
|
||||
certs, sslPolicy, err := buildListenerCertificates(protocol, input.Certificates, input.SslPolicy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
listenerID := utils.GenerateResourceID("lst")
|
||||
listenerArn := buildListenerArn(s.region, accountID, lb.Name, lb.LoadBalancerID, listenerID, lb.Type)
|
||||
|
||||
@@ -1874,6 +1933,8 @@ func (s *ELBv2ServiceImpl) CreateListener(input *elbv2.CreateListenerInput, acco
|
||||
DefaultActions: actions,
|
||||
AccountID: accountID,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
Certificates: certs,
|
||||
SslPolicy: sslPolicy,
|
||||
Tags: tags,
|
||||
}
|
||||
|
||||
@@ -2056,6 +2117,37 @@ func (s *ELBv2ServiceImpl) ModifyListener(input *elbv2.ModifyListenerInput, acco
|
||||
}
|
||||
}
|
||||
|
||||
// Certificates / SSL policy. The effective protocol after any change drives
|
||||
// the requirement: switching to a non-secure protocol clears cert material;
|
||||
// staying on or switching to a secure protocol validates and defaults it.
|
||||
if !protocolRequiresCert(updated.Protocol) {
|
||||
if len(input.Certificates) > 0 || (input.SslPolicy != nil && *input.SslPolicy != "") {
|
||||
return nil, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
updated.Certificates = nil
|
||||
updated.SslPolicy = ""
|
||||
} else {
|
||||
inCerts := input.Certificates
|
||||
if inCerts == nil {
|
||||
for _, c := range updated.Certificates {
|
||||
inCerts = append(inCerts, &elbv2.Certificate{
|
||||
CertificateArn: aws.String(c.CertificateArn),
|
||||
IsDefault: aws.Bool(c.IsDefault),
|
||||
})
|
||||
}
|
||||
}
|
||||
sslPolicy := input.SslPolicy
|
||||
if sslPolicy == nil && updated.SslPolicy != "" {
|
||||
sslPolicy = aws.String(updated.SslPolicy)
|
||||
}
|
||||
certs, policy, certErr := buildListenerCertificates(updated.Protocol, inCerts, sslPolicy)
|
||||
if certErr != nil {
|
||||
return nil, certErr
|
||||
}
|
||||
updated.Certificates = certs
|
||||
updated.SslPolicy = policy
|
||||
}
|
||||
|
||||
if err := s.store.PutListener(&updated); err != nil {
|
||||
slog.Error("ModifyListener: failed to persist record", "listenerId", updated.ListenerID, "err", err)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
@@ -2325,6 +2417,13 @@ func (s *ELBv2ServiceImpl) listenerRecordToSDK(r *ListenerRecord) *elbv2.Listene
|
||||
listener.DefaultActions = append(listener.DefaultActions, listenerActionToSDK(a))
|
||||
}
|
||||
|
||||
if len(r.Certificates) > 0 {
|
||||
listener.Certificates = listenerCertsToSDK(r.Certificates)
|
||||
}
|
||||
if r.SslPolicy != "" {
|
||||
listener.SslPolicy = aws.String(r.SslPolicy)
|
||||
}
|
||||
|
||||
return listener
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
package handlers_elbv2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/elbv2"
|
||||
"github.com/mulgadc/spinifex/spinifex/awserrors"
|
||||
)
|
||||
|
||||
// listenerCertsToSDK renders stored listener certificates as SDK certificates.
|
||||
func listenerCertsToSDK(certs []ListenerCertificate) []*elbv2.Certificate {
|
||||
out := make([]*elbv2.Certificate, 0, len(certs))
|
||||
for _, c := range certs {
|
||||
out = append(out, &elbv2.Certificate{
|
||||
CertificateArn: aws.String(c.CertificateArn),
|
||||
IsDefault: aws.Bool(c.IsDefault),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// AddListenerCertificates attaches additional (SNI) certificates to a secure
|
||||
// listener. The default certificate is set at create time and is unaffected;
|
||||
// added certificates are non-default. Re-adding an existing certificate is a
|
||||
// no-op.
|
||||
func (s *ELBv2ServiceImpl) AddListenerCertificates(input *elbv2.AddListenerCertificatesInput, accountID string) (*elbv2.AddListenerCertificatesOutput, error) {
|
||||
if input == nil || input.ListenerArn == nil || *input.ListenerArn == "" {
|
||||
return nil, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
if len(input.Certificates) == 0 {
|
||||
return nil, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
listener, err := s.store.GetListenerByArn(*input.ListenerArn)
|
||||
if err != nil {
|
||||
slog.Error("AddListenerCertificates: failed to get listener", "arn", *input.ListenerArn, "err", err)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
if listener == nil || listener.AccountID != accountID {
|
||||
return nil, errors.New(awserrors.ErrorELBv2ListenerNotFound)
|
||||
}
|
||||
if !protocolRequiresCert(listener.Protocol) {
|
||||
return nil, errors.New(awserrors.ErrorELBv2InvalidConfigurationRequest)
|
||||
}
|
||||
|
||||
updated := *listener
|
||||
certs := append([]ListenerCertificate(nil), updated.Certificates...)
|
||||
seen := make(map[string]bool, len(certs))
|
||||
for _, c := range certs {
|
||||
seen[c.CertificateArn] = true
|
||||
}
|
||||
for _, c := range input.Certificates {
|
||||
if c == nil || c.CertificateArn == nil || *c.CertificateArn == "" {
|
||||
return nil, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
if seen[*c.CertificateArn] {
|
||||
continue
|
||||
}
|
||||
certs = append(certs, ListenerCertificate{CertificateArn: *c.CertificateArn, IsDefault: false})
|
||||
seen[*c.CertificateArn] = true
|
||||
}
|
||||
updated.Certificates = certs
|
||||
|
||||
if err := s.store.PutListener(&updated); err != nil {
|
||||
slog.Error("AddListenerCertificates: failed to persist record", "listenerId", updated.ListenerID, "err", err)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
|
||||
return &elbv2.AddListenerCertificatesOutput{
|
||||
Certificates: listenerCertsToSDK(certs),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RemoveListenerCertificates detaches certificates from a listener by ARN. The
|
||||
// default certificate cannot be removed. Removing an absent certificate is a
|
||||
// no-op (idempotent).
|
||||
func (s *ELBv2ServiceImpl) RemoveListenerCertificates(input *elbv2.RemoveListenerCertificatesInput, accountID string) (*elbv2.RemoveListenerCertificatesOutput, error) {
|
||||
if input == nil || input.ListenerArn == nil || *input.ListenerArn == "" {
|
||||
return nil, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
if len(input.Certificates) == 0 {
|
||||
return nil, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
listener, err := s.store.GetListenerByArn(*input.ListenerArn)
|
||||
if err != nil {
|
||||
slog.Error("RemoveListenerCertificates: failed to get listener", "arn", *input.ListenerArn, "err", err)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
if listener == nil || listener.AccountID != accountID {
|
||||
return nil, errors.New(awserrors.ErrorELBv2ListenerNotFound)
|
||||
}
|
||||
|
||||
remove := make(map[string]bool, len(input.Certificates))
|
||||
for _, c := range input.Certificates {
|
||||
if c == nil || c.CertificateArn == nil || *c.CertificateArn == "" {
|
||||
return nil, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
remove[*c.CertificateArn] = true
|
||||
}
|
||||
|
||||
updated := *listener
|
||||
kept := make([]ListenerCertificate, 0, len(updated.Certificates))
|
||||
for _, c := range updated.Certificates {
|
||||
if remove[c.CertificateArn] {
|
||||
if c.IsDefault {
|
||||
// The default certificate is managed via the listener itself.
|
||||
return nil, errors.New(awserrors.ErrorELBv2InvalidConfigurationRequest)
|
||||
}
|
||||
continue
|
||||
}
|
||||
kept = append(kept, c)
|
||||
}
|
||||
updated.Certificates = kept
|
||||
|
||||
if err := s.store.PutListener(&updated); err != nil {
|
||||
slog.Error("RemoveListenerCertificates: failed to persist record", "listenerId", updated.ListenerID, "err", err)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
|
||||
return &elbv2.RemoveListenerCertificatesOutput{}, nil
|
||||
}
|
||||
|
||||
// DescribeListenerCertificates returns the certificates attached to a listener.
|
||||
func (s *ELBv2ServiceImpl) DescribeListenerCertificates(input *elbv2.DescribeListenerCertificatesInput, accountID string) (*elbv2.DescribeListenerCertificatesOutput, error) {
|
||||
if input == nil || input.ListenerArn == nil || *input.ListenerArn == "" {
|
||||
return nil, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
listener, err := s.store.GetListenerByArn(*input.ListenerArn)
|
||||
if err != nil {
|
||||
slog.Error("DescribeListenerCertificates: failed to get listener", "arn", *input.ListenerArn, "err", err)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
if listener == nil || listener.AccountID != accountID {
|
||||
return nil, errors.New(awserrors.ErrorELBv2ListenerNotFound)
|
||||
}
|
||||
|
||||
return &elbv2.DescribeListenerCertificatesOutput{
|
||||
Certificates: listenerCertsToSDK(listener.Certificates),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DescribeSSLPolicies returns the fixed catalog of supported security policies.
|
||||
// An explicit Names filter selects a subset; an unknown name is rejected.
|
||||
func (s *ELBv2ServiceImpl) DescribeSSLPolicies(input *elbv2.DescribeSSLPoliciesInput, _ string) (*elbv2.DescribeSSLPoliciesOutput, error) {
|
||||
var names []string
|
||||
if input != nil && len(input.Names) > 0 {
|
||||
for _, n := range input.Names {
|
||||
if n == nil || *n == "" {
|
||||
return nil, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
if !isKnownSslPolicy(*n) {
|
||||
return nil, errors.New(awserrors.ErrorELBv2SSLPolicyNotFound)
|
||||
}
|
||||
names = append(names, *n)
|
||||
}
|
||||
} else {
|
||||
names = sslPolicyOrder
|
||||
}
|
||||
|
||||
policies := make([]*elbv2.SslPolicy, 0, len(names))
|
||||
for _, n := range names {
|
||||
policies = append(policies, sslPolicyToSDK(sslPolicyCatalog[n]))
|
||||
}
|
||||
|
||||
return &elbv2.DescribeSSLPoliciesOutput{SslPolicies: policies}, nil
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
package handlers_elbv2
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/elbv2"
|
||||
"github.com/mulgadc/spinifex/spinifex/awserrors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testCertArn = "arn:aws:acm:ap-southeast-2:000000000001:certificate/aaaaaaaa-1111"
|
||||
const testCertArn2 = "arn:aws:acm:ap-southeast-2:000000000001:certificate/bbbbbbbb-2222"
|
||||
|
||||
// fixedResponseAction is a valid default action that needs no target group.
|
||||
func fixedResponseAction() []*elbv2.Action {
|
||||
return []*elbv2.Action{{
|
||||
Type: aws.String(ActionTypeFixedResponse),
|
||||
FixedResponseConfig: &elbv2.FixedResponseActionConfig{
|
||||
StatusCode: aws.String("200"),
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func createHTTPSListener(t *testing.T, svc *ELBv2ServiceImpl) string {
|
||||
t.Helper()
|
||||
lbArn := createLBArn(t, svc, "https-lb")
|
||||
out, err := svc.CreateListener(&elbv2.CreateListenerInput{
|
||||
LoadBalancerArn: aws.String(lbArn),
|
||||
Protocol: aws.String(ProtocolHTTPS),
|
||||
Port: aws.Int64(443),
|
||||
Certificates: []*elbv2.Certificate{{CertificateArn: aws.String(testCertArn)}},
|
||||
DefaultActions: fixedResponseAction(),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
return *out.Listeners[0].ListenerArn
|
||||
}
|
||||
|
||||
func TestCreateListener_HTTPSRequiresCert(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
lbArn := createLBArn(t, svc, "https-nocert")
|
||||
|
||||
_, err := svc.CreateListener(&elbv2.CreateListenerInput{
|
||||
LoadBalancerArn: aws.String(lbArn),
|
||||
Protocol: aws.String(ProtocolHTTPS),
|
||||
Port: aws.Int64(443),
|
||||
DefaultActions: fixedResponseAction(),
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorELBv2CertificateNotFound)
|
||||
}
|
||||
|
||||
func TestCreateListener_HTTPSDefaultsSslPolicy(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
lbArn := createLBArn(t, svc, "https-default-policy")
|
||||
|
||||
out, err := svc.CreateListener(&elbv2.CreateListenerInput{
|
||||
LoadBalancerArn: aws.String(lbArn),
|
||||
Protocol: aws.String(ProtocolHTTPS),
|
||||
Port: aws.Int64(443),
|
||||
Certificates: []*elbv2.Certificate{{CertificateArn: aws.String(testCertArn)}},
|
||||
DefaultActions: fixedResponseAction(),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
l := out.Listeners[0]
|
||||
assert.Equal(t, DefaultSslPolicy, aws.StringValue(l.SslPolicy))
|
||||
require.Len(t, l.Certificates, 1)
|
||||
assert.Equal(t, testCertArn, aws.StringValue(l.Certificates[0].CertificateArn))
|
||||
assert.True(t, aws.BoolValue(l.Certificates[0].IsDefault), "the sole cert must be marked default")
|
||||
|
||||
// DescribeListeners round-trips certificates and SslPolicy.
|
||||
desc, err := svc.DescribeListeners(&elbv2.DescribeListenersInput{
|
||||
ListenerArns: []*string{l.ListenerArn},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, desc.Listeners, 1)
|
||||
assert.Equal(t, DefaultSslPolicy, aws.StringValue(desc.Listeners[0].SslPolicy))
|
||||
require.Len(t, desc.Listeners[0].Certificates, 1)
|
||||
}
|
||||
|
||||
func TestCreateListener_HTTPRejectsCert(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
lbArn := createLBArn(t, svc, "http-withcert")
|
||||
|
||||
_, err := svc.CreateListener(&elbv2.CreateListenerInput{
|
||||
LoadBalancerArn: aws.String(lbArn),
|
||||
Protocol: aws.String(ProtocolHTTP),
|
||||
Port: aws.Int64(80),
|
||||
Certificates: []*elbv2.Certificate{{CertificateArn: aws.String(testCertArn)}},
|
||||
DefaultActions: fixedResponseAction(),
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
func TestCreateListener_UnknownSslPolicy(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
lbArn := createLBArn(t, svc, "https-badpolicy")
|
||||
|
||||
_, err := svc.CreateListener(&elbv2.CreateListenerInput{
|
||||
LoadBalancerArn: aws.String(lbArn),
|
||||
Protocol: aws.String(ProtocolHTTPS),
|
||||
Port: aws.Int64(443),
|
||||
Certificates: []*elbv2.Certificate{{CertificateArn: aws.String(testCertArn)}},
|
||||
SslPolicy: aws.String("ELBSecurityPolicy-Does-Not-Exist"),
|
||||
DefaultActions: fixedResponseAction(),
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorELBv2SSLPolicyNotFound)
|
||||
}
|
||||
|
||||
func TestModifyListener_SwitchToHTTPSRequiresCert(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
lbArn := createLBArn(t, svc, "switch-lb")
|
||||
lst, err := svc.CreateListener(&elbv2.CreateListenerInput{
|
||||
LoadBalancerArn: aws.String(lbArn),
|
||||
Protocol: aws.String(ProtocolHTTP),
|
||||
Port: aws.Int64(80),
|
||||
DefaultActions: fixedResponseAction(),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
arn := lst.Listeners[0].ListenerArn
|
||||
|
||||
// HTTP -> HTTPS without a cert is rejected.
|
||||
_, err = svc.ModifyListener(&elbv2.ModifyListenerInput{
|
||||
ListenerArn: arn,
|
||||
Protocol: aws.String(ProtocolHTTPS),
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorELBv2CertificateNotFound)
|
||||
|
||||
// HTTP -> HTTPS with a cert succeeds and defaults the policy.
|
||||
out, err := svc.ModifyListener(&elbv2.ModifyListenerInput{
|
||||
ListenerArn: arn,
|
||||
Protocol: aws.String(ProtocolHTTPS),
|
||||
Certificates: []*elbv2.Certificate{{CertificateArn: aws.String(testCertArn)}},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, DefaultSslPolicy, aws.StringValue(out.Listeners[0].SslPolicy))
|
||||
require.Len(t, out.Listeners[0].Certificates, 1)
|
||||
}
|
||||
|
||||
func TestModifyListener_SwitchAwayClearsCerts(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
arn := createHTTPSListener(t, svc)
|
||||
|
||||
out, err := svc.ModifyListener(&elbv2.ModifyListenerInput{
|
||||
ListenerArn: aws.String(arn),
|
||||
Protocol: aws.String(ProtocolHTTP),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, out.Listeners[0].Certificates, "certs cleared when leaving a secure protocol")
|
||||
assert.Nil(t, out.Listeners[0].SslPolicy)
|
||||
}
|
||||
|
||||
func TestListenerCertificates_AddRemoveDescribe(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
arn := createHTTPSListener(t, svc)
|
||||
|
||||
// Add an SNI cert.
|
||||
addOut, err := svc.AddListenerCertificates(&elbv2.AddListenerCertificatesInput{
|
||||
ListenerArn: aws.String(arn),
|
||||
Certificates: []*elbv2.Certificate{{CertificateArn: aws.String(testCertArn2)}},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, addOut.Certificates, 2)
|
||||
|
||||
// Re-adding is idempotent.
|
||||
addOut, err = svc.AddListenerCertificates(&elbv2.AddListenerCertificatesInput{
|
||||
ListenerArn: aws.String(arn),
|
||||
Certificates: []*elbv2.Certificate{{CertificateArn: aws.String(testCertArn2)}},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, addOut.Certificates, 2)
|
||||
|
||||
// Describe shows both.
|
||||
desc, err := svc.DescribeListenerCertificates(&elbv2.DescribeListenerCertificatesInput{
|
||||
ListenerArn: aws.String(arn),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, desc.Certificates, 2)
|
||||
|
||||
// Removing the default cert is rejected.
|
||||
_, err = svc.RemoveListenerCertificates(&elbv2.RemoveListenerCertificatesInput{
|
||||
ListenerArn: aws.String(arn),
|
||||
Certificates: []*elbv2.Certificate{{CertificateArn: aws.String(testCertArn)}},
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorELBv2InvalidConfigurationRequest)
|
||||
|
||||
// Removing the SNI cert succeeds.
|
||||
_, err = svc.RemoveListenerCertificates(&elbv2.RemoveListenerCertificatesInput{
|
||||
ListenerArn: aws.String(arn),
|
||||
Certificates: []*elbv2.Certificate{{CertificateArn: aws.String(testCertArn2)}},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
desc, err = svc.DescribeListenerCertificates(&elbv2.DescribeListenerCertificatesInput{
|
||||
ListenerArn: aws.String(arn),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, desc.Certificates, 1)
|
||||
assert.Equal(t, testCertArn, aws.StringValue(desc.Certificates[0].CertificateArn))
|
||||
}
|
||||
|
||||
func TestAddListenerCertificates_HTTPRejected(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
lbArn := createLBArn(t, svc, "http-addcert")
|
||||
lst, err := svc.CreateListener(&elbv2.CreateListenerInput{
|
||||
LoadBalancerArn: aws.String(lbArn),
|
||||
Protocol: aws.String(ProtocolHTTP),
|
||||
Port: aws.Int64(80),
|
||||
DefaultActions: fixedResponseAction(),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = svc.AddListenerCertificates(&elbv2.AddListenerCertificatesInput{
|
||||
ListenerArn: lst.Listeners[0].ListenerArn,
|
||||
Certificates: []*elbv2.Certificate{{CertificateArn: aws.String(testCertArn)}},
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorELBv2InvalidConfigurationRequest)
|
||||
}
|
||||
|
||||
func TestListenerCertificates_ValidationAndNotFound(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
arn := createHTTPSListener(t, svc)
|
||||
const otherAccount = "999999999999"
|
||||
badArn := "arn:aws:elasticloadbalancing:us-east-1:000000000001:listener/app/x/y/z"
|
||||
|
||||
// Add: missing arn, empty certs, nil cert entry, cross-account not-found.
|
||||
_, err := svc.AddListenerCertificates(&elbv2.AddListenerCertificatesInput{}, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
|
||||
_, err = svc.AddListenerCertificates(&elbv2.AddListenerCertificatesInput{
|
||||
ListenerArn: aws.String(arn),
|
||||
}, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
|
||||
_, err = svc.AddListenerCertificates(&elbv2.AddListenerCertificatesInput{
|
||||
ListenerArn: aws.String(arn),
|
||||
Certificates: []*elbv2.Certificate{{}},
|
||||
}, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
|
||||
_, err = svc.AddListenerCertificates(&elbv2.AddListenerCertificatesInput{
|
||||
ListenerArn: aws.String(arn),
|
||||
Certificates: []*elbv2.Certificate{{CertificateArn: aws.String(testCertArn2)}},
|
||||
}, otherAccount)
|
||||
assert.EqualError(t, err, awserrors.ErrorELBv2ListenerNotFound)
|
||||
|
||||
// Remove: missing arn, empty certs, nil cert entry, cross-account not-found.
|
||||
_, err = svc.RemoveListenerCertificates(&elbv2.RemoveListenerCertificatesInput{}, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
|
||||
_, err = svc.RemoveListenerCertificates(&elbv2.RemoveListenerCertificatesInput{
|
||||
ListenerArn: aws.String(arn),
|
||||
}, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
|
||||
_, err = svc.RemoveListenerCertificates(&elbv2.RemoveListenerCertificatesInput{
|
||||
ListenerArn: aws.String(arn),
|
||||
Certificates: []*elbv2.Certificate{{}},
|
||||
}, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
|
||||
_, err = svc.RemoveListenerCertificates(&elbv2.RemoveListenerCertificatesInput{
|
||||
ListenerArn: aws.String(badArn),
|
||||
Certificates: []*elbv2.Certificate{{CertificateArn: aws.String(testCertArn)}},
|
||||
}, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorELBv2ListenerNotFound)
|
||||
|
||||
// Describe: missing arn, cross-account not-found.
|
||||
_, err = svc.DescribeListenerCertificates(&elbv2.DescribeListenerCertificatesInput{}, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
|
||||
_, err = svc.DescribeListenerCertificates(&elbv2.DescribeListenerCertificatesInput{
|
||||
ListenerArn: aws.String(arn),
|
||||
}, otherAccount)
|
||||
assert.EqualError(t, err, awserrors.ErrorELBv2ListenerNotFound)
|
||||
}
|
||||
|
||||
func TestDescribeSSLPolicies_EmptyName(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
_, err := svc.DescribeSSLPolicies(&elbv2.DescribeSSLPoliciesInput{
|
||||
Names: []*string{aws.String("")},
|
||||
}, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
func TestDescribeSSLPolicies(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
|
||||
// No filter → full catalog.
|
||||
all, err := svc.DescribeSSLPolicies(&elbv2.DescribeSSLPoliciesInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, all.SslPolicies, len(sslPolicyOrder))
|
||||
assert.Equal(t, DefaultSslPolicy, aws.StringValue(all.SslPolicies[0].Name))
|
||||
require.NotEmpty(t, all.SslPolicies[0].Ciphers)
|
||||
require.NotEmpty(t, all.SslPolicies[0].SslProtocols)
|
||||
|
||||
// Name filter → subset.
|
||||
one, err := svc.DescribeSSLPolicies(&elbv2.DescribeSSLPoliciesInput{
|
||||
Names: []*string{aws.String(DefaultSslPolicy)},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, one.SslPolicies, 1)
|
||||
|
||||
// Unknown name → error.
|
||||
_, err = svc.DescribeSSLPolicies(&elbv2.DescribeSSLPoliciesInput{
|
||||
Names: []*string{aws.String("nope")},
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorELBv2SSLPolicyNotFound)
|
||||
}
|
||||
@@ -1195,8 +1195,9 @@ func TestModifyListener_Protocol(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := svc.ModifyListener(&elbv2.ModifyListenerInput{
|
||||
ListenerArn: lstOut.Listeners[0].ListenerArn,
|
||||
Protocol: aws.String("HTTPS"),
|
||||
ListenerArn: lstOut.Listeners[0].ListenerArn,
|
||||
Protocol: aws.String("HTTPS"),
|
||||
Certificates: []*elbv2.Certificate{{CertificateArn: aws.String(testCertArn)}},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "HTTPS", *out.Listeners[0].Protocol)
|
||||
@@ -1419,10 +1420,14 @@ func TestModifyListener_NLB_AcceptsAllProtocols(t *testing.T) {
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := svc.ModifyListener(&elbv2.ModifyListenerInput{
|
||||
modIn := &elbv2.ModifyListenerInput{
|
||||
ListenerArn: lstOut.Listeners[0].ListenerArn,
|
||||
Protocol: aws.String(tc.listenerProto),
|
||||
}, testAccountID)
|
||||
}
|
||||
if protocolRequiresCert(tc.listenerProto) {
|
||||
modIn.Certificates = []*elbv2.Certificate{{CertificateArn: aws.String(testCertArn)}}
|
||||
}
|
||||
out, err := svc.ModifyListener(modIn, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.listenerProto, *out.Listeners[0].Protocol)
|
||||
})
|
||||
@@ -1604,14 +1609,18 @@ func TestCreateListener_NLB_AllProtocols(t *testing.T) {
|
||||
Port: aws.Int64(8080),
|
||||
}, testAccountID)
|
||||
|
||||
out, err := svc.CreateListener(&elbv2.CreateListenerInput{
|
||||
createIn := &elbv2.CreateListenerInput{
|
||||
LoadBalancerArn: lbOut.LoadBalancers[0].LoadBalancerArn,
|
||||
Protocol: aws.String(proto),
|
||||
Port: aws.Int64(8080),
|
||||
DefaultActions: []*elbv2.Action{
|
||||
{Type: aws.String("forward"), TargetGroupArn: tgOut.TargetGroups[0].TargetGroupArn},
|
||||
},
|
||||
}, testAccountID)
|
||||
}
|
||||
if protocolRequiresCert(proto) {
|
||||
createIn.Certificates = []*elbv2.Certificate{{CertificateArn: aws.String(testCertArn)}}
|
||||
}
|
||||
out, err := svc.CreateListener(createIn, testAccountID)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, proto, *out.Listeners[0].Protocol)
|
||||
@@ -1707,6 +1716,7 @@ func TestCreateListener_NLB_ProtocolCompatibility_TLSToTCP(t *testing.T) {
|
||||
LoadBalancerArn: lbOut.LoadBalancers[0].LoadBalancerArn,
|
||||
Protocol: aws.String("TLS"),
|
||||
Port: aws.Int64(443),
|
||||
Certificates: []*elbv2.Certificate{{CertificateArn: aws.String(testCertArn)}},
|
||||
DefaultActions: []*elbv2.Action{
|
||||
{Type: aws.String("forward"), TargetGroupArn: tgOut.TargetGroups[0].TargetGroupArn},
|
||||
},
|
||||
|
||||
@@ -143,6 +143,22 @@ func (s *NATSELBv2Service) SetIpAddressType(input *elbv2.SetIpAddressTypeInput,
|
||||
return utils.NATSRequest[elbv2.SetIpAddressTypeOutput](s.natsConn, "elbv2.SetIpAddressType", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSELBv2Service) AddListenerCertificates(input *elbv2.AddListenerCertificatesInput, accountID string) (*elbv2.AddListenerCertificatesOutput, error) {
|
||||
return utils.NATSRequest[elbv2.AddListenerCertificatesOutput](s.natsConn, "elbv2.AddListenerCertificates", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSELBv2Service) RemoveListenerCertificates(input *elbv2.RemoveListenerCertificatesInput, accountID string) (*elbv2.RemoveListenerCertificatesOutput, error) {
|
||||
return utils.NATSRequest[elbv2.RemoveListenerCertificatesOutput](s.natsConn, "elbv2.RemoveListenerCertificates", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSELBv2Service) DescribeListenerCertificates(input *elbv2.DescribeListenerCertificatesInput, accountID string) (*elbv2.DescribeListenerCertificatesOutput, error) {
|
||||
return utils.NATSRequest[elbv2.DescribeListenerCertificatesOutput](s.natsConn, "elbv2.DescribeListenerCertificates", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSELBv2Service) DescribeSSLPolicies(input *elbv2.DescribeSSLPoliciesInput, accountID string) (*elbv2.DescribeSSLPoliciesOutput, error) {
|
||||
return utils.NATSRequest[elbv2.DescribeSSLPoliciesOutput](s.natsConn, "elbv2.DescribeSSLPolicies", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSELBv2Service) LBAgentHeartbeat(input *LBAgentHeartbeatInput, accountID string) (*LBAgentHeartbeatOutput, error) {
|
||||
return utils.NATSRequest[LBAgentHeartbeatOutput](s.natsConn, "elbv2.LBAgentHeartbeat", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package handlers_elbv2
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/elbv2"
|
||||
)
|
||||
|
||||
// protocolRequiresCert reports whether a listener protocol terminates TLS and
|
||||
// therefore requires at least one certificate: ALB HTTPS and NLB TLS.
|
||||
func protocolRequiresCert(protocol string) bool {
|
||||
switch protocol {
|
||||
case ProtocolHTTPS, ProtocolTLS:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// sslPolicyCipher is a named cipher with its negotiation priority.
|
||||
type sslPolicyCipher struct {
|
||||
name string
|
||||
priority int64
|
||||
}
|
||||
|
||||
// sslPolicyDef is a static security-policy definition served by
|
||||
// DescribeSSLPolicies. Spinifex does not terminate TLS yet, so these exist to
|
||||
// satisfy listener SslPolicy validation and terraform reads.
|
||||
type sslPolicyDef struct {
|
||||
name string
|
||||
protocols []string
|
||||
ciphers []sslPolicyCipher
|
||||
}
|
||||
|
||||
// sslPolicyCatalog is the fixed set of supported security policies, keyed by
|
||||
// name. Static data — no persistence.
|
||||
var sslPolicyCatalog = map[string]sslPolicyDef{
|
||||
DefaultSslPolicy: {
|
||||
name: DefaultSslPolicy,
|
||||
protocols: []string{"TLSv1.2"},
|
||||
ciphers: []sslPolicyCipher{
|
||||
{"ECDHE-ECDSA-AES128-GCM-SHA256", 1},
|
||||
{"ECDHE-RSA-AES128-GCM-SHA256", 2},
|
||||
{"ECDHE-ECDSA-AES256-GCM-SHA384", 3},
|
||||
{"ECDHE-RSA-AES256-GCM-SHA384", 4},
|
||||
},
|
||||
},
|
||||
"ELBSecurityPolicy-TLS13-1-2-2021-06": {
|
||||
name: "ELBSecurityPolicy-TLS13-1-2-2021-06",
|
||||
protocols: []string{"TLSv1.3", "TLSv1.2"},
|
||||
ciphers: []sslPolicyCipher{
|
||||
{"TLS_AES_128_GCM_SHA256", 1},
|
||||
{"TLS_AES_256_GCM_SHA384", 2},
|
||||
{"TLS_CHACHA20_POLY1305_SHA256", 3},
|
||||
{"ECDHE-ECDSA-AES128-GCM-SHA256", 4},
|
||||
{"ECDHE-RSA-AES128-GCM-SHA256", 5},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// sslPolicyOrder is the deterministic order DescribeSSLPolicies returns the
|
||||
// catalog in.
|
||||
var sslPolicyOrder = []string{
|
||||
DefaultSslPolicy,
|
||||
"ELBSecurityPolicy-TLS13-1-2-2021-06",
|
||||
}
|
||||
|
||||
// isKnownSslPolicy reports whether name is in the catalog.
|
||||
func isKnownSslPolicy(name string) bool {
|
||||
_, ok := sslPolicyCatalog[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
// sslPolicyToSDK renders a catalog definition as an SDK SslPolicy.
|
||||
func sslPolicyToSDK(def sslPolicyDef) *elbv2.SslPolicy {
|
||||
ciphers := make([]*elbv2.Cipher, 0, len(def.ciphers))
|
||||
for _, c := range def.ciphers {
|
||||
ciphers = append(ciphers, &elbv2.Cipher{
|
||||
Name: aws.String(c.name),
|
||||
Priority: aws.Int64(c.priority),
|
||||
})
|
||||
}
|
||||
protocols := make([]*string, 0, len(def.protocols))
|
||||
for _, p := range def.protocols {
|
||||
protocols = append(protocols, aws.String(p))
|
||||
}
|
||||
return &elbv2.SslPolicy{
|
||||
Name: aws.String(def.name),
|
||||
SslProtocols: protocols,
|
||||
Ciphers: ciphers,
|
||||
SupportedLoadBalancerTypes: []*string{aws.String("application")},
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,10 @@ const (
|
||||
ProtocolTLS = "TLS"
|
||||
ProtocolTCPUDP = "TCP_UDP"
|
||||
|
||||
// DefaultSslPolicy is applied to a secure listener when the caller does
|
||||
// not specify an SslPolicy.
|
||||
DefaultSslPolicy = "ELBSecurityPolicy-2016-08"
|
||||
|
||||
// Listener action types
|
||||
ActionTypeForward = "forward"
|
||||
ActionTypeFixedResponse = "fixed-response"
|
||||
@@ -202,9 +206,22 @@ type ListenerRecord struct {
|
||||
AccountID string `json:"account_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
// Certificates holds the listener's TLS certificates for a secure
|
||||
// protocol (ALB HTTPS / NLB TLS). The first IsDefault entry is the
|
||||
// default cert; the rest are additional SNI certs. Empty for non-secure
|
||||
// listeners. SslPolicy is the negotiated security policy name.
|
||||
Certificates []ListenerCertificate `json:"certificates,omitempty"`
|
||||
SslPolicy string `json:"ssl_policy,omitempty"`
|
||||
|
||||
Tags map[string]string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
// ListenerCertificate is a TLS certificate reference attached to a listener.
|
||||
type ListenerCertificate struct {
|
||||
CertificateArn string `json:"certificate_arn"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
// ListenerAction defines a listener default action or a rule action.
|
||||
type ListenerAction struct {
|
||||
Type string `json:"type"` // "forward", "redirect", or "fixed-response"
|
||||
|
||||
Reference in New Issue
Block a user