Getting Started With Mikrotik and Terraform
Adopting your Mikrotik router to Terraform for automation and management.
After much ado and months of procrastination, we’re finally here! In this blog post I’ll be getting started with my Terraform-Mikrotik network automation. The goals for this one are not overly ambitious, but that’s not to say they’re not important. What I’m setting out to achieve is:
- Connect Terraform to Mikrotik,
- Import the default Mikrotik configuration that comes with my RB5009 into Terraform,
- Make the least amount of changes necessary to get internet access.
Again, this might not seem like much, but it will be a solid base for my network automation. At the end of this post (hopefully), my entire Mikrotik router will be terraform-managed!
I should mention that I’m completely new to MikroTik devices - this RB5009 is my first one! So I’m actually learning how to manage it as I go along. If you’re also new to MikroTik, I hope you’ll find this especially helpful since I’ll be explaining things from a beginner’s perspective rather than assuming much prior knowledge.
Prerequisites
If you want to follow along you don’t really need much for this one:
-
A MikroTik router
I am using my RB5009, but realistically, any device will do. Be advised, however, that if you are using a different device you will likely have to edit the commands and snippets I’m sharing here since our default configs will be different.
With that being said, the operations and concepts should still be applicable. -
Terraform CLI installed on your machine
You can follow the official installation guide or use whatever package manager you fancy. For this tutorial, I will once more be using
mise
to manage my dev tools:
Connecting Terraform to Mikrotik
Provider Setup
The provider I’m going to use is terraform-routeros. I will install the latest version which, at the time of writing, is 1.76.3
. To do that, I typically create a providers.tf
file in the root of my project and configure the required_providers
block there:
1
2
3
4
5
6
7
8
terraform {
required_providers {
routeros = {
source = "terraform-routeros/routeros"
version = "1.76.3"
}
}
}
With that in place, I can now initialize the workspace to pull down the provider and install it:
Initializing the Terraform workspace
Provider Configuration
By default, Mikrotik routers use an IP of
192.168.88.1
and a username ofadmin
.
Depending on your model, the password might be blank or it might be randomized. If it’s blank, well… you’ll know. If it’s randomized, it will be written on your device, on the label next to the serial number and whatnot.
With the provider installed, I need to configure it to tell it how it can connect to my router to manage it. It needs to know the IP address and the credentials to authenticate against the RouterOS API.
This is done by adding a provider
block with the required parameters which you can find in the official documentation of the provider. I typically dump that config in my providers.tf
file as well:
1
2
3
4
5
6
7
# ...
provider "routeros" {
hosturl = var.mikrotik_host_url
username = var.mikrotik_username
password = var.mikrotik_password
insecure = var.mikrotik_insecure
}
You can see here that I defined variables for all these connection parameters instead of hardcoding them in. You can do that if you want to (put your credentials in there), but I don’t really recommend to, especially if you intend to push this code to git eventually.
To be able to use these variables, I need to first define them:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
variable "mikrotik_host_url" {
type = string
sensitive = false
description = "The URL of the MikroTik device."
}
variable "mikrotik_username" {
type = string
sensitive = true
description = "The username for accessing the MikroTik device."
}
variable "mikrotik_password" {
type = string
sensitive = true
description = "The password for accessing the MikroTik device."
}
variable "mikrotik_insecure" {
type = bool
default = true
description = "Whether to allow insecure connections to the MikroTik device."
}
And now I need to tell terraform what the values for these variables actually are. I typically create a credentials.auto.tfvars
file and make sure to gitignore
it:
1
2
3
4
mikrotik_host_url = "https://192.168.88.1"
mikrotik_username = "terraform"
mikrotik_password = "terr4f0rm"
mikrotik_insecure = true
While you can continue using the
admin
user, it is best practice to log in and create a dedicated user for terraform. I personally created mine with full administrative rights.
Validating the Connection
At this point everything should be all set up and ready to go… Or so I thought.
I’ll first create a dummy resource - say a test file - to make sure commands are sent properly to the RouterOS API:
1
2
3
4
resource "routeros_file" "test" {
name = "test"
contents = "This is a test"
}
With all this in place, I should be able to run terraform apply
and see some successful output, right?
I’ll spare you the details and the troubleshooting time. It’s certificates… It’s always certificates… Unless it’s DNS, of course, but if it’s not DNS it’s definitely certificates 😅
RouterOS Setup
Terraform connects to my router via the API, which is the www-ssl
service. Not to be confounded with the api
or api-ssl
services. No no no no no. Those are, as far as I understand, the older versions of the API and www-ssl
is the newer REST API implementation which we need.
Thus, before Terraform can connect to my router, I need to ensure that the www-ssl
service is properly configured. By default, RouterOS doesn’t enable it and, more importantly, it doesn’t bind a certificate to it. The solution is simple, then. I just need to:
- Create a self-signed certificate authority
- Generate a certificate for the web interface
- Bind that certificate to the
www-ssl
service - Enable the service
- Profit???
Long story short, here are the commands I needed to run on my router to set all these up:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Create a local root certificate
[admin@MikroTik] > /certificate/add name=local-root-cert common-name=local-cert key-size=prime256v1 key-usage=key-cert-sign,crl-sign trusted=yes
# Sign the root certificate
[admin@MikroTik] > /certificate/sign local-root-cert
progress: done
# Create a certificate for the web interface
[admin@MikroTik] > /certificate/add name=webfig common-name=192.168.88.1 country=RO locality=BUC organization=MIRCEANTON unit=HOME days-valid=3650 key-size=prime256v1 key-usage=key-cert-sign,crl-sign,digital-signature,key-agreement,tls-server trusted=yes
# Sign the web interface certificate using our local CA
[admin@MikroTik] > /certificate/sign ca=local-root-cert webfig
progress: done
# Bind the certificate to the www-ssl service and configure it
[admin@MikroTik] > /ip/service/set www-ssl certificate=webfig disabled=no
# Enable the www-ssl service
[admin@MikroTik] > /ip/service/enable www-ssl
I don’t actually recommend copy-pasting these commands as they are rather specific to my setup. You can see the organization
I set to mirceanton
and so on. Take the extra 30 seconds to customize these commands to suit your setup before running them!
Note: Since we’re using a self-signed certificate, you’ll need to set
insecure = true
in your Terraform provider configuration, as we’ve already done in our setup.
Validating the Connection (again)
At this point everything should be all set up and ready to go. For real this time! To test that, I will run terraform plan
once again:
If you see output similar to the above, then it means terraform has successfully connected to your Mikrotik device, and you’re ready to move on to the next steps!
Importing the Default Config
When bringing a MikroTik device under Terraform management, you pretty much have two options:
- Reset the router and build from scratch,
- Import the existing configuration into Terraform
Each option has pros and cons, and I’m not really going to debate which is better and why.
The former option, at least in terms of the procedure, is simpler. You just reset the device (assuming it’s not already at factory settings) and start terraforming right away. The latter is a bit more involved, since you have to create all of the config from scratch but at least you don’t have to bother with importing a lot of resources in terraform.
That being said, I’ll go for the second option so that I don’t have to battle configuring my router at the same time as I am battling managing it via terraform. To be honest, I am fairly new to mikrotik devices in general, so I want to take it one step at a time. I want to onboard the default configuration and get a feeling for managing this router via terraform and I can get fancy with the config later on.
For now, let’s start importing the default configuration that came with my RB5009. While I won’t drop the entire export
here, I did save it in a gist if you’re interested.
Certificate
Let’s start off by importing the certificates we created earlier. Creating them with terraform looks something like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
resource "routeros_system_certificate" "local-root-ca-cert" {
name = "local-root-cert"
common_name = "local-cert"
key_size = "prime256v1"
key_usage = ["key-cert-sign", "crl-sign"]
trusted = true
sign {}
lifecycle {
ignore_changes = [
sign
]
}
}
resource "routeros_system_certificate" "webfig" {
name = "webfig"
common_name = "192.168.88.1"
country = "RO"
locality = "BUC"
organization = "MIRCEANTON"
unit = "HOME"
days_valid = 3650
key_usage = ["key-cert-sign", "crl-sign", "digital-signature", "key-agreement", "tls-server"]
key_size = "prime256v1"
trusted = true
sign {
ca = routeros_system_certificate.local-root-ca-cert.name
}
lifecycle {
ignore_changes = [
sign
]
}
}
There are a couple of things to note here.
First off, we’re specifically ignoring changes to the sign
status of the resources via the lifecycle
block to avoid deleting and re-creating the certificates after the import.
Secondly, while we’re specifying that the local-root-cert
is signed with no extra config, we are explicitly stating that webfig
is signed with local-root-cert
as the CA.
If I were to apply this right now, however, the operation would fail. Terraform would send the request to the ROS API to create 2 new certificates and RouterOS will complain saying that they already exist.
To fix that, I need to import them into my state. This can be done either manually by running a terraform import
command, or by adding an import
block in my terraform config.
Regardless of which option I choose, I firstly need to get the IDs of the certs.
From this output, we can see that local-root-cert
has an ID of *1
and webfig
is *2
(the “*” is actually required). To import them, I will add the following import
blocks to my certificates.tf
file:
1
2
3
4
5
6
7
8
import {
to = routeros_system_certificate.local-root-ca-cert
id = "*1"
}
import {
to = routeros_system_certificate.webfig
id = "*2"
}
Almost every resource we create today will have to be imported since we are trying to take over the default configuration. From now on, to keep things a bit more concise, I will add the command to get the resource ID from Mikrotik in the initial code snippet and then add an import
block in all my terraform configs.
This will make sure that I can just terraform apply
my code and it will automatically import all of the resources and update them if needed.
IP Services
Getting the services sorted out was, surprisingly, very simple. The official documentation has the perfect example listed so all I really had to do was to copy-paste it into my config and make some small adjustments:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
resource "routeros_ip_service" "disabled" {
for_each = { "api" = 8728, "ftp" = 21, "telnet" = 23, "www" = 80, "ssh" = 22 }
numbers = each.key
port = each.value
disabled = true
}
resource "routeros_ip_service" "enabled" {
for_each = { "winbox" = 8291 }
numbers = each.key
port = each.value
disabled = false
}
resource "routeros_ip_service" "ssl" {
for_each = { "api-ssl" = 8729, "www-ssl" = 443 }
numbers = each.key
port = each.value
tls_version = "only-1.2"
certificate = routeros_system_certificate.webfig.name
}
With this, I am making sure that all of the services I don’t need are disabled, and, most importantly, all of the services that need TLS have the webfig certificate bound to them.
These resources don’t actually need to be imported, so there’s nothing else to do here. We can safely apply this config and move on with our lives.
Bridge Interface
By default, Mikrotik creates one bridge interface per switch chip in your device. As far as I understand, this is due to performance optimizations since only one bridge per switch chip can take advantage of hardware acceleration.
There’s nothing stopping you from creating more, just know that it will likely perform poorly since traffic between them will be processed by the CPU instead of the switch chip.
Since my RB5009 has only one switch chip, it has one default bridge called… well… bridge
😅.
I can define this bridge as a terraform resource like so (note that I don’t include the “defconf” comments):
1
2
3
4
resource "routeros_interface_bridge" "bridge" {
name = "bridge"
admin_mac = "48:A9:8A:BD:AB:D5"
}
Bridge Ports
With the bridge imported, I can now move on to the bridge ports. Typically, one interface will be dedicated as a WAN port (in my case ether1
), and then all other interfaces will be added to this bridge as part of the LAN network:
In terraform terms, we can bundle together all bridge ports into a single resource with a for_each
block to keep things a bit cleaner, both for the import
part and for the actual resource definition:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import {
for_each = {
"ether2" = { id = "*0" }
"ether3" = { id = "*1" }
"ether4" = { id = "*2" }
"ether5" = { id = "*3" }
"ether6" = { id = "*4" }
"ether7" = { id = "*5" }
"ether8" = { id = "*6" }
"sfp-sfpplus1" = { id = "*7" }
}
to = routeros_interface_bridge_port.bridge_ports[each.key]
id = each.value.id
}
resource "routeros_interface_bridge_port" "bridge_ports" {
for_each = {
"ether2" = { comment = "", pvid = "1" }
"ether3" = { comment = "", pvid = "1" }
"ether4" = { comment = "", pvid = "1" }
"ether5" = { comment = "", pvid = "1" }
"ether6" = { comment = "", pvid = "1" }
"ether7" = { comment = "", pvid = "1" }
"ether8" = { comment = "", pvid = "1" }
"sfp-sfpplus1" = { comment = "", pvid = "1" }
}
bridge = routeros_interface_bridge.bridge.name
interface = each.key
comment = each.value.comment
pvid = each.value.pvid
}
IP Addresses
By default, a Mikrotik router comes configured with two IP settings:
-
Static LAN IP: This is the IP address used by devices on the local network. In my default configuration, the router assigns the LAN interface a static IP of
192.168.88.1/24
via the bridge. -
Dynamic WAN IP (DHCP Client): For external connectivity, the router typically obtains a dynamic IP address on the WAN interface through DHCP. In my particular case this won’t actually work, as I am not using DHCP, but we’ll cross that bridge when we get to it.
The IDs for the resources can be exported using the following commands:
And then we can create the resources in terraform and import them like so:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import {
to = routeros_ip_address.lan
id = "*1"
}
resource "routeros_ip_address" "lan" {
address = "192.168.88.1/24"
interface = routeros_interface_bridge.bridge.name
network = "192.168.88.0"
}
import {
to = routeros_ip_dhcp_client.wan
id = "*1"
}
resource "routeros_ip_dhcp_client" "wan" {
interface = "ether1"
}
DHCP Server
Mikrotik routers come pre-configured with a DHCP server on the LAN network so that devices connecting to them automatically receive an IP address. This setup is composed of three elements:
-
IP Pool: This defines the range of IP addresses that the DHCP server can assign to clients. In my default setup, the pool covers addresses from
192.168.88.10
to192.168.88.254
. -
DHCP Server Network: This resource specifies the network details to be handed out to DHCP clients such as the network address, gateway, and DNS server.
-
DHCP Server: This is the actual service that listens on the designated interface (in this case, the bridge) and assigns IP addresses from the defined pool.
DHCP-Server related resource IDs
Below is how you can mirror this setup in Terraform:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import {
to = routeros_ip_pool.dhcp
id = "*1"
}
resource "routeros_ip_pool" "dhcp" {
name = "default-dhcp"
ranges = ["192.168.88.10-192.168.88.254"]
}
import {
to = routeros_ip_dhcp_server_network.dhcp
id = "*1"
}
resource "routeros_ip_dhcp_server_network" "dhcp" {
address = "192.168.88.0/24"
gateway = "192.168.88.1"
dns_server = ["192.168.88.1"]
}
import {
to = routeros_ip_dhcp_server.defconf
id = "*1"
}
resource "routeros_ip_dhcp_server" "defconf" {
name = "defconf"
address_pool = routeros_ip_pool.dhcp.name
interface = routeros_interface_bridge.bridge.name
}
DNS
Mikrotik routers include a built-in DNS server by default, allowing network clients to resolve domain names without needing an external DNS resolver. Additionally, a static DNS entry (router.lan) is created so that the router itself can be easily referenced within the LAN.
To mirror that in terraform, we need the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
resource "routeros_dns" "dns-server" {
allow_remote_requests = true
servers = [ "1.1.1.1", "8.8.8.8" ]
}
import {
to = routeros_ip_dns_record.defconf
id = "*1"
}
resource "routeros_ip_dns_record" "defconf" {
name = "router.lan"
address = "192.168.88.1"
type = "A"
}
Note that here I am specifying the upstream dns servers as 1.1.1.1
and 8.8.8.8
. I think that by default Mikrotik uses the values it receives via DHCP on the WAN interface.
Interface Lists
Mikrotik routers use interface lists to group interfaces together for easier management. These lists are particularly useful when applying firewall rules, routing configurations, and other network policies.
By default, Mikrotik creates two interface lists:
- WAN: Represents the external (internet-facing) interfaces
- LAN: Represents the internal (local network) interfaces
To replicate this setup in Terraform, we need to define the interface lists like so:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import {
to = routeros_interface_list.wan
id = "*2000010"
}
resource "routeros_interface_list" "wan" {
name = "WAN"
}
import {
to = routeros_interface_list.lan
id = "*2000011"
}
resource "routeros_interface_list" "lan" {
name = "LAN"
}
Additionally, interfaces are assigned to these lists as follows:
- The bridge interface (which includes LAN ports) is added to the
LAN
list. - The ether1 interface (usually the WAN port) is added to the
WAN
list.
Interface List Member resource ID
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import {
to = routeros_interface_list_member.wan_ether1
id = "*2"
}
resource "routeros_interface_list_member" "wan_ether1" {
interface = "ether1"
list = routeros_interface_list.wan.name
}
import {
to = routeros_interface_list_member.lan_bridge
id = "*1"
}
resource "routeros_interface_list_member" "lan_bridge" {
interface = routeros_interface_bridge.bridge.name
list = routeros_interface_list.lan.name
}
These interface lists are particularly useful when configuring firewall rules. Instead of applying rules to individual interfaces, we can apply them to entire groups. This makes managing network security and policies much easier.
IPv4 Firewall
Speaking of the devil, let’s talk about firewall rules now. For the sake of keeping this post within a reasonable length (and also to hopefully hide my noob-ness), I won’t go into detail about the default firewall ruleset.
All I’m going to say here is that firewall rules are applied from the top down. This means that, when you read the list starting from the top, if traffic matches one of the rules it will get filtered accordingly. If it does not, it keeps going down the list until it either finds a match or it reaches the end.
If it reaches the end without finding a match, mikrotik has, for whatever reason, a default allow policy in place. This means that having no firewall rules will actually leave you wide open instead of completely blocked, as would be the case with other firewalls.
With that being said, here is the default ruleset for IPv4:
1
2
3
4
5
6
7
8
9
10
11
12
/ip firewall filter
add action=accept chain=input comment="defconf: accept established,related,untracked" connection-state=established,related,untracked
add action=drop chain=input comment="defconf: drop invalid" connection-state=invalid
add action=accept chain=input comment="defconf: accept ICMP" protocol=icmp
add action=accept chain=input comment="defconf: accept to local loopback (for CAPsMAN)" dst-address=127.0.0.1
add action=drop chain=input comment="defconf: drop all not coming from LAN" in-interface-list=!LAN
add action=accept chain=forward comment="defconf: accept in ipsec policy" ipsec-policy=in,ipsec
add action=accept chain=forward comment="defconf: accept out ipsec policy" ipsec-policy=out,ipsec
add action=fasttrack-connection chain=forward comment="defconf: fasttrack" connection-state=established,related hw-offload=yes
add action=accept chain=forward comment="defconf: accept established,related, untracked" connection-state=established,related,untracked
add action=drop chain=forward comment="defconf: drop invalid" connection-state=invalid
add action=drop chain=forward comment="defconf: drop all from WAN not DSTNATed" connection-nat-state=!dstnat connection-state=new in-interface-list=WAN
Importing all of this into terraform would be pretty annoying given that each rule would be an individual resource. Given that I currently don’t have internet connectivity anyway, it will be much quicker and easier to just delete all firewall rules and then re-create them from terraform.
Deleting all IPv4 firewall rules
And now to re-create them all in terraform, we need to add the following code to our firewall.tf
file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
resource "routeros_ip_firewall_filter" "accept_established_related_untracked" {
action = "accept"
chain = "input"
comment = "accept established, related, untracked"
connection_state = "established,related,untracked"
place_before = routeros_ip_firewall_filter.drop_invalid.id
}
resource "routeros_ip_firewall_filter" "drop_invalid" {
action = "drop"
chain = "input"
comment = "drop invalid"
connection_state = "invalid"
place_before = routeros_ip_firewall_filter.accept_icmp.id
}
resource "routeros_ip_firewall_filter" "accept_icmp" {
action = "accept"
chain = "input"
comment = "accept ICMP"
protocol = "icmp"
place_before = routeros_ip_firewall_filter.capsman_accept_local_loopback.id
}
resource "routeros_ip_firewall_filter" "capsman_accept_local_loopback" {
action = "accept"
chain = "input"
comment = "accept to local loopback for capsman"
dst_address = "127.0.0.1"
place_before = routeros_ip_firewall_filter.drop_all_not_lan.id
}
resource "routeros_ip_firewall_filter" "drop_all_not_lan" {
action = "drop"
chain = "input"
comment = "drop all not coming from LAN"
in_interface_list = "!LAN"
place_before = routeros_ip_firewall_filter.accept_ipsec_policy_in.id
}
resource "routeros_ip_firewall_filter" "accept_ipsec_policy_in" {
action = "accept"
chain = "forward"
comment = "accept in ipsec policy"
ipsec_policy = "in,ipsec"
place_before = routeros_ip_firewall_filter.accept_ipsec_policy_out.id
}
resource "routeros_ip_firewall_filter" "accept_ipsec_policy_out" {
action = "accept"
chain = "forward"
comment = "accept out ipsec policy"
ipsec_policy = "out,ipsec"
place_before = routeros_ip_firewall_filter.fasttrack_connection.id
}
resource "routeros_ip_firewall_filter" "fasttrack_connection" {
action = "fasttrack-connection"
chain = "forward"
comment = "fasttrack"
connection_state = "established,related"
hw_offload = "true"
place_before = routeros_ip_firewall_filter.accept_established_related_untracked_forward.id
}
resource "routeros_ip_firewall_filter" "accept_established_related_untracked_forward" {
action = "accept"
chain = "forward"
comment = "accept established, related, untracked"
connection_state = "established,related,untracked"
place_before = routeros_ip_firewall_filter.drop_invalid_forward.id
}
resource "routeros_ip_firewall_filter" "drop_invalid_forward" {
action = "drop"
chain = "forward"
comment = "drop invalid"
connection_state = "invalid"
place_before = routeros_ip_firewall_filter.drop_all_wan_not_dstnat.id
}
resource "routeros_ip_firewall_filter" "drop_all_wan_not_dstnat" {
action = "drop"
chain = "forward"
comment = "drop all from WAN not DSTNATed"
connection_nat_state = "!dstnat"
connection_state = "new"
in_interface_list = "WAN"
}
Not that, since ordering is important, we do have the
place_before
argument for each rule to ensure they end up in the correct order.
There probably is a way to make this code more efficient/clean using a loop block or a similar approach. Given how critical firewall rules are, however, and especially given the risk of locking myself out due to misconfigurations, I’ve decided to keep things dumb and define each rule as a separate resource.
NAT Configuration
Mikrotik routers use NAT (Network Address Translation) to allow devices on the internal network (LAN) to access the internet through the WAN interface. This is done using a masquerade rule, which dynamically translates private IP addresses into the router’s public IP:
To replicate this setup in Terraform, define the resource:
1
2
3
4
5
6
7
8
9
10
import {
to = routeros_ip_firewall_nat.masquerade
id = "*1"
}
resource "routeros_ip_firewall_nat" "masquerade" {
chain = "srcnat"
action = "masquerade"
ipsec_policy = "out,none"
out_interface_list = routeros_interface_list.wan.name
}
IPv6 Rules (or Lack Thereof)
You might be expecting some detailed IPv6 firewall configurations here. I am, however, going to keep this section short. I’m fully aware that some might not agree with this approach, but here we go…
Simply put, I don’t use IPv6 in my network. I have no need for it at the moment, and I don’t want to complicate things by introducing it needlessly into my setup. I may change my mind and explore it in the future, but I’m not going to bother with it right now.
With that in mind (and with the comment section now full of angry people), the solution here is simple. I’ve decided to disable IPv6 entirely. 😅
1
2
3
resource "routeros_ipv6_settings" "disable" {
disable_ipv6 = "true"
}
Miscellaneous Configurations
The default config includes a few additional settings that control network discovery and administrative access. These settings ensure that only devices within the LAN can discover and manage the router.
- Neighbor Discovery → Only devices in the LAN can see the router via Mikrotik’s Neighbor Discovery Protocol (MNDP).
- MAC Server → Restricts access to the router’s MAC-based login services (used for debugging and management).
- Winbox MAC Access → Limits MAC-based access via Winbox (Mikrotik’s GUI management tool) to LAN devices.
1
2
3
/ip neighbor discovery-settings set discover-interface-list=LAN
/tool mac-server set allowed-interface-list=LAN
/tool mac-server mac-winbox set allowed-interface-list=LAN
To mirror these settings in Terraform:
1
2
3
4
5
6
7
8
9
resource "routeros_ip_neighbor_discovery_settings" "lan_discovery" {
discover_interface_list = routeros_interface_list.lan.name
}
resource "routeros_tool_mac_server" "mac_server" {
allowed_interface_list = routeros_interface_list.lan.name
}
resource "routeros_tool_mac_server_winbox" "winbox_mac_access" {
allowed_interface_list = routeros_interface_list.lan.name
}
For a nice change of scenery here, these resources don’t need to be imported. We can simply create them and the ROS API will modify the config as needed.
Modifying the Default Config
I mentioned in the introduction of this blog post that I want to make as few changes as possible to the default config in order to get internet access. Well… I lied. I am also going to make 2 additions to this configuration that are not strictly speaking required but are things I typically do on any machine once I start managing it.
Basic System Settings
Setting the hostname - or identity, as Mikrotik calls it - and the timezone of a machine are basic things I do every single time I get a new computer in my lab. Fortunately, the RouterOS API exposes these settings and the terraform provider implements them. I will set my hostname to Router
(very clever, I know 😉) and the timezone to Europe/Bucharest
:
1
2
3
4
5
6
7
resource "routeros_system_identity" "identity" {
name = "Router"
}
resource "routeros_system_clock" "timezone" {
time_zone_name = "Europe/Bucharest"
time_zone_autodetect = false
}
PPPoE Config
I mentioned a couple of time throughout this process that the default DHCP client on the WAN does not work for me. This is because my ISP uses PPPoE to assign me an IP address.
This means that in order to get an IP address and be able to access the internet I need to:
- Remove DHCP Client,
- Create a PPPoE Client Interface (with credentials),
- Add PPPoE to the WAN interface list.
The first step is rather easy. Assuming I delete the code for the DHCP client, terraform will simply remove that config from my router on the next apply
command.
As for the second step, I need to create a PPPoE client and configure it to use my credentials. Just as I did when I had to specify my ROS credentials, I will configure my username and password as variables like so:
1
2
3
4
5
6
7
8
9
10
11
# ...
variable "digi_pppoe_username" {
type = string
sensitive = true
description = "The PPPoE username for the Digi connection."
}
variable "digi_pppoe_password" {
type = string
sensitive = true
description = "The PPPoE password for the Digi connection."
}
Now I can add them to my credentials.auto.tfvars
file I mentioned previously and reference them in the actual terraform resource:
1
2
3
4
5
6
7
8
resource "routeros_interface_pppoe_client" "digi" {
interface = "ether1"
name = "PPPoE-Digi"
add_default_route = true
use_peer_dns = false
password = var.digi_pppoe_password
user = var.digi_pppoe_username
}
Finally, I want to make sure to add this pppoe interface to my “WAN” interface list. This ensures that my firewall and NAT rules still apply to the new WAN connection:
1
2
3
4
resource "routeros_interface_list_member" "pppoe_wan" {
interface = routeros_interface_pppoe_client.digi.name
list = routeros_interface_list.wan.name
}
Wrapping Up
At this point, I think I covered the basics and set up a fully functional, automated MikroTik configuration using Terraform. I can access the internet again and all of my config is defined as code. Let’s run one glorious terraform apply
command to see all resources being imported/created and/or updated:
Of course, this is just the beginning. Sure, I have a solid base that got me up and running, but there’s plenty of room for refinement. In a future update, I’ll dive deeper into VLANs and firewall rules as I flesh out my network further. For now, though, I think I managed to achieve the goals I set out to, so I’m calling it a win.
Stay tuned for the next iteration. Things are only going to get more interesting from here!