Background info
Oracle Cloud (OCI) offers a free tier for cloud resources, which I’m using for some time now. More info here. (I’m no affiliate or something like that)
I have a virtual machine with some services running for private use. It’s like a homelab server, just not at home.
I’m running a reverse proxy so that some services are intentionally made public to the world via port 443
. It also has a SSH server and other services that I don’t want to expose to the internet, but I want to have access with my IP address.
You could use overlay networks for this purpose, but I wanted to reach it via the public IP directly. It’s also possible to achieve the following with other tools or build something yourself with the OCI API, but I want to do more with Ansible, this is why I chose this solution
Security lists
OCI has virtual cloud networks (VCN
), which can have subnets. You can assign security lists to subnets, so that only specific (port based) traffic is allowed there.
This is an example of a list that allows ingress traffic on port 443
and 80
for everyone. (0.0.0.0/0
) and has no policy for egress traffic. The egress traffic is handled by another security list.
Why Ansible automation?
With Ansible you can do a lot in OCI, but I’ll limit this to just dynamically changing an existing security list.
Let’s say you made a list that allows all traffic to the server from your home IP address. It would look like this:
Now my ISP changes this IP from time to time, so I’ll need a way to update it. There would be ways to trigger actions with dynamic DNS etc., but I just wanted a simple script-like solution that runs on a server in my network.
Limitations
Be aware that this will allow all traffic for your current source NAT uplink IP. Which means if you use this on your company network or on a public hotspot, everyone using this uplink IP will have the same access.
In my case only I use the IP address for the duration of the assignment.
Requirements
Security list assignment
The security list has to be assigned to a resource, like a subnet.
OCI auth
On your Oracle Cloud profile you can add an API key. (which can be done for a user with limited rights also)
When you download the public- and private key, you’ll also get a configuration file preview. You have to add the path to the private OCI keyfile there and save it as ~/.oci/config
(Linux, macOS)
[DEFAULT]
user=ocid1.user.oc1.......
fingerprint=<fingerprint>
tenancy=ocid1.tenancy.oc1.....
region=eu-frankfurt-1
key_file=/path/to/key/file.pem
Ansible / the OCI SDK will automatically fetch the key from there later, so you don’t need auth in Ansible.
Python OCI SDK
Install the python OCI SDK, for example via
pip install oci
Ansible Collection
Install the OCI ansible collection, for example via
ansible-galaxy collection install oracle.oci
Generate Ansible galaxy role
This would work in a single playbook too, but I’m trying to stick to the Ansible galaxy role structure.
ansible-galaxy init oci-allow-my-ip
Ansible Role
Info: Of course you can also create a fresh OCI security list with Ansible, but to make this example easier, I’ll just update an existing list.
Let’s use the list from before, which has just one ingress rule, which allows my IP address everything:
At the top you can see the OCID
, which is a unique ID for a resource. In this case a security list. Because it already exists compartment_id
and vcn_id
isn’t required in the Ansible role.
File and folder structure
… with just the relevant files:
.
├── oci-allow-my-ip
│ ├── README.md
│ ├── meta
│ │ └── main.yml
│ ├── tasks
│ │ └── main.yml
│ └── vars
│ └── main.yml
└── site.yml
vars/main.yml
You can copy the OCID
from OCI and paste it in vars/main.yml
like this
oci_security_list_id: "ocid1.securitylist.oc1......."
# ... more vars
You can name oci_security_list_id
whatever you like, you just have to use the same name in the task later. You can also add other variables there, in case you need to do more (like compartment_id
or vcn_id
), but it’s not required for this example.
tasks/main.yml
I left some debug tasks and some TCP and UDP options in this example, so you may adapt this to your needs.
If you only want to allow TCP, UDP or ICMP: protocol
needs the protocol number, not the name (ICMP: 1
, ICMPv6: 58
TCP: 6
, UDP: 17
). In my example I use all
.
Docs for oracle.oci.oci_network_security_list
---
- name: Install Python OCI SDK if it's not installed
ansible.builtin.pip:
name: oci
state: present
- name: Get own public IP with ipify
community.general.ipify_facts:
register: ip_address
# - name: Debug output from what ipify returned
# ansible.builtin.debug:
# msg: "My IP address is {{ ip_address.ansible_facts.ipify_public_ip }}"
- name: Update OCI security list with current IP
oracle.oci.oci_network_security_list:
name: "allow-my-ip"
# compartment_id: "{{ oci_compartment_id }}" # <- not necessary for just updating an existing list
# vcn_id: "{{ oci_vcn_id }}" # <- not necessary for just updating an existing list
security_list_id: "{{ oci_security_list_id }}"
state: "present"
ingress_security_rules:
- description: "allow my dynamic IP"
is_stateless: false
protocol: "all"
source: "{{ ip_address.ansible_facts.ipify_public_ip }}/32"
source_type: "CIDR_BLOCK"
# tcp_options:
# destination_port_range:
# min: 7777
# max: 7777
# udp_options:
# destination_port_range:
# min: 7777
# max: 7777
egress_security_rules: [] # <- I don't want to specify egress rules, so this is an empty list, which is required then.
# description: "something"
# is_stateless: false
# protocol: "all"
# destination: "0.0.0.0/0"
# destination_type: "CIDR_BLOCK"
# tcp_options:
# destination_port_range:
# min: 8888
# max: 8888
# udp_options:
# destination_port_range:
# min: 8888
# max: 8888
# register: oci_security_rules
# - name: Dump result
# ansible.builtin.debug:
# msg: '{{ oci_security_rules }}'
...
So the version without all the debug and helper comments looks like this
---
- name: Install Python OCI SDK if it's not installed
ansible.builtin.pip:
name: oci
state: present
- name: Get own public IP with ipify
community.general.ipify_facts:
register: ip_address
- name: Update OCI security list with current IP
oracle.oci.oci_network_security_list:
name: "allow-my-ip"
security_list_id: "{{ oci_security_list_id }}"
state: "present"
ingress_security_rules:
- description: "allow my dynamic IP"
is_stateless: false
protocol: "all"
source: "{{ ip_address.ansible_facts.ipify_public_ip }}/32"
source_type: "CIDR_BLOCK"
egress_security_rules: []
...
site.yml (main playbook)
You can then execute the role from a playbook. (on a regular basis to always update your current IP)
---
- name: Main playbook for OCI Cloud
hosts: localhost
vars:
# specify your python interpreter if you use multiple
ansible_python_interpreter: "/Users/toor/.pyenv/shims/python"
gather_facts: false
roles:
- oci-allow-my-ip
# ... more roles here
...