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:
Josh Siviour
2026-06-04 09:51:10 +10:00
committed by GitHub
parent bbff004b45
commit e06d0c0865
19 changed files with 981 additions and 6 deletions
+4
View File
@@ -95,6 +95,10 @@ services:
- "elbv2.CreateListener"
- "elbv2.DeleteListener"
- "elbv2.DescribeListeners"
- "elbv2.AddListenerCertificates"
- "elbv2.RemoveListenerCertificates"
- "elbv2.DescribeListenerCertificates"
- "elbv2.DescribeSSLPolicies"
publishes:
# → vpcd
- "vpc.create"
+4
View File
@@ -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."},
}
+4
View File
@@ -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"},
)
}
+16
View File
@@ -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)
}
+12
View File
@@ -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)
}
+4
View File
@@ -69,6 +69,10 @@ func TestELBv2ActionsMap_AllActionsRegistered(t *testing.T) {
"DeleteRule",
"DescribeRules",
"SetRulePriorities",
"AddListenerCertificates",
"RemoveListenerCertificates",
"DescribeListenerCertificates",
"DescribeSSLPolicies",
}
for _, action := range expectedActions {
+5
View File
@@ -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)
+99
View File
@@ -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)
}
+16 -6
View File
@@ -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},
},
+16
View File
@@ -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)
}
+92
View File
@@ -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")},
}
}
+17
View File
@@ -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"