Written by
Sascha Schwarze (schwarzs@de.ibm.com)
Senior Software Engineer - IBM Cloud Code Engine
Enrico Regge (reggeenr@de.ibm.com)
Architect - IBM Cloud Code Engine
Abstract
IBM Cloud Code Engine is a fully managed, serverless platform that runs your containerized workloads, including web apps, REST APIs, microservices, event-driven functions or batch jobs. In this article, we're demonstrating how to increase the protection of Code Engine apps and functions, that are exposed to the public internet through a custom domain, by a firewall provided by IBM Cloud Internet Services.
# Select the Code Engine project
ibmcloud ce project select --name <your-project>
# Create an app based on the Code Engine sample image
ibmcloud ce app create --name myapp --image icr.io/codeengine/hello --cpu 0.125 --memory 0.25G --min-scale 1
Once the app is deployed, you'll be able to access it on the URL path "/debug" through its public system domain using your browser, or cURL in your command-line, or any other HTTPS client of your choice:
curl --silent "$(ibmcloud ce app get --name myapp --output url)/debug" | jq
{
"headers": {
"host": "myapp.1mr3jprdj7mi.eu-es.codeengine.appdomain.cloud",
"user-agent": "curl/8.7.1",
"accept": "*/*",
"forwarded": "for=172.30.115.128;proto=https, for=127.0.0.6",
"x-forwarded-for": "172.30.115.128, 127.0.0.6, 127.0.0.6",
"x-forwarded-proto": "https",
"x-request-id": "d8bdd2c6-7376-4964-8cf2-114d80ac416e",
…
},
"url": "/debug",
"method": "GET",
"env": {
…
}
}
Note: To improve formatting, the command above pipes the JSON data output of the cURL operation to the tool jq.
As mentioned before, the sample app "hello" can be used to observe the request input provided to the app. The listed header "x-forwarded-for", is a well-known source for identifying the originating IP address of the client that connects to a web server. But in our case it does only list Code Engine owned internal and external IP addresses. To prove this, use an online services of your choice, like ifconfig.me, ip.me, or whatismyip.com, to obtain your public internet IP address and check whether it appears in the apps debug output… It will not.
curl -4 ifconfig.me
158.175.119.4
At the time we are writing this blog post, our public IP address was "158.175.119.4"
In the following section, we'll make sure that the clients IP address gets exposed to the app, too.
Configuring CIS to forward the users IP address to your app
In general, there are multiple ways to order a TLS certificate for a custom domain, such as using Certbot and Let's Encrypt, or by ordering an origin certificate in CIS, or through your well established certificate creation initiation/renewal process. For this blog post we chose to use CIS origin certificates. But the choice of how certificates are managed, has no effect on the security features that allow to filter for IP addresses.
In the IBM Cloud Console, following steps can be used to configure your domain properly:
-
In the Cloud Console, navigate to the "Resource list" and filter for your "Internet Services" instance. Click on the name of the target instance to navigate to its detail page
-
On the CIS instance details page navigate to "Security > Origin" and Click "Order"
-
In the "Order origin certificate" panel, keep the defaults and enter the domain name that you want to have handled, e.g. "myapp.example.com". Confirm by clicking "Order"
-
Copy and paste the ordered "Origin certificate" and "Private key" values into temporary local files
-
Navigate to the Serverless projects page in Code Engine and click on an existing project, or create a new one
-
On the project details page, navigate to "Applications" and click "Create"
-
Choose "myapp" as a name and specifcy "icr.io/codeengine/hello" as the image reference to use. Confirm by clicking "Create"
-
On the apps detail page, navigate to "Domain mappings" and click "Create"
-
In the "Create domain mapping" panel, paste the Certificate and private key and enter the fully-qualified domain name, e.g. "myapp.example.com". Confirm by clicking "Create", after copying the CNAME target value into the clipboard
-
On the same page, select the option "No external system domain mapping" to deactivate the public system domain mapping. Confirm the modal by clicking "Apply".
-
Navigate to the CIS instance details page (see step 1)
-
On the CIS instance details page navigate to "Reliability > DNS", scroll to "DNS records" and click "Add"
-
In "Add record" panel, choose "CNAME" as the type, set your subdomain; e.g. "myapp" as name and copy the CNAME target captured previously into the Alias domain name. Make sure the details output mentions something similar than "myapp.example.com is an alias of custom.<id>.<region>.codeengine.appdomain.cloud.", before confirming the dialogue by clicking "Add"
-
Once all steps have been completed, the DNS record should look as shown in the screenshot below
The steps in the IBM Cloud CLI are depicted below. Please note, that the commands listed will work on most Linux and macOS distributions. If for some reason they don't work on yours, or if you are using Windows, please consider to use the IBM Cloud Console to create the sample configuration.
# Define your target hostname that should receive traffic on your custom domain and direct it to the Code Engine app
BASE_DOMAIN=example.com
SUB_DOMAIN=myapp
DOMAIN_NAME=${SUB_DOMAIN}.${BASE_DOMAIN}
CE_APP=myapp
# Install Cloud Internet Services plugin
ibmcloud plugin install cis -f
# List all CIS instances in your account.
ibmcloud cis instances
# Set the context, by using the ID of the previous command
ibmcloud cis instance-set <ID>
# Obtain the DNS domain ID
DNS_DOMAIN_ID=$(ibmcloud cis domains --output json |jq -r --arg base_domain $BASE_DOMAIN '.[]|select(.name==$base_domain)|.id')
# Create a new origin certificate that is used to protected the traffic between CIS and the Code Engine app
ibmcloud cis origin-certificate-create $DNS_DOMAIN_ID --cert-type origin-rsa --hostnames $DOMAIN_NAME --output json
# Store the output of the "certificate" and "private_key" properties in individual files (make sure to preserve the trailing line feeds)
echo "<certificate_value>" > myapp.crt
echo "<private_key_value>" > myapp.key
# Create a Code Engine TLS secret
ibmcloud ce secret create --name "${CE_APP}-tls" -format tls --private-key-file myapp.key --cert-chain-file myapp.crt
rm myapp.crt
rm myapp.key
# Create a Code Engine domain mapping
ibmcloud ce domainmapping create --name $DOMAIN_NAME --target $CE_APP --target-type application --tls-secret "${CE_APP}-tls"
CNAME_TARGET="$(ibmcloud ce domainmapping get --name $DOMAIN_NAME --output json | jq -r '.cname_target')"
# Update the app configuration and deactivate its system-generated public endpoint
ibmcloud ce app update --name $CE_APP --cluster-local
# Create the DNS record in CIS to make sure that traffic for the domain is routed to Code Engine
ibmcloud cis dns-record-create $DNS_DOMAIN_ID --type CNAME --name $DOMAIN_NAME --content $CNAME_TARGET --proxied true
It is important to point out, that we enabled the proxy mode on the created DNS record in CIS, as this setting makes sure that traffic flows through the security and performance functions on CIS. In essence, this means that the clients IP address will be extracted and exposed into request headers that are forwarded to the Code Engine app.
Now, let us check what kind of headers are exposed to the app, after completing the configuration. We'll use the same tooling as before but this time we are calling the external domain, instead of the public system domain provided by Code Engine.
curl --silent https://myapp.example.com/debug | jq
{
"headers": {
"host": "myapp.example.com",
"user-agent": "curl/8.7.1",
"accept": "*/*",
"accept-encoding": "gzip, br",
"cdn-loop": "cloudflare; loops=1",
"cf-connecting-ip": "158.175.119.4",
"cf-ipcountry": "DE",
"cf-ray": "8dd7289a6cfbd2d2-FRA",
"cf-visitor": "{\"scheme\":\"https\"}",
"forwarded": "for=\"[158.175.119.4]\";proto=https, for=172.30.207.64, for=172.30.217.195, for=127.0.0.6",
"k-proxy-request": "activator",
"x-envoy-attempt-count": "1",
"x-envoy-external-address": "172.30.207.64",
"x-forwarded-for": "158.175.119.4,172.30.207.64,172.30.217.195, 127.0.0.6, 127.0.0.6",
"x-forwarded-proto": "https",
"x-request-id": "f24a0d54-7668-4236-84a4-9fb635a62c0e"
},
"url": "/debug",
"method": "GET",
"env": {
…
}
}
As you can see, the IP address that is exposed in the app, now maps to our own IP address 🎉.
In the next section, we'll configure a firewall in CIS that only allows certain calling IP addresses.
Enable CIS firewall capabilities
CIS firewall rules offer power and flexibility by targeting HTTP traffic and applying custom criteria to block, challenge, log, or allow certain requests.
To demonstrate that a firewall actually blocks traffic, let us start with a rule that blocks just our current IP address:
-
In the CIS instance, navigate to "Security > Firewall rules" and click "Create"
-
In the "Create firewall rule" panel
-
Enter a name, e.g. "block-me"
-
Click on the Disabled toggle, to activate the rule after creation
-
Choose "Hostname" as field , "equals" as operator and as value put in the hostname, e.g. "myapp.example.com"
-
Click "And" to add a second row
-
Choose "IP source address" as field , "equals" as operator and as value put in your IP address, e.g. "158.175.119.4"
-
As response action choose "Block"
-
Complete the panel by clicking "Create"
-
Use cURL or your browser to verify whether you can still access your domain