|
| 1 | +--- |
| 2 | +title: Enable automatic HTTPS with Caddy as a sidecar container |
| 3 | +description: This guide describes how Caddy can be used as a reverse proxy to enhance your application with automatic HTTPS |
| 4 | +ms.author: tomcassidy |
| 5 | +author: tomvcassidy |
| 6 | +ms.service: container-instances |
| 7 | +services: container-instances |
| 8 | +ms.topic: how-to |
| 9 | +ms.date: 06/12/2023 |
| 10 | +--- |
| 11 | + |
| 12 | +# Enable automatic HTTPS with Caddy in a sidecar container |
| 13 | + |
| 14 | +This article describes how Caddy can be used as a sidecar container in a [container group](container-instances-container-groups.md) acting as a reverse proxy to provide an automatically managed HTTPS endpoint for your application. |
| 15 | + |
| 16 | +Caddy is a powerful, enterprise-ready, open source web server with automatic HTTPS written in Go and represents an alternative to Nginx. |
| 17 | + |
| 18 | +The automatization of certificates is possible because Caddy supports the ACMEv2 API ([RFC 8555](https://www.rfc-editor.org/rfc/rfc8555)) that interacts with [Let's Encrypt](https://letsencrypt.org/) to issue certificates. |
| 19 | + |
| 20 | +In this example, only the Caddy container gets exposed on ports 80/TCP and 443/TCP. The application behind the reverse proxy remains private. The network communication between Caddy and your application happens via localhost. |
| 21 | + |
| 22 | +> [!NOTE] |
| 23 | +> This stands in contrast to the intra container group communication known from docker compose, where containers can be referenced by name. |
| 24 | +
|
| 25 | +The example mounts the [Caddyfile](https://caddyserver.com/docs/caddyfile), which is required to configure the reverse proxy, from a file share hosted on an Azure Storage account. |
| 26 | + |
| 27 | +> [!NOTE] |
| 28 | +> For production deployments, most users will want to bake the Caddyfile into a custom docker image based on [caddy](https://hub.docker.com/_/caddy). This way, there is no need to mount files into the container. |
| 29 | +
|
| 30 | +[!INCLUDE [azure-cli-prepare-your-environment.md](~/articles/reusable-content/azure-cli/azure-cli-prepare-your-environment.md)] |
| 31 | + |
| 32 | +- This article requires version 2.0.55 or later of the Azure CLI. If using Azure Cloud Shell, the latest version is already installed. |
| 33 | + |
| 34 | +## Prepare the Caddyfile |
| 35 | + |
| 36 | +Create a file called `Caddyfile` and paste the following configuration. This configuration creates a reverse proxy configuration, pointing to your application container listening on 5000/TCP. |
| 37 | + |
| 38 | +```console |
| 39 | +my-app.westeurope.azurecontainer.io { |
| 40 | + reverse_proxy http://localhost:5000 |
| 41 | +} |
| 42 | +``` |
| 43 | + |
| 44 | +It's important to note, that the configuration references a domain name instead of an IP address. Caddy needs to be reachable by this URL to carry out the challenge step required by the ACME protocol and to successfully retrieve a certificate from Let's Encrypt. |
| 45 | + |
| 46 | +> [!NOTE] |
| 47 | +> For production deployment, users might want to use a domain name they control, e.g., `api.company.com` and create a CNAME record pointing to e.g. `my-app.westeurope.azurecontainer.io`. If so, it needs to be ensured, that the custom domain name is also used in the Caddyfile, instead of the one assigned by Azure (e.g., `*.westeurope.azurecontainer.io`). Further, the custom domain name, needs to be referenced in the ACI YAML configuration described later in this example. |
| 48 | +
|
| 49 | +## Prepare storage account |
| 50 | + |
| 51 | +Create a storage account |
| 52 | + |
| 53 | +```azurecli |
| 54 | +az storage account create \ |
| 55 | + --name <storage-account> \ |
| 56 | + --resource-group <resource-group> \ |
| 57 | + --location westeurope |
| 58 | +``` |
| 59 | + |
| 60 | +Store the connection string to an environment variable |
| 61 | + |
| 62 | +```azurecli |
| 63 | +AZURE_STORAGE_CONNECTION_STRING=$(az storage account show-connection-string --name <storage-account> --resource-group <resource-group> --output tsv) |
| 64 | +``` |
| 65 | + |
| 66 | +Create the file shares required to store the container state and caddy configuration. |
| 67 | + |
| 68 | +```azurecli |
| 69 | +az storage share create \ |
| 70 | + --name proxy-caddyfile \ |
| 71 | + --account-name <storage-account> |
| 72 | +
|
| 73 | +az storage share create \ |
| 74 | + --name proxy-config \ |
| 75 | + --account-name <storage-account> |
| 76 | + |
| 77 | + az storage share create \ |
| 78 | + --name proxy-data \ |
| 79 | + --account-name <storage-account> |
| 80 | +``` |
| 81 | + |
| 82 | +Retrieve the storage account keys and make a note for later use |
| 83 | + |
| 84 | +```azurecli |
| 85 | +az storage account keys list -g <resource-group> -n <storage-account> |
| 86 | +``` |
| 87 | + |
| 88 | +## Deploy container group |
| 89 | + |
| 90 | +### Create YAML file |
| 91 | + |
| 92 | +Create a file called `ci-my-app.yaml` and paste the following content. Ensure to replace `<account-key>` with one of the access keys previously received and `<storage-account>` accordingly. |
| 93 | + |
| 94 | +This YAML file defines two containers `reverse-proxy` and `my-app`. The `reverse-proxy` container mounts the three previously created file shares. The configuration also exposes port 80/TCP and 443/TCP of the `reverse-proxy` container. The communication between both containers happens on localhost only. |
| 95 | + |
| 96 | +>[!NOTE] |
| 97 | +> It's important to note, that the `dnsNameLabel` key, defines the public DNS name, under which the container instance group will be reachable, it needs to match the FQDN defined in the `Caddyfile` |
| 98 | +
|
| 99 | +```yml |
| 100 | +name: ci-my-app |
| 101 | +apiVersion: "2021-10-01" |
| 102 | +location: westeurope |
| 103 | +properties: |
| 104 | + containers: |
| 105 | + - name: reverse-proxy |
| 106 | + properties: |
| 107 | + image: caddy:2.6 |
| 108 | + ports: |
| 109 | + - protocol: TCP |
| 110 | + port: 80 |
| 111 | + - protocol: TCP |
| 112 | + port: 443 |
| 113 | + resources: |
| 114 | + requests: |
| 115 | + memoryInGB: 1.0 |
| 116 | + cpu: 1.0 |
| 117 | + limits: |
| 118 | + memoryInGB: 1.0 |
| 119 | + cpu: 1.0 |
| 120 | + volumeMounts: |
| 121 | + - name: proxy-caddyfile |
| 122 | + mountPath: /etc/caddy |
| 123 | + - name: proxy-data |
| 124 | + mountPath: /data |
| 125 | + - name: proxy-config |
| 126 | + mountPath: /config |
| 127 | + - name: my-app |
| 128 | + properties: |
| 129 | + image: mcr.microsoft.com/azuredocs/aci-helloworld |
| 130 | + ports: |
| 131 | + - port: 5000 |
| 132 | + protocol: TCP |
| 133 | + environmentVariables: |
| 134 | + - name: PORT |
| 135 | + value: 5000 |
| 136 | + resources: |
| 137 | + requests: |
| 138 | + memoryInGB: 1.0 |
| 139 | + cpu: 1.0 |
| 140 | + limits: |
| 141 | + memoryInGB: 1.0 |
| 142 | + cpu: 1.0 |
| 143 | + ipAddress: |
| 144 | + ports: |
| 145 | + - protocol: TCP |
| 146 | + port: 80 |
| 147 | + - protocol: TCP |
| 148 | + port: 443 |
| 149 | + type: Public |
| 150 | + dnsNameLabel: my-app |
| 151 | + osType: Linux |
| 152 | + volumes: |
| 153 | + - name: proxy-caddyfile |
| 154 | + azureFile: |
| 155 | + shareName: proxy-caddyfile |
| 156 | + storageAccountName: "<storage-account>" |
| 157 | + storageAccountKey: "<account-key>" |
| 158 | + - name: proxy-data |
| 159 | + azureFile: |
| 160 | + shareName: proxy-data |
| 161 | + storageAccountName: "<storage-account>" |
| 162 | + storageAccountKey: "<account-key>" |
| 163 | + - name: proxy-config |
| 164 | + azureFile: |
| 165 | + shareName: proxy-config |
| 166 | + storageAccountName: "<storage-account>" |
| 167 | + storageAccountKey: "<account-key>" |
| 168 | +``` |
| 169 | +
|
| 170 | +### Deploy the container group |
| 171 | +
|
| 172 | +Create a resource group with the [az group create](/cli/azure/group#az-group-create) command: |
| 173 | +
|
| 174 | +```azurecli |
| 175 | +az group create --name <resource-group> --location westeurope |
| 176 | +``` |
| 177 | + |
| 178 | +Deploy the container group with the [az container create](/cli/azure/container#az-container-create) command, passing the YAML file as an argument. |
| 179 | + |
| 180 | +```azurecli |
| 181 | +az container create --resource-group <resource-group> --file ci-my-app.yaml |
| 182 | +``` |
| 183 | + |
| 184 | +### View the deployment state |
| 185 | + |
| 186 | +To view the state of the deployment, use the following [az container show](/cli/azure/container#az-container-show) command: |
| 187 | + |
| 188 | +```azurecli |
| 189 | +az container show --resource-group <resource-group> --name ci-my-app --output table |
| 190 | +``` |
| 191 | + |
| 192 | +### Verify TLS connection |
| 193 | + |
| 194 | +Before verifying if everything went well, give the container group some time to fully start and for Caddy to request a certificate. |
| 195 | + |
| 196 | +#### OpenSSL |
| 197 | + |
| 198 | +We can use the `s_client` subcommand of OpenSSL for that purpose. |
| 199 | + |
| 200 | +```bash |
| 201 | +echo "Q" | openssl s_client -connect my-app.westeurope.azurecontainer.io:443 |
| 202 | +``` |
| 203 | + |
| 204 | +```console |
| 205 | +CONNECTED(00000188) |
| 206 | +--- |
| 207 | +Certificate chain |
| 208 | + 0 s:CN = my-app.westeurope.azurecontainer.io |
| 209 | + i:C = US, O = Let's Encrypt, CN = R3 |
| 210 | + 1 s:C = US, O = Let's Encrypt, CN = R3 |
| 211 | + i:C = US, O = Internet Security Research Group, CN = ISRG Root X1 |
| 212 | + 2 s:C = US, O = Internet Security Research Group, CN = ISRG Root X1 |
| 213 | + i:O = Digital Signature Trust Co., CN = DST Root CA X3 |
| 214 | +--- |
| 215 | +Server certificate |
| 216 | +-----BEGIN CERTIFICATE----- |
| 217 | +MIIEgTCCA2mgAwIBAgISAxxidSnpH4vVuCZk9UNG/pd2MA0GCSqGSIb3DQEBCwUA |
| 218 | +MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD |
| 219 | +EwJSMzAeFw0yMzA0MDYxODAzMzNaFw0yMzA3MDUxODAzMzJaMC4xLDAqBgNVBAMT |
| 220 | +I215LWFwcC53ZXN0ZXVyb3BlLmF6dXJlY29udGFpbmVyLmlvMFkwEwYHKoZIzj0C |
| 221 | +AQYIKoZIzj0DAQcDQgAEaaN/wGyFcimM+1O4WzbFgO6vIlXxXqp9vgmLZHpFrNwV |
| 222 | +aO8JbaB7hE+M5EAg34LDY80RyHgY+Ff4vTh2Z96rVqOCAl4wggJaMA4GA1UdDwEB |
| 223 | +/wQEAwIHgDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/ |
| 224 | +BAIwADAdBgNVHQ4EFgQUoL5DP+4PWiyE79hL5o+v8uymHdAwHwYDVR0jBBgwFoAU |
| 225 | +FC6zF7dYVsuuUAlA5h+vnYsUwsYwVQYIKwYBBQUHAQEESTBHMCEGCCsGAQUFBzAB |
| 226 | +hhVodHRwOi8vcjMuby5sZW5jci5vcmcwIgYIKwYBBQUHMAKGFmh0dHA6Ly9yMy5p |
| 227 | +LmxlbmNyLm9yZy8wLgYDVR0RBCcwJYIjbXktYXBwLndlc3RldXJvcGUuYXp1cmVj |
| 228 | +b250YWluZXIuaW8wTAYDVR0gBEUwQzAIBgZngQwBAgEwNwYLKwYBBAGC3xMBAQEw |
| 229 | +KDAmBggrBgEFBQcCARYaaHR0cDovL2Nwcy5sZXRzZW5jcnlwdC5vcmcwggEEBgor |
| 230 | +BgEEAdZ5AgQCBIH1BIHyAPAAdgC3Pvsk35xNunXyOcW6WPRsXfxCz3qfNcSeHQmB |
| 231 | +Je20mQAAAYdX8+CQAAAEAwBHMEUCIQC9Ztqd3DXoJhOIHBW+P7ketGrKlVA6nPZl |
| 232 | +9CiOrn6t8gIgXHcrbBqItemndRMv+UJ3DaBfTkYOqECecOJCgLhSYNUAdgDoPtDa |
| 233 | +PvUGNTLnVyi8iWvJA9PL0RFr7Otp4Xd9bQa9bgAAAYdX8+CAAAAEAwBHMEUCIBJ1 |
| 234 | +24z44vKFUOLCi1a7ymVuWErkmLb/GtysvcxILaj0AiEAr49hyKfen4BbSTwC8Fg4 |
| 235 | +/LgZnn2F3uHI+9p+ZMO9xTAwDQYJKoZIhvcNAQELBQADggEBACqxa21eiW3JrZwk |
| 236 | +FHgpd6SxhUeecrYXxFNva1Y6G//q2qCmGeKK3GK+ZGPqDtcoASH5t5ghV4dIT4WU |
| 237 | +auVDLFVywXzR8PT6QUu3W8QxU+W7406twBf23qMIgrF8PIWhStI5mn1uCpeqlnf5 |
| 238 | +HpRaj2f5/5n19pcCZcrRx94G9qhPYdMzuy4mZRhxXRqrpIsabqX3DC2ld8dszCvD |
| 239 | +pkV61iuARgm3MIQz1yL/x5Bn4nywjnhYZA4KFktC0Ti55cPRh1mkzGQAsYQDdWrq |
| 240 | +dVav+U9dOLQ4Sq4suaDmzDzApr+hpQSJhwgRN16+tLMyZ6INAU2JWKDxiyDTdOuH |
| 241 | +jz456og= |
| 242 | +-----END CERTIFICATE----- |
| 243 | +subject=CN = my-app.westeurope.azurecontainer.io |
| 244 | + |
| 245 | +issuer=C = US, O = Let's Encrypt, CN = R3 |
| 246 | + |
| 247 | +--- |
| 248 | +No client certificate CA names sent |
| 249 | +Peer signing digest: SHA256 |
| 250 | +Peer signature type: ECDSA |
| 251 | +Server Temp Key: X25519, 253 bits |
| 252 | +--- |
| 253 | +SSL handshake has read 4208 bytes and written 401 bytes |
| 254 | +Verification error: unable to get local issuer certificate |
| 255 | +--- |
| 256 | +New, TLSv1.3, Cipher is TLS_AES_128_GCM_SHA256 |
| 257 | +Server public key is 256 bit |
| 258 | +Secure Renegotiation IS NOT supported |
| 259 | +Compression: NONE |
| 260 | +Expansion: NONE |
| 261 | +No ALPN negotiated |
| 262 | +Early data was not sent |
| 263 | +Verify return code: 20 (unable to get local issuer certificate) |
| 264 | +--- |
| 265 | +--- |
| 266 | +Post-Handshake New Session Ticket arrived: |
| 267 | +SSL-Session: |
| 268 | + Protocol : TLSv1.3 |
| 269 | + Cipher : TLS_AES_128_GCM_SHA256 |
| 270 | + Session-ID: 85F1A4290F99A0DD28C8CB21EF4269E7016CC5D23485080999A8548057729B24 |
| 271 | + Session-ID-ctx: |
| 272 | + Resumption PSK: 752D438C19A5DBDBF10781F863D5E5D9A8859230968A9EAFFF7BBA86937D004F |
| 273 | + PSK identity: None |
| 274 | + PSK identity hint: None |
| 275 | + SRP username: None |
| 276 | + TLS session ticket lifetime hint: 604800 (seconds) |
| 277 | + TLS session ticket: |
| 278 | + 0000 - 2f 25 98 90 9d 46 9b 01-03 78 db bd 4d 64 b3 a6 /%...F...x..Md.. |
| 279 | + 0010 - 52 c0 7a 8a b6 3d b8 4b-c0 d7 fc 04 e8 63 d4 bb R.z..=.K.....c.. |
| 280 | + 0020 - 15 b3 25 b7 be 64 3d 30-2b d7 dc 7a 1a d1 22 63 ..%..d=0+..z.."c |
| 281 | + 0030 - 42 30 90 65 6b b5 e1 83-a3 6c 76 c8 f6 ae e9 31 B0.ek....lv....1 |
| 282 | + 0040 - 45 91 33 57 8e 9f 4b 6a-2e 2c 9b f9 87 5f 71 1d E.3W..Kj.,..._q. |
| 283 | + 0050 - 5a 84 59 50 17 31 1f 62-2b 0e 1e e5 70 03 d9 e9 Z.YP.1.b+...p... |
| 284 | + 0060 - 50 1c 5d 1f a4 3c 8a 0e-f4 c5 7d ce 9e 5c 98 de P.]..<....}..\.. |
| 285 | + 0070 - e5 . |
| 286 | + |
| 287 | + Start Time: 1680808973 |
| 288 | + Timeout : 7200 (sec) |
| 289 | + Verify return code: 20 (unable to get local issuer certificate) |
| 290 | + Extended master secret: no |
| 291 | + Max Early Data: 0 |
| 292 | +--- |
| 293 | +read R BLOCK |
| 294 | +``` |
| 295 | + |
| 296 | +#### Chrome browser |
| 297 | + |
| 298 | +Navigate to https://my-app.westeurope.azurecontainer.io and verify the certificate by clicking on the padlock next to the URL. |
| 299 | + |
| 300 | +:::image type="content" source="media/container-instances-container-group-automatic-ssl/url-padlock.png" alt-text="Screenshot highlighting the padlock next to the URL that verifies the certificate."::: |
| 301 | + |
| 302 | +To see the certificate details, click on "Connection is secure" followed by "certificate is valid". |
| 303 | + |
| 304 | +:::image type="content" source="media/container-instances-container-group-automatic-ssl/lets-encrypt-certificate.png" alt-text="Screenshot of the certificate issued by Let's Encrypt"::: |
| 305 | + |
| 306 | +## Next steps |
| 307 | +- [Caddy documentation](https://caddyserver.com/docs/) |
| 308 | +- [GitHub aci-helloworld](https://github.com/Azure-Samples/aci-helloworld) |
| 309 | +- [YAML reference: Azure Container Instances](container-instances-reference-yaml.md) |
| 310 | +- [Secure your codeless REST API with automatic HTTPS using Data API builder and Caddy](https://www.azureblue.io/secure-your-codeless-rest-api-with-automatic-https-using-data-api-builder-and-caddy/) |
0 commit comments