Resources for the dmarc reports service.

This commit is contained in:
Andrea Dell'Amico 2026-09-29 16:29:22 +02:00
parent cd25846b38
commit 4082c8f573
Signed by: adellam
GPG Key ID: 147ABE6CEB9E20FF
9 changed files with 465 additions and 0 deletions

View File

@ -0,0 +1,133 @@
#
# DMARC reports service: parsedmarc reads the aggregate and forensic reports
# from the IMAP mailbox, OpenSearch stores them, OpenSearch Dashboards shows
# them. All three on one VM.
#
# One interface, on the main private network: the L7 load balancers reach
# Dashboards there, and the VM reaches the IMAP server through the router of
# the project. No floating IP.
#
locals {
# One rule per (load balancer, service port) pair
dmarc_reports_service_rules = {
for pair in setproduct(var.haproxy_l7_ip, var.dmarc_reports_data.service_ports) :
"${pair[0]}-${pair[1]}" => { address = pair[0], port = pair[1] }
}
}
#
# Traffic from the main L7 load balancers
#
resource "openstack_networking_secgroup_v2" "traffic_to_dmarc_reports" {
name = "traffic_to_dmarc_reports_from_the_main_load_balancers"
delete_default_rules = "true"
description = "Traffic from the main L7 HAPROXY load balancers to OpenSearch Dashboards"
}
resource "openstack_networking_secgroup_rule_v2" "haproxy_to_dmarc_reports" {
for_each = local.dmarc_reports_service_rules
security_group_id = openstack_networking_secgroup_v2.traffic_to_dmarc_reports.id
description = "Traffic from the HAPROXY L7 ${each.value.address} to the port ${each.value.port}"
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = each.value.port
port_range_max = each.value.port
remote_ip_prefix = "${each.value.address}/32"
}
#
# Data volume: the OpenSearch indexes. Online resize enabled, like every other
# additional volume of the project
#
resource "openstack_blockstorage_volume_v3" "dmarc_reports_data_vol" {
name = var.dmarc_reports_data.vol_data_name
description = "OpenSearch data directory of the DMARC reports service"
size = var.dmarc_reports_data.vol_data_size
volume_type = var.dmarc_reports_data.volume_type
enable_online_resize = true
}
#
# Port, declared outside the instance
#
resource "openstack_networking_port_v2" "dmarc_reports_main_port" {
name = "${var.dmarc_reports_data.name}-main-port"
description = "Port of the DMARC reports service on the main private network"
admin_state_up = true
network_id = var.main_private_network_id
security_group_ids = [
var.default_security_group_id,
openstack_networking_secgroup_v2.traffic_to_dmarc_reports.id,
]
fixed_ip {
subnet_id = var.main_private_subnet_id
ip_address = var.dmarc_reports_main_ip
}
}
#
# Instance
#
resource "openstack_compute_instance_v2" "dmarc_reports" {
name = var.dmarc_reports_data.name
availability_zone_hints = var.availability_zone
flavor_name = var.dmarc_reports_data.flavor
key_pair = var.ssh_key_name
block_device {
uuid = var.image.uuid
source_type = "image"
volume_size = var.dmarc_reports_data.boot_vol_size
boot_index = 0
destination_type = "volume"
delete_on_termination = false
}
network {
port = openstack_networking_port_v2.dmarc_reports_main_port.id
}
user_data = file(var.image.user_data_file)
# Do not replace the instance when the ssh key or the user data change
lifecycle {
ignore_changes = [
key_pair, user_data, network
]
}
}
resource "openstack_compute_volume_attach_v2" "dmarc_reports_data_attach" {
instance_id = openstack_compute_instance_v2.dmarc_reports.id
volume_id = openstack_blockstorage_volume_v3.dmarc_reports_data_vol.id
device = var.dmarc_reports_data.vol_data_device
}
#
# A record on the main network address, so that the name can be used by the
# playbooks and by the load balancer configuration
#
resource "openstack_dns_recordset_v2" "dmarc_reports_recordset" {
zone_id = var.dns_zone_id
name = "${var.dmarc_reports_data.name}.${var.dns_zone_name}"
description = "Address of the DMARC reports service on the main private network"
ttl = 8600
type = "A"
records = [var.dmarc_reports_main_ip]
}
#
# Public name of the service, a CNAME of the main load balancer that publishes
# it
#
resource "openstack_dns_recordset_v2" "dmarc_reports_public_recordset" {
count = length(var.dmarc_reports_public_name) > 0 ? 1 : 0
zone_id = var.dns_zone_id
name = "${var.dmarc_reports_public_name}.${var.dns_zone_name}"
description = "DMARC reports, published by the main load balancer"
ttl = 8600
type = "CNAME"
records = [var.dmarc_reports_cname_target]
}

View File

@ -0,0 +1,35 @@
output "dmarc_reports_data" {
description = "The input data, re-exported for the dependent workspaces"
value = var.dmarc_reports_data
}
output "dmarc_reports_instance_id" {
value = openstack_compute_instance_v2.dmarc_reports.id
}
output "dmarc_reports_server_name" {
value = openstack_compute_instance_v2.dmarc_reports.name
}
output "dmarc_reports_main_ip" {
description = "Address on the main private network, used by the ansible inventory"
value = var.dmarc_reports_main_ip
}
output "dmarc_reports_data_volume_id" {
value = openstack_blockstorage_volume_v3.dmarc_reports_data_vol.id
}
output "traffic_to_dmarc_reports_security_group_id" {
value = openstack_networking_secgroup_v2.traffic_to_dmarc_reports.id
}
output "dmarc_reports_hostname" {
description = "Internal name, on the main private network address"
value = openstack_dns_recordset_v2.dmarc_reports_recordset.name
}
output "dmarc_reports_public_hostname" {
description = "Public name, a CNAME of the main load balancer"
value = length(var.dmarc_reports_public_name) > 0 ? openstack_dns_recordset_v2.dmarc_reports_public_recordset[0].name : ""
}

View File

@ -0,0 +1,10 @@
# Define required providers
terraform {
required_version = ">= 0.14.0"
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = ">= 2.0.0"
}
}
}

View File

@ -0,0 +1,96 @@
#
# DMARC reports service: parsedmarc, OpenSearch and OpenSearch Dashboards on
# one VM.
#
# Sizing and service ports belong to the service, so they have defaults here.
# The address on the main private network does not: it is part of the address
# plan of the project, and it is passed in by the caller.
#
variable "dmarc_reports_data" {
description = "Instance, volume and ports of the DMARC reports service. m1.large is RAM 8 - VCPUs 4"
type = object({
name = optional(string, "opensearch-dmarc")
description = optional(string, "DMARC reports: parsedmarc, OpenSearch and OpenSearch Dashboards")
flavor = optional(string, "m1.large")
boot_vol_size = optional(number, 20)
# OpenSearch data directory. SSD, as every volume that holds an index
vol_data_name = optional(string, "opensearch-dmarc-data")
vol_data_size = optional(number, 50)
vol_data_device = optional(string, "/dev/vdb")
volume_type = optional(string, "CephSSD")
# OpenSearch Dashboards, the only port the load balancers reach. It
# terminates TLS itself with the certificate of the internal CA
service_ports = optional(list(number), [5601])
})
default = {}
}
# Part of the address plan of the project: no default on purpose
variable "dmarc_reports_main_ip" {
type = string
description = "Address of the instance on the main private network"
}
# Data that comes from the network/DNS and project setup workspaces
variable "main_private_network_id" {
type = string
description = "ID of the main private network of the project"
}
variable "main_private_subnet_id" {
type = string
description = "ID of the main private subnet of the project"
}
variable "default_security_group_id" {
type = string
description = "ID of the 'default_for_all' security group of the project"
}
variable "haproxy_l7_ip" {
type = list(string)
description = "Addresses of the L7 HAPROXY load balancers, the only ones allowed to reach the service"
}
variable "availability_zone" {
type = string
description = "Availability zone hint of the instance"
}
variable "image" {
description = "Image of the instance: uuid and cloud-init user data file"
type = object({
uuid = string
user_data_file = string
})
}
variable "ssh_key_name" {
type = string
description = "Name of the SSH key pair injected by cloud-init"
}
# DNS. The A record on the main network address is always created; the public
# name is a CNAME of the load balancer that publishes the service
variable "dns_zone_id" {
type = string
description = "ID of the DNS zone of the project"
}
variable "dns_zone_name" {
type = string
description = "Name of the DNS zone of the project, with the trailing dot"
}
variable "dmarc_reports_public_name" {
type = string
default = "dmarc"
description = "Left part of the public name, a CNAME of the load balancer. Empty means no record"
}
variable "dmarc_reports_cname_target" {
type = string
default = ""
description = "Target of the CNAME, usually the name of the main load balancer, with the trailing dot"
}

View File

@ -0,0 +1,57 @@
# DMARC reports service of the S2I2S project
One VM, `m1.large` (RAM 8 - VCPUs 4), Ubuntu 24.04, 20 GB of root disk, on the
main private network only (`10.10.0.166`), and **one 50 GB SSD volume**
(`CephSSD`, `enable_online_resize`) on `/dev/vdb`, for the OpenSearch indexes.
It runs parsedmarc, OpenSearch and OpenSearch Dashboards. parsedmarc reads the
aggregate and failure reports sent to `dmarc-reports@isti.cnr.it` (the `rua`
and `ruf` of `_dmarc.isti.cnr.it`) over IMAP, through the router of the
project: no floating IP.
The resources live in [`../../modules/dmarc_reports`](../../modules/dmarc_reports),
which carries the sizing and the service port as defaults. Only the address on
the main private network, from the address plan in [`../variables`](../variables)
(`basic_services_ip.dmarc_reports`), and the IDs read from the other
workspaces are set in `main.tf`.
Security groups on the port:
* `default_for_all`;
* `traffic_to_dmarc_reports_from_the_main_load_balancers`: **5601**
(OpenSearch Dashboards, TLS with the certificate of the internal CA) from
each L7 load balancer.
OpenSearch itself (9200) is not reachable from outside the VM. The Grafana
server that will read the indexes (`public_grafana_server_cidr` in
`../variables`) is a separate activity: it will need either a rule here or a
service on the load balancers.
## Names
| Name | Type |
|---|---|
| `opensearch-dmarc.s2i2s.cloud.isti.cnr.it` | A → `10.10.0.166`, used by the playbooks and by the load balancer |
| `dmarc.s2i2s.cloud.isti.cnr.it` | CNAME → `main-lb.s2i2s.cloud.isti.cnr.it.` |
The public name is served by the L7 load balancers: the `dmarc_reports` entry
of `haproxy_l7_services` in `group_vars/main_haproxy_l7/main_haproxy_l7.yml` of
`infrastructure-playbooks`.
## Order of the applies
```
main_net_dns_router -> project-setup -> dmarc-reports
```
```bash
tofu init
tofu plan -out=dmarc-reports.plan
tofu apply dmarc-reports.plan
```
Then regenerate the ansible inventory in `infrastructure-playbooks`:
```bash
ansible-playbook tofu-inventory.yml --diff
```

View File

@ -0,0 +1,80 @@
# DMARC reports service of the S2I2S OpenStack project: parsedmarc, OpenSearch
# and OpenSearch Dashboards on one VM.
#
# The resources are in ../../modules/dmarc_reports, which also carries the
# sizing (m1.large: RAM 8 - VCPUs 4, 50 GB of SSD for the indexes) and the
# service port. Only what belongs to this project is set here: the address on
# the main private network, taken from the address plan in ../variables, and the
# IDs that come from the other workspaces.
#
# Apply order: main_net_dns_router -> project-setup -> this one.
data "terraform_remote_state" "privnet_dns_router" {
backend = "local"
config = {
path = "../main_net_dns_router/terraform.tfstate"
}
}
data "terraform_remote_state" "project_setup" {
backend = "local"
config = {
path = "../project-setup/terraform.tfstate"
}
}
module "labs_common_variables" {
source = "../../modules/labs_common_variables"
}
module "project_variables" {
source = "../variables"
}
module "ssh_settings" {
source = "../../modules/ssh-key-ref"
}
locals {
# From the network/DNS state
dns_zone = data.terraform_remote_state.privnet_dns_router.outputs.dns_zone
dns_zone_id = data.terraform_remote_state.privnet_dns_router.outputs.dns_zone_id
main_private_network_id = data.terraform_remote_state.privnet_dns_router.outputs.main_private_network_id
main_private_subnet_id = data.terraform_remote_state.privnet_dns_router.outputs.main_subnet_network_id
# From the project setup state
default_security_group_id = data.terraform_remote_state.project_setup.outputs.default_security_group_id
main_haproxy_l7_ip = data.terraform_remote_state.project_setup.outputs.main_haproxy_l7_ip
main_loadbalancer_name = data.terraform_remote_state.project_setup.outputs.main_loadbalancer_hostname
# From the common and project variables
availability_zone = module.labs_common_variables.availability_zones_names.availability_zone_no_gpu
ubuntu_2404 = module.labs_common_variables.ubuntu_2404
ubuntu2404_data_file = module.labs_common_variables.ubuntu2404_data_file
basic_services_ip = module.project_variables.basic_services_ip
}
module "dmarc_reports" {
source = "../../modules/dmarc_reports"
# Address plan of the project. The sizing comes from the module defaults
dmarc_reports_main_ip = local.basic_services_ip.dmarc_reports
main_private_network_id = local.main_private_network_id
main_private_subnet_id = local.main_private_subnet_id
default_security_group_id = local.default_security_group_id
haproxy_l7_ip = local.main_haproxy_l7_ip
availability_zone = local.availability_zone
image = {
uuid = local.ubuntu_2404.uuid
user_data_file = local.ubuntu2404_data_file
}
ssh_key_name = module.ssh_settings.ssh_key_name
# dmarc.s2i2s.cloud.isti.cnr.it, a CNAME of the main load balancer
dns_zone_id = local.dns_zone_id
dns_zone_name = local.dns_zone.name
dmarc_reports_public_name = "dmarc"
dmarc_reports_cname_target = local.main_loadbalancer_name
}

View File

@ -0,0 +1,39 @@
output "dmarc_reports_instance_id" {
value = module.dmarc_reports.dmarc_reports_instance_id
}
output "dmarc_reports_server_name" {
value = module.dmarc_reports.dmarc_reports_server_name
}
output "dmarc_reports_server_data" {
value = module.dmarc_reports.dmarc_reports_data
}
output "dmarc_reports_main_ip" {
description = "Address on the main private network. Used by the ansible inventory"
value = module.dmarc_reports.dmarc_reports_main_ip
}
output "dmarc_reports_data_volume_id" {
value = module.dmarc_reports.dmarc_reports_data_volume_id
}
output "traffic_to_dmarc_reports_security_group_id" {
value = module.dmarc_reports.traffic_to_dmarc_reports_security_group_id
}
output "dmarc_reports_hostname" {
description = "Internal name, on the main private network address"
value = module.dmarc_reports.dmarc_reports_hostname
}
output "dmarc_reports_public_hostname" {
description = "Public name, a CNAME of the main load balancer"
value = module.dmarc_reports.dmarc_reports_public_hostname
}
# Re-exported for the ansible inventory generator
output "dns_zone" {
value = local.dns_zone
}

View File

@ -0,0 +1,14 @@
# Define required providers
terraform {
required_version = ">= 0.14.0"
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = ">= 2.0.0"
}
}
}
provider "openstack" {
cloud = "s2i2s"
}

File diff suppressed because one or more lines are too long