fix: aliyun waf3.0 cloud resource cert deploy

This commit is contained in:
Asakura
2025-12-25 22:54:33 +08:00
committed by GitHub
parent af50635751
commit 454bf5eb81
2 changed files with 332 additions and 83 deletions
@@ -125,16 +125,18 @@ func (d *Deployer) deployToWAF3(ctx context.Context, certPEM, privkeyPEM string)
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
certId, ok := upres.ExtendedData["CertIdentifier"].(string)
if !ok || certId == "" {
return fmt.Errorf("invalid certificate identifier in upload response")
}
// 根据接入方式决定部署方式
switch d.config.ServiceType {
case SERVICE_TYPE_CLOUDRESOURCE:
certId := upres.ExtendedData["CertIdentifier"].(string)
if err := d.deployToWAF3WithCloudResource(ctx, certId); err != nil {
return err
}
case SERVICE_TYPE_CNAME:
certId := upres.ExtendedData["CertIdentifier"].(string)
if err := d.deployToWAF3WithCNAME(ctx, certId); err != nil {
return err
}
@@ -147,20 +149,23 @@ func (d *Deployer) deployToWAF3(ctx context.Context, certPEM, privkeyPEM string)
}
func (d *Deployer) deployToWAF3WithCloudResource(ctx context.Context, cloudCertId string) error {
// 参数校验
if d.config.ResourceProduct == "" {
return errors.New("config `resourceProduct` is required")
}
if d.config.ResourceId == "" {
return errors.New("config `resourceId` is required")
}
// 默认端口为 443
if d.config.ResourcePort == 0 {
d.config.ResourcePort = 443
}
// 查询已同步的云产品资产
// DescribeProductInstances
// 查询云产品实例列表,包含实例的基本信息和接入端口等信息。
// REF: https://www.alibabacloud.com/help/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-describeproductinstances
var resourceInstance *aliwaf.DescribeProductInstancesResponseBodyProductInstances
var resourceInstancePort *aliwaf.DescribeProductInstancesResponseBodyProductInstancesResourcePorts
var resourceInstancePort *aliwaf.DescribeProductInstancesResponseBodyProductInstancesAccessPortAndProtocols
describeProductInstancesReq := &aliwaf.DescribeProductInstancesRequest{
ResourceManagerResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId),
RegionId: tea.String(d.config.Region),
@@ -177,7 +182,7 @@ func (d *Deployer) deployToWAF3WithCloudResource(ctx context.Context, cloudCertI
} else {
resourceInstance = describeProductInstancesResp.Body.ProductInstances[0]
resourceInstancePort, _ = lo.Find(resourceInstance.ResourcePorts, func(p *aliwaf.DescribeProductInstancesResponseBodyProductInstancesResourcePorts) bool {
resourceInstancePort, _ = lo.Find(resourceInstance.AccessPortAndProtocols, func(p *aliwaf.DescribeProductInstancesResponseBodyProductInstancesAccessPortAndProtocols) bool {
return tea.Int32Value(p.Port) == d.config.ResourcePort
})
if resourceInstancePort == nil {
@@ -185,89 +190,148 @@ func (d *Deployer) deployToWAF3WithCloudResource(ctx context.Context, cloudCertI
}
}
// 查询云产品实例的证书列表
var resourceInstanceCertificates []*aliwaf.DescribeResourceInstanceCertsResponseBodyCerts = make([]*aliwaf.DescribeResourceInstanceCertsResponseBodyCerts, 0)
describeResourceInstanceCertsPageNumber := 1
describeResourceInstanceCertsPageSize := 10
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// DescribeResourceInstanceCerts
// 查询云产品实例已同步的证书列表。
// 这个接口返回的证书列表里包含了所有已同步的证书信息,包括证书 ID、证书公共名称、过期时间等信息。
// 但是不会区分哪些是默认证书,哪些是扩展证书。
// 而且此接口可能会返回当前 WAF 下所有的证书,而不只是指定的的云实例使用的证书。
// REF: https://www.alibabacloud.com/help/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-describeresourceinstancecerts
var resourceInstanceCertificates = make([]*aliwaf.DescribeResourceInstanceCertsResponseBodyCerts, 0)
describeResourceInstanceCertsReq := &aliwaf.DescribeResourceInstanceCertsRequest{
ResourceManagerResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId),
InstanceId: tea.String(d.config.InstanceId),
ResourceInstanceId: tea.String(d.config.ResourceId),
PageNumber: tea.Int64(int64(describeResourceInstanceCertsPageNumber)),
PageSize: tea.Int64(int64(describeResourceInstanceCertsPageSize)),
}
describeResourceInstanceCertsResp, err := d.sdkClient.DescribeResourceInstanceCertsWithContext(ctx, describeResourceInstanceCertsReq, &dara.RuntimeOptions{})
d.logger.Debug("sdk request 'waf.DescribeResourceInstanceCerts'", slog.Any("request", describeResourceInstanceCertsReq), slog.Any("response", describeResourceInstanceCertsResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'wafw.DescribeResourceInstanceCerts': %w", err)
}
if describeResourceInstanceCertsResp.Body == nil {
break
}
resourceInstanceCertificates = append(resourceInstanceCertificates, describeResourceInstanceCertsResp.Body.Certs...)
if len(describeResourceInstanceCertsResp.Body.Certs) < describeResourceInstanceCertsPageSize {
break
}
describeResourceInstanceCertsPageNumber++
}
// 生成请求参数
modifyCloudResourceReq := &aliwaf.ModifyCloudResourceRequest{
ResourceManagerResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId),
describeResourceInstanceCertsReq := &aliwaf.DescribeResourceInstanceCertsRequest{
RegionId: tea.String(d.config.Region),
Listen: &aliwaf.ModifyCloudResourceRequestListen{
ResourceProduct: resourceInstance.ResourceProduct,
ResourceInstanceId: resourceInstance.ResourceInstanceId,
Protocol: tea.String("https"),
Port: resourceInstancePort.Port,
Certificates: lo.Map(resourceInstancePort.Certificates, func(c *aliwaf.DescribeProductInstancesResponseBodyProductInstancesResourcePortsCertificates, _ int) *aliwaf.ModifyCloudResourceRequestListenCertificates {
return &aliwaf.ModifyCloudResourceRequestListenCertificates{
CertificateId: c.CertificateId,
AppliedType: c.AppliedType,
}
}),
},
ResourceManagerResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId),
InstanceId: tea.String(d.config.InstanceId),
ResourceInstanceId: tea.String(d.config.ResourceId),
}
describeResourceInstanceCertsResp, err := d.sdkClient.DescribeResourceInstanceCertsWithContext(ctx, describeResourceInstanceCertsReq, &dara.RuntimeOptions{})
d.logger.Debug("sdk request 'waf.DescribeResourceInstanceCerts'", slog.Any("request", describeResourceInstanceCertsReq), slog.Any("response", describeResourceInstanceCertsResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'waf.DescribeResourceInstanceCerts': %w", err)
}
if d.config.Domain == "" {
// 未指定扩展域名,只需替换默认证书
const certAppliedTypeDefault = "default"
for _, certItem := range modifyCloudResourceReq.Listen.Certificates {
if tea.StringValue(certItem.AppliedType) == certAppliedTypeDefault &&
tea.StringValue(certItem.CertificateId) == cloudCertId {
return nil
}
}
modifyCloudResourceReq.Listen.Certificates = lo.Filter(modifyCloudResourceReq.Listen.Certificates, func(c *aliwaf.ModifyCloudResourceRequestListenCertificates, _ int) bool {
return tea.StringValue(c.AppliedType) != certAppliedTypeDefault
})
modifyCloudResourceReq.Listen.Certificates = append(modifyCloudResourceReq.Listen.Certificates, &aliwaf.ModifyCloudResourceRequestListenCertificates{
CertificateId: tea.String(cloudCertId),
AppliedType: tea.String(certAppliedTypeDefault),
if describeResourceInstanceCertsResp.Body == nil {
return fmt.Errorf("describe waf %s cloud resource instance certificates response body is empty", d.config.InstanceId)
}
resourceInstanceCertificates = describeResourceInstanceCertsResp.Body.Certs
if len(resourceInstanceCertificates) == 0 {
return fmt.Errorf("describe waf %s cloud resource instance certificates response certs list is empty", d.config.InstanceId)
}
// DescribeCloudResourceAccessPortDetails
// 获取云产品实例的接入端口详情,包含证书列表、实例 ID 等信息。
// REF: https://www.alibabacloud.com/help/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-describecloudresourceaccessportdetails
var cloudResourceAccessPortDetailsCertificates = make([]*aliwaf.DescribeCloudResourceAccessPortDetailsResponseBodyAccessPortDetailsCertificates, 0)
describeCloudResourceAccessPortDetailsRequest := &aliwaf.DescribeCloudResourceAccessPortDetailsRequest{
RegionId: tea.String(d.config.Region),
InstanceId: tea.String(d.config.InstanceId),
ResourceInstanceId: tea.String(d.config.ResourceId),
Port: tea.String(fmt.Sprintf("%d", d.config.ResourcePort)),
}
describeCloudResourceAccessPortDetailsResponse, err := d.sdkClient.DescribeCloudResourceAccessPortDetailsWithContext(ctx, describeCloudResourceAccessPortDetailsRequest, &dara.RuntimeOptions{})
d.logger.Debug("sdk request 'waf.DescribeCloudResourceAccessPortDetails'", slog.Any("request", describeCloudResourceAccessPortDetailsRequest), slog.Any("response", describeCloudResourceAccessPortDetailsResponse))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'waf.DescribeCloudResourceAccessPortDetails': %w", err)
}
if describeCloudResourceAccessPortDetailsResponse.Body.AccessPortDetails == nil {
return fmt.Errorf("could not get access port details of waf '%s' cloud resource '%s %s:%d'", d.config.InstanceId, d.config.ResourceProduct, d.config.ResourceId, d.config.ResourcePort)
}
cloudResourceAccessPortDetailsCertificates = describeCloudResourceAccessPortDetailsResponse.Body.AccessPortDetails[0].Certificates
// 至少应该存在一张默认证书
if len(cloudResourceAccessPortDetailsCertificates) == 0 {
return fmt.Errorf("could not get certificates of waf '%s' cloud resource '%s %s:%d'", d.config.InstanceId, d.config.ResourceProduct, d.config.ResourceId, d.config.ResourcePort)
}
// 获取由 WAF 自动生成的 CloudResourceId,后续更新证书配置时需要用到
cloudResourceId := describeCloudResourceAccessPortDetailsResponse.Body.AccessPortDetails[0].CloudResourceId
if cloudResourceId == nil || tea.StringValue(cloudResourceId) == "" {
return fmt.Errorf("could not get cloud resource id of waf '%s' cloud resource '%s %s:%d'", d.config.InstanceId, d.config.ResourceProduct, d.config.ResourceId, d.config.ResourcePort)
}
// ModifyCloudResourceCert
// 更新证书配置
// REF: https://www.alibabacloud.com/help/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-modifycloudresourcecert
modifyCloudResourceCertReq := &aliwaf.ModifyCloudResourceCertRequest{
RegionId: tea.String(d.config.Region),
InstanceId: tea.String(d.config.InstanceId),
CloudResourceId: cloudResourceId,
}
if d.config.Domain == "" {
// 没有指定扩展域名,只需替换默认证书
const certAppliedTypeDefault = "default"
// 从已有列表里找出默认证书 ID,替换为新的证书 ID,其他的扩展证书保持不变(如果有)
modifyCloudResourceCertReq.Certificates = lo.Map(cloudResourceAccessPortDetailsCertificates, func(c *aliwaf.DescribeCloudResourceAccessPortDetailsResponseBodyAccessPortDetailsCertificates, _ int) *aliwaf.ModifyCloudResourceCertRequestCertificates {
appliedType := tea.StringValue(c.AppliedType)
certId := tea.StringValue(c.CertificateId)
if appliedType == certAppliedTypeDefault {
certId = cloudCertId
}
return &aliwaf.ModifyCloudResourceCertRequestCertificates{
CertificateId: tea.String(certId),
AppliedType: tea.String(appliedType),
}
})
} else {
// 指定扩展域名,需替换扩展证书
// 指定了扩展域名,如果已有就替换,没有就新增扩展证书
const certAppliedTypeExtension = "extension"
modifyCloudResourceReq.Listen.Certificates = append(modifyCloudResourceReq.Listen.Certificates, &aliwaf.ModifyCloudResourceRequestListenCertificates{
CertificateId: tea.String(cloudCertId),
AppliedType: tea.String(certAppliedTypeExtension),
})
// 先拿到当前实例已有的扩展证书的列表
// 构建新的证书列表,保留默认证书,处理扩展证书的替换或保留
var found bool
newCertificates := make([]*aliwaf.ModifyCloudResourceCertRequestCertificates, 0, len(cloudResourceAccessPortDetailsCertificates)+1)
for _, c := range cloudResourceAccessPortDetailsCertificates {
applied := tea.StringValue(c.AppliedType)
certId := tea.StringValue(c.CertificateId)
if applied == certAppliedTypeExtension {
// 从已同步的云产品实例全部证书列表里找到对应的证书,获取 CommonName
var certCommonName string
for _, rc := range resourceInstanceCertificates {
if tea.StringValue(rc.CertIdentifier) == certId {
certCommonName = tea.StringValue(rc.CommonName)
break
}
}
if certCommonName == d.config.Domain {
// 找到和目标域名匹配的扩展证书,替换为新证书 ID
newCertificates = append(newCertificates, &aliwaf.ModifyCloudResourceCertRequestCertificates{
CertificateId: tea.String(cloudCertId),
AppliedType: tea.String(certAppliedTypeExtension),
})
found = true
} else {
// 保留原来的扩展证书
newCertificates = append(newCertificates, &aliwaf.ModifyCloudResourceCertRequestCertificates{
CertificateId: c.CertificateId,
AppliedType: c.AppliedType,
})
}
} else {
// 默认证书保持不变
newCertificates = append(newCertificates, &aliwaf.ModifyCloudResourceCertRequestCertificates{
CertificateId: c.CertificateId,
AppliedType: c.AppliedType,
})
}
}
if !found {
// 如果没有找到要替换的扩展证书,追加一个扩展证书
newCertificates = append(newCertificates, &aliwaf.ModifyCloudResourceCertRequestCertificates{
CertificateId: tea.String(cloudCertId),
AppliedType: tea.String(certAppliedTypeExtension),
})
}
modifyCloudResourceCertReq.Certificates = newCertificates
}
// 过滤掉不存在或已过期的证书,防止接口报错
modifyCloudResourceReq.Listen.Certificates = lo.Filter(modifyCloudResourceReq.Listen.Certificates, func(c *aliwaf.ModifyCloudResourceRequestListenCertificates, _ int) bool {
modifyCloudResourceCertReq.Certificates = lo.Filter(modifyCloudResourceCertReq.Certificates, func(c *aliwaf.ModifyCloudResourceCertRequestCertificates, _ int) bool {
if tea.StringValue(c.CertificateId) == cloudCertId {
return true
}
@@ -285,14 +349,24 @@ func (d *Deployer) deployToWAF3WithCloudResource(ctx context.Context, cloudCertI
return false
})
// 修改云产品接入的配置
// REF: https://www.alibabacloud.com/help/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-modifycloudresource
modifyCloudResourceResp, err := d.sdkClient.ModifyCloudResourceWithContext(ctx, modifyCloudResourceReq, &dara.RuntimeOptions{})
d.logger.Debug("sdk request 'waf.ModifyCloudResource'", slog.Any("request", modifyCloudResourceReq), slog.Any("response", modifyCloudResourceResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'waf.ModifyCloudResource': %w", err)
// 最后检查待更新的列表里是否存在默认证书,没有则退出,否则接口会报错
var hasDefaultCert bool
for _, c := range modifyCloudResourceCertReq.Certificates {
if tea.StringValue(c.AppliedType) == "default" {
hasDefaultCert = true
break
}
}
if !hasDefaultCert {
return fmt.Errorf("could not find valid default certificate to apply for waf '%s' cloud resource '%s %s:%d'", d.config.InstanceId, d.config.ResourceProduct, d.config.ResourceId, d.config.ResourcePort)
}
// 执行更新请求
modifyCloudResourceCertResp, err := d.sdkClient.ModifyCloudResourceCertWithContext(ctx, modifyCloudResourceCertReq, &dara.RuntimeOptions{})
d.logger.Debug("sdk request 'waf.ModifyCloudResourceCert'", slog.Any("request", modifyCloudResourceCertReq), slog.Any("response", modifyCloudResourceCertResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'waf.ModifyCloudResourceCert': %w", err)
}
return nil
}
@@ -458,3 +458,178 @@ func (client *WafClient) ModifyDomainWithContext(ctx context.Context, tmpReq *al
_err = dara.Convert(_body, &_result)
return _result, _err
}
func (client *WafClient) CreateCertsWithContext(ctx context.Context, request *aliwaf.CreateCertsRequest, runtime *dara.RuntimeOptions) (_result *aliwaf.CreateCertsResponse, _err error) {
_err = request.Validate()
if _err != nil {
return _result, _err
}
query := map[string]interface{}{}
if !dara.IsNil(request.CertContent) {
query["CertContent"] = request.CertContent
}
if !dara.IsNil(request.CertKey) {
query["CertKey"] = request.CertKey
}
if !dara.IsNil(request.CertName) {
query["CertName"] = request.CertName
}
if !dara.IsNil(request.InstanceId) {
query["InstanceId"] = request.InstanceId
}
if !dara.IsNil(request.RegionId) {
query["RegionId"] = request.RegionId
}
if !dara.IsNil(request.ResourceManagerResourceGroupId) {
query["ResourceManagerResourceGroupId"] = request.ResourceManagerResourceGroupId
}
req := &openapiutil.OpenApiRequest{
Query: openapiutil.Query(query),
}
params := &openapiutil.Params{
Action: dara.String("CreateCerts"),
Version: dara.String("2021-10-01"),
Protocol: dara.String("HTTPS"),
Pathname: dara.String("/"),
Method: dara.String("POST"),
AuthType: dara.String("AK"),
Style: dara.String("RPC"),
ReqBodyType: dara.String("formData"),
BodyType: dara.String("json"),
}
_result = &aliwaf.CreateCertsResponse{}
_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)
if _err != nil {
return _result, _err
}
_err = dara.Convert(_body, &_result)
return _result, _err
}
func (client *WafClient) DescribeCloudResourceAccessPortDetailsWithContext(ctx context.Context, request *aliwaf.DescribeCloudResourceAccessPortDetailsRequest, runtime *dara.RuntimeOptions) (_result *aliwaf.DescribeCloudResourceAccessPortDetailsResponse, _err error) {
_err = request.Validate()
if _err != nil {
return _result, _err
}
query := map[string]interface{}{}
if !dara.IsNil(request.InstanceId) {
query["InstanceId"] = request.InstanceId
}
if !dara.IsNil(request.PageNumber) {
query["PageNumber"] = request.PageNumber
}
if !dara.IsNil(request.PageSize) {
query["PageSize"] = request.PageSize
}
if !dara.IsNil(request.Port) {
query["Port"] = request.Port
}
if !dara.IsNil(request.Protocol) {
query["Protocol"] = request.Protocol
}
if !dara.IsNil(request.RegionId) {
query["RegionId"] = request.RegionId
}
if !dara.IsNil(request.ResourceInstanceId) {
query["ResourceInstanceId"] = request.ResourceInstanceId
}
if !dara.IsNil(request.ResourceManagerResourceGroupId) {
query["ResourceManagerResourceGroupId"] = request.ResourceManagerResourceGroupId
}
if !dara.IsNil(request.ResourceProduct) {
query["ResourceProduct"] = request.ResourceProduct
}
req := &openapiutil.OpenApiRequest{
Query: openapiutil.Query(query),
}
params := &openapiutil.Params{
Action: dara.String("DescribeCloudResourceAccessPortDetails"),
Version: dara.String("2021-10-01"),
Protocol: dara.String("HTTPS"),
Pathname: dara.String("/"),
Method: dara.String("POST"),
AuthType: dara.String("AK"),
Style: dara.String("RPC"),
ReqBodyType: dara.String("formData"),
BodyType: dara.String("json"),
}
_result = &aliwaf.DescribeCloudResourceAccessPortDetailsResponse{}
_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)
if _err != nil {
return _result, _err
}
_err = dara.Convert(_body, &_result)
return _result, _err
}
func (client *WafClient) ModifyCloudResourceCertWithContext(ctx context.Context, request *aliwaf.ModifyCloudResourceCertRequest, runtime *dara.RuntimeOptions) (_result *aliwaf.ModifyCloudResourceCertResponse, _err error) {
_err = request.Validate()
if _err != nil {
return _result, _err
}
query := map[string]interface{}{}
if !dara.IsNil(request.Certificates) {
query["Certificates"] = request.Certificates
}
if !dara.IsNil(request.CloudResourceId) {
query["CloudResourceId"] = request.CloudResourceId
}
if !dara.IsNil(request.InstanceId) {
query["InstanceId"] = request.InstanceId
}
if !dara.IsNil(request.Port) {
query["Port"] = request.Port
}
if !dara.IsNil(request.RegionId) {
query["RegionId"] = request.RegionId
}
if !dara.IsNil(request.ResourceInstanceId) {
query["ResourceInstanceId"] = request.ResourceInstanceId
}
if !dara.IsNil(request.ResourceProduct) {
query["ResourceProduct"] = request.ResourceProduct
}
req := &openapiutil.OpenApiRequest{
Query: openapiutil.Query(query),
}
params := &openapiutil.Params{
Action: dara.String("ModifyCloudResourceCert"),
Version: dara.String("2021-10-01"),
Protocol: dara.String("HTTPS"),
Pathname: dara.String("/"),
Method: dara.String("POST"),
AuthType: dara.String("AK"),
Style: dara.String("RPC"),
ReqBodyType: dara.String("formData"),
BodyType: dara.String("json"),
}
_result = &aliwaf.ModifyCloudResourceCertResponse{}
_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)
if _err != nil {
return _result, _err
}
_err = dara.Convert(_body, &_result)
return _result, _err
}