Azure 證書自動化管理解決方案


       節前不打烊,以此小文提前預祝大家鼠年大吉。最近和小伙伴們溝通發現,經常有系統出現證書過期的問題,究其原因主要是1. 證書后台自動 renew 了但是系統並沒有及時更新 2. 因為沒有郵件提醒所以忽略的證書過期。針對上述問題,目前在 Azure 平台內大部分的 PaaS 服務都已經內置了證書自動更新的能力,但對於 IaaS 服務,應為管理界面從操作系統開始都掌握在客戶自己手上,所以操作系統內應用的證書更新需要客戶自己來完成,那么 Azure 平台有什么好的方法可以幫助客戶避免證書過期的問題。本文將介紹如何通過 Azure 平台的服務實現證書過期發現及自動更新。

證書過期預警:

        Azure 平台的 Key Vault 服務提供了證書托管服務,客戶可以將自己的管理的證書上傳至 Key Vault 服務,然后通過設置 Issuance Policy,通過在其中設置 Lifetime Action Type 來實現證書過期郵件提醒,可參閱:https://docs.microsoft.com/en-us/azure/key-vault/about-keys-secrets-and-certificates#key-vault-certificates

證書自動更新:

        Azure 平台的 Key Vault 服務支持對自簽名或支持的集成證書提供方 (DigiCert, GlobalSign) 提供自動證書 Renew 服務,借助該功能可實現無干預的證書自動續簽。但是類似虛擬機 IaaS 服務內的應用服務證書更新仍然需要用戶自己來處理完成。這部分的自動化可以通過 Event Grid + Logic App 來實現。Azure Event Grid 服務原生集成支持 Key Vault 服務的事件消息,當有證書續簽的時候會釋放證書事件消息,Logic App 通過訂閱監聽改證書事件消息可以實現后續證書更細的自動化流程。本文幫助大家更快上手選擇了 Logic App(基本上可以是零代碼),喜歡代碼解決問題的同學可以考慮使用 Function 服務。本文的例子中以更新虛擬機中 Nginx 服務其中的 SSL 證書為例,在 Logic App 中通過調用 Azure Resource Management -- Resource Update 模塊來將續簽好的證書上傳至虛擬機,這里需要強調微軟雲的 VM 原生的模板中已經支持證書屬性,該屬性可以通過指定 Azure Key Vault 中存放的證書地址實現將證書上傳至虛擬機中,這樣就實現了 Azure Resource Template 來自動上傳證書。上傳證書后再調用 Azure Resource Management -- Resource Update 來調用 VM 的 customscript extension 擴展模塊,通過執行腳本來實現將上傳好的續簽證書來自動更新 Nginx SSL 證書存放路徑的過期證書。下面我們來看一下方案的架構圖:

 

 

        上圖中還引入了 Azure Table,主要是通過基礎的 KV 服務實現多應用/多主機證書自動更新的支持,在 Azure Table 中存儲 KeyVault 名稱,證書名稱和虛擬機的對應關系,這樣當 Event Grid 中 Fire 事件后,可以通過在 Table 中查詢到與該事件相關證書與什么虛擬機相關。

        Logic App 里面三個模塊的具體實現這里不再做詳細的贅述,基本實現邏輯在上面已經介紹,大家可以參照下面的實例模板代碼進行導入修改。其中資源組名稱,作為初始變量,大家可以根據自己的實際環境進行調整。

 
         
{
    "definition": {
        "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
        "actions": {
            "Compose_cert_template": {
                "inputs": {
                    "osProfile": {
                        "secrets": [
                            {
                                "sourceVault": {
                                    "id": "@concat('/subscriptions/c04b3c63-8dfe-4f98-be18-e71ff67a1f4e/resourceGroups/',variables('resourcegroupname'),'/providers/Microsoft.KeyVault/vaults/',body('Parse_JSON')?['VaultName'])"
                                },
                                "vaultCertificates": [
                                    {
                                        "certificateUrl": "@concat('https://',body('Parse_JSON')?['VaultName'],'.vault.azure.net/secrets/',body('Parse_JSON')?['ObjectName'],'/',body('Parse_JSON')?['Version'])"
                                    }
                                ]
                            }
                        ]
                    }
                },
                "runAfter": {
                    "Initialize_vmname_variable": [
                        "Succeeded"
                    ]
                },
                "type": "Compose"
            },
            "Delay": {
                "inputs": {
                    "interval": {
                        "count": 30,
                        "unit": "Second"
                    }
                },
                "runAfter": {
                    "Update_Application_certificate": [
                        "Succeeded"
                    ]
                },
                "type": "Wait"
            },
            "Get_entity": {
                "inputs": {
                    "host": {
                        "connection": {
                            "name": "@parameters('$connections')['azuretables']['connectionId']"
                        }
                    },
                    "method": "get",
                    "path": "/Tables/@{encodeURIComponent('vmcertmapping')}/entities(PartitionKey='@{encodeURIComponent(body('Parse_JSON')?['VaultName'])}',RowKey='@{encodeURIComponent(body('Parse_JSON')?['ObjectName'])}')"
                },
                "runAfter": {
                    "Parse_JSON": [
                        "Succeeded"
                    ]
                },
                "type": "ApiConnection"
            },
            "Initialize_resourcegroup_variable": {
                "inputs": {
                    "variables": [
                        {
                            "name": "resourcegroupname",
                            "type": "string",
                            "value": "akvdemo"
                        }
                    ]
                },
                "runAfter": {},
                "type": "InitializeVariable"
            },
            "Initialize_vmname_variable": {
                "inputs": {
                    "variables": [
                        {
                            "name": "vmname",
                            "type": "string",
                            "value": "@{body('Get_entity')?['vmname']}"
                        }
                    ]
                },
                "runAfter": {
                    "Get_entity": [
                        "Succeeded"
                    ]
                },
                "type": "InitializeVariable"
            },
            "Parse_JSON": {
                "inputs": {
                    "content": "@triggerBody()?['data']",
                    "schema": {
                        "properties": {
                            "EXP": {
                                "type": "integer"
                            },
                            "Id": {
                                "type": "string"
                            },
                            "NBF": {
                                "type": "integer"
                            },
                            "ObjectName": {
                                "type": "string"
                            },
                            "ObjectType": {
                                "type": "string"
                            },
                            "VaultName": {
                                "type": "string"
                            },
                            "Version": {
                                "type": "string"
                            }
                        },
                        "type": "object"
                    }
                },
                "runAfter": {
                    "Initialize_resourcegroup_variable": [
                        "Succeeded"
                    ]
                },
                "type": "ParseJson"
            },
            "Refresh_Customextension": {
                "inputs": {
                    "body": {
                        "location": "eastus",
                        "properties": {
                            "autoUpgradeMinorVersion": true,
                            "publisher": "Microsoft.Azure.Extensions",
                            "settings": {
                                "commandToExecute": "date && pwd && rm /var/lib/waagent/*.prv && rm /var/lib/waagent/*.crt",
                                "fileUris": []
                            },
                            "type": "CustomScript",
                            "typeHandlerVersion": "2.0"
                        }
                    },
                    "host": {
                        "connection": {
                            "name": "@parameters('$connections')['arm']['connectionId']"
                        }
                    },
                    "method": "put",
                    "path": "/subscriptions/@{encodeURIComponent('c04b3c63-8dfe-4f98-be18-e71ff67a1f4e')}/resourcegroups/@{encodeURIComponent(variables('resourcegroupname'))}/providers/@{encodeURIComponent('Microsoft.Compute')}/@{encodeURIComponent(concat('virtualMachines/',variables('vmname'),'/extensions/customScript'))}",
                    "queries": {
                        "x-ms-api-version": "2019-03-01"
                    }
                },
                "runAfter": {
                    "Delay": [
                        "Succeeded"
                    ]
                },
                "type": "ApiConnection"
            },
            "Update_Application_certificate": {
                "inputs": {
                    "body": {
                        "location": "eastus",
                        "properties": {
                            "autoUpgradeMinorVersion": true,
                            "publisher": "Microsoft.Azure.Extensions",
                            "settings": {
                                "commandToExecute": "sh config-cert.sh",
                                "fileUris": [
                                    "https://akvdemoscript.blob.core.windows.net/customscript/config-cert.sh"
                                ]
                            },
                            "type": "CustomScript",
                            "typeHandlerVersion": "2.0"
                        }
                    },
                    "host": {
                        "connection": {
                            "name": "@parameters('$connections')['arm']['connectionId']"
                        }
                    },
                    "method": "put",
                    "path": "/subscriptions/@{encodeURIComponent('c04b3c63-8dfe-4f98-be18-e71ff67a1f4e')}/resourcegroups/@{encodeURIComponent(variables('resourcegroupname'))}/providers/@{encodeURIComponent('Microsoft.Compute')}/@{encodeURIComponent(concat('virtualMachines/',variables('vmname'),'/extensions/customScript'))}",
                    "queries": {
                        "x-ms-api-version": "2019-03-01"
                    }
                },
                "runAfter": {
                    "Update_VM_certificate": [
                        "Succeeded"
                    ]
                },
                "type": "ApiConnection"
            },
            "Update_VM_certificate": {
                "inputs": {
                    "body": {
                        "location": "eastus",
                        "properties": "@outputs('Compose_cert_template')"
                    },
                    "host": {
                        "connection": {
                            "name": "@parameters('$connections')['arm']['connectionId']"
                        }
                    },
                    "method": "put",
                    "path": "/subscriptions/@{encodeURIComponent('c04b3c63-8dfe-4f98-be18-e71ff67a1f4e')}/resourcegroups/@{encodeURIComponent(variables('resourcegroupname'))}/providers/@{encodeURIComponent('Microsoft.Compute')}/@{encodeURIComponent(concat('virtualMachines/',variables('vmname')))}",
                    "queries": {
                        "x-ms-api-version": "2019-03-01"
                    }
                },
                "runAfter": {
                    "Compose_cert_template": [
                        "Succeeded"
                    ]
                },
                "type": "ApiConnection"
            }
        },
        "contentVersion": "1.0.0.0",
        "outputs": {},
        "parameters": {
            "$connections": {
                "defaultValue": {},
                "type": "Object"
            }
        },
        "triggers": {
            "When_a_resource_event_occurs": {
                "inputs": {
                    "body": {
                        "properties": {
                            "destination": {
                                "endpointType": "webhook",
                                "properties": {
                                    "endpointUrl": "@{listCallbackUrl()}"
                                }
                            },
                            "filter": {
                                "includedEventTypes": [
                                    "Microsoft.KeyVault.CertificateNewVersionCreated"
                                ]
                            },
                            "topic": "/subscriptions/c04b3c63-8dfe-4f98-be18-e71ff67a1f4e/resourceGroups/akvdemo/providers/Microsoft.KeyVault/vaults/akvdemorepo"
                        }
                    },
                    "host": {
                        "connection": {
                            "name": "@parameters('$connections')['azureeventgrid']['connectionId']"
                        }
                    },
                    "path": "/subscriptions/@{encodeURIComponent('c04b3c63-8dfe-4f98-be18-e71ff67a1f4e')}/providers/@{encodeURIComponent('Microsoft.KeyVault.vaults')}/resource/eventSubscriptions",
                    "queries": {
                        "x-ms-api-version": "2017-09-15-preview"
                    }
                },
                "splitOn": "@triggerBody()",
                "type": "ApiConnectionWebhook"
            }
        }
    },
    "parameters": {
        "$connections": {
            "value": {
                "arm": {
                    "connectionId": "/subscriptions/c04b3c63-8dfe-4f98-be18-e71ff67a1f4e/resourceGroups/akvdemo/providers/Microsoft.Web/connections/arm",
                    "connectionName": "arm",
                    "id": "/subscriptions/c04b3c63-8dfe-4f98-be18-e71ff67a1f4e/providers/Microsoft.Web/locations/eastus/managedApis/arm"
                },
                "azureeventgrid": {
                    "connectionId": "/subscriptions/c04b3c63-8dfe-4f98-be18-e71ff67a1f4e/resourceGroups/akvdemo/providers/Microsoft.Web/connections/azureeventgrid",
                    "connectionName": "azureeventgrid",
                    "id": "/subscriptions/c04b3c63-8dfe-4f98-be18-e71ff67a1f4e/providers/Microsoft.Web/locations/eastus/managedApis/azureeventgrid"
                },
                "azuretables": {
                    "connectionId": "/subscriptions/c04b3c63-8dfe-4f98-be18-e71ff67a1f4e/resourceGroups/akvdemo/providers/Microsoft.Web/connections/azuretables",
                    "connectionName": "azuretables",
                    "id": "/subscriptions/c04b3c63-8dfe-4f98-be18-e71ff67a1f4e/providers/Microsoft.Web/locations/eastus/managedApis/azuretables"
                }
            }
        }
    }
}
 

        Azure VM 對於 Azure Key Vault 的證書原生支持,通過在 VM 描述 Template 描述屬性中直接引用 Key Vault 證書路徑,即可實現虛擬機對證書的上傳導入, Azure Resource VM Template 示例可以參閱如下示例。

{
  "osProfile": {
    "secrets": [
      {
        "sourceVault": {
          "id": "@{keyvaultid}"
        },
        "vaultCertificates": "@{certificate secret id}"
          }
        ]
      }
    ]
  }
}

 

        在 Logic App 中執行的 Custom Script Extension, 是通過執行已經預先上載在 Blob 服務中的腳本文件來實現應用證書更新的,這部分代碼邏輯是按照 Nginx SSL 證書示例來做的,大家可以按照實際自己的應用證書路徑進行修改。

#!/bin/bash

secretsname=$(find /var/lib/waagent/ -name "*.prv" | cut -c -57)
cp $secretsname.crt /etc/nginx/ssl/demo.cert
cp $secretsname.prv /etc/nginx/ssl/demo.prv
rm -f $secretsname.crt
rm -f $secretsname.prv
sudo service nginx restart
echo $secretsname

         Azure Table 中的 Schema 非常簡單,只包含一個 vmname 屬性,如果大家有更復雜的 Mapping 邏輯可以按照自己的要求定義自己的 Schema。下面供大家參考。其中 PartitionKey 采用 KeyVault Name,RowKey 采用證書名稱,該場景 entity 數量和訪問頻次都不會很多,所以這種 Partition 方式已經可以滿足要求。

 

         好了就寫到這里啦,小伙伴們鼠年再見!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM