Goal
You want the OCI load balancer to handle plumbing that usually ends up in every backend: redirect port 80 to HTTPS, add Strict-Transport-Security, hide the backend Server header, and lock an internal listener down to known CIDRs. Rule sets do all of this on HTTP and HTTPS listeners. They don't apply to TCP listeners or to the Network Load Balancer.
Two behaviours catch people out:
oci lb rule-set updatereplaces the whole item list. Any rule you leave out is deleted.- An
ALLOWrule turns the listener into deny by default. Traffic that doesn't match is rejected.
1. Inventory what's there
LB=ocid1.loadbalancer.oc1..xxxx
oci lb rule-set list --load-balancer-id $LB --output json
oci lb load-balancer get --load-balancer-id $LB \
--query 'data.listeners.* | [].{name:name,port:port,proto:protocol,backend:"default-backend-set-name",rules:"rule-set-names"}' \
--output table
Save the full listener definitions before you change anything. You'll need them to roll back:
oci lb load-balancer get --load-balancer-id $LB --query 'data.listeners' > listeners-before.json
Action reference
| Action | Use it for | Key fields |
|---|---|---|
REDIRECT | HTTP→HTTPS, old path→new path | conditions (PATH), redirectUri, responseCode (301, 302, 303, 307, 308) |
ADD_HTTP_RESPONSE_HEADER / ADD_HTTP_REQUEST_HEADER | HSTS, tracing or identity headers | header, value (replaces an existing header of the same name) |
REMOVE_HTTP_RESPONSE_HEADER / REMOVE_HTTP_REQUEST_HEADER | Hide Server, X-Powered-By | header |
EXTEND_HTTP_*_HEADER_VALUE | Prefix or suffix an existing value | header, prefix, suffix |
ALLOW | IP or VCN allow list | conditions (SOURCE_IP_ADDRESS, SOURCE_VCN_ID, SOURCE_VCN_IP_ADDRESS), description |
CONTROL_ACCESS_USING_HTTP_METHODS | Allow only GET/POST/... | allowedMethods, statusCode (default 405) |
HTTP_HEADER | Large headers or invalid characters | httpLargeHeaderSizeInKb (8/16/32/64), areInvalidCharactersAllowed |
IP_BASED_MAX_CONNECTIONS | Per-client connection caps | defaultMaxConnections, ipMaxConnections |
Limits: up to 20 rules in one rule set and 50 rules per load balancer. Rule sets belong to a single load balancer and can't be shared. Check the current service limits before you design around them.
The CLI accepts camelCase JSON. To get a skeleton for your CLI version, run oci lb rule-set create --generate-param-json-input items.
2. HTTP-to-HTTPS redirect
redirect-https.json:
[
{
"action": "REDIRECT",
"conditions": [
{ "attributeName": "PATH", "attributeValue": "/", "operator": "PREFIX_MATCH" }
],
"redirectUri": {
"protocol": "HTTPS",
"host": "{host}",
"port": 443,
"path": "{path}",
"query": "{query}"
},
"responseCode": 301
}
]
oci lb rule-set create --load-balancer-id $LB --name redirect_https \
--items file://redirect-https.json --wait-for-state SUCCEEDED
The {host}, {path} and {query} tokens are case sensitive and keep the incoming values. If you don't change at least one component (here the protocol and port), the client loops on redirects. A listener can have only one redirect rule per incoming path, and the incoming path condition can't include a query string.
Tip
Start with
302while you test, and switch to301or308once it works. Browsers cache permanent redirects aggressively, so a mistake with 301 sticks on clients after you fix it. Use307or308if clients send POSTs to the HTTP endpoint and must keep the method.
3. Security headers on the HTTPS listener
https-headers.json:
[
{
"action": "ADD_HTTP_RESPONSE_HEADER",
"header": "Strict-Transport-Security",
"value": "max-age=31536000; includeSubDomains"
},
{
"action": "ADD_HTTP_RESPONSE_HEADER",
"header": "X-Content-Type-Options",
"value": "nosniff"
},
{ "action": "REMOVE_HTTP_RESPONSE_HEADER", "header": "Server" },
{ "action": "REMOVE_HTTP_RESPONSE_HEADER", "header": "X-Powered-By" },
{
"action": "CONTROL_ACCESS_USING_HTTP_METHODS",
"allowedMethods": ["GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"],
"statusCode": 405
}
]
oci lb rule-set create --load-balancer-id $LB --name https_headers \
--items file://https-headers.json --wait-for-state SUCCEEDED
Header values can't contain $ or {variable} placeholders. Only one method-control list is allowed per listener. Rejected methods never reach the backend.
Warning
Once browsers have cached HSTS, they refuse plain HTTP to that host for the whole
max-age. Only addincludeSubDomainsif every subdomain serves valid HTTPS. Start with a shortmax-age(for example 300) and raise it after a day.
4. IP allow list for an internal or admin listener
allow-corp.json:
[
{
"action": "ALLOW",
"description": "Corporate egress and VPN pool",
"conditions": [ { "attributeName": "SOURCE_IP_ADDRESS", "attributeValue": "203.0.113.0/24" } ]
},
{
"action": "ALLOW",
"description": "On-prem via FastConnect",
"conditions": [ { "attributeName": "SOURCE_IP_ADDRESS", "attributeValue": "10.20.0.0/16" } ]
}
]
oci lb rule-set create --load-balancer-id $LB --name allow_corp \
--items file://allow-corp.json --wait-for-state SUCCEEDED
Caution
When this rule set is attached, the listener rejects every source that isn't listed. Before you attach it, include health check and monitoring sources, partner IPs, and any NAT addresses your users come through. Attach it to one listener first and test from outside the list.
ALLOW rules complement NSG and security list rules; they don't replace them. Keep the network rules in place, and use ALLOW where you need listener-level control, for example a shared load balancer with one public listener and one restricted listener.
5. Attach rule sets to listeners
listener update requires the listener's core settings, and --rule-set-names is the complete list for that listener. Copy every value from listeners-before.json:
# Port 80 listener: redirect only
oci lb listener update --load-balancer-id $LB --listener-name http-80 \
--default-backend-set-name bs-app --port 80 --protocol HTTP \
--rule-set-names '["redirect_https"]' \
--force --wait-for-state SUCCEEDED
Before you run the update on an HTTPS listener, pass its existing SSL settings as well: certificate name or IDs, verify-peer, cipher suite. Pass the hostnames, path route set, routing policy and idle timeout too. Any optional value you leave out may be reset. If you're not sure you've captured them all, attach rule sets in the console (Listeners → Edit → Rule sets). That changes only the rule-set selection.
6. Test
curl -sI http://app.example.com/login?x=1 | grep -iE '^(HTTP|location)'
# expect 301 and Location: https://app.example.com:443/login?x=1
curl -sI https://app.example.com/ | grep -iE 'strict-transport|server|x-powered'
curl -s -o /dev/null -w '%{http_code}\n' -X TRACE https://app.example.com/ # expect 405
To test the allow list, run curl from a source outside the list (Cloud Shell has its own public egress IP). You should see a rejection, while a listed source gets a normal response.
7. Change a rule set safely
update overwrites the item list. Fetch the current items, edit them, and send the whole list back:
oci lb rule-set get --load-balancer-id $LB --rule-set-name https_headers \
--query 'data.items' > items.json
# edit items.json: add or remove entries, keep the rest
oci lb rule-set update --load-balancer-id $LB --rule-set-name https_headers \
--items file://items.json --force --wait-for-state SUCCEEDED
The get output uses kebab-case keys. If update rejects the file, convert the keys to camelCase or regenerate the item list from your source-controlled JSON. Source control is the better habit in any case.
To roll back quickly, detach the rule set from the listener (or reattach the previous list). Delete it only after it's detached:
oci lb rule-set delete --load-balancer-id $LB --rule-set-name allow_corp --force
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Browser shows "too many redirects" | Redirect attached to the HTTPS listener too, or nothing changed in redirectUri | Attach redirect only to the port 80 listener, and set protocol and port |
| Everyone gets rejected after a change | ALLOW rule set attached without all source CIDRs | Detach it, add the missing CIDRs, reattach |
| A header rule vanished | rule-set update sent a partial list | Restore from source control and send the full list |
| Other listener settings changed after attach | listener update reset optional fields | Reapply from listeners-before.json, or use the console |
| Backend still sees the header you removed | REMOVE_HTTP_RESPONSE_HEADER used for a request header | Use REMOVE_HTTP_REQUEST_HEADER |
| 400 errors with large cookies | Default header buffer too small | Add HTTP_HEADER with a larger httpLargeHeaderSizeInKb |
Prevent it next time
- Keep rule sets in Terraform (
oci_load_balancer_rule_set) or in JSON in source control. Never hand-edit the only copy. - Before every
listener update, save the listener JSON to the change ticket. - Name rule sets by purpose (
redirect_https,https_headers,allow_corp) so the listener view explains itself. - Run the curl checks in a synthetic monitor so a missing redirect or HSTS header raises an alarm.