Skip to main content

torrust_tracker_deployer_lib/application/steps/system/
configure_firewall.rs

1//! UFW firewall configuration step
2//!
3//! This module provides the `ConfigureFirewallStep` which handles configuration
4//! of UFW (Uncomplicated Firewall) on remote hosts via Ansible playbooks.
5//! This step ensures that the firewall is configured with restrictive default
6//! policies while maintaining SSH access to prevent lockout.
7//!
8//! ## Key Features
9//!
10//! - Configures UFW with restrictive default policies (deny incoming, allow outgoing)
11//! - Preserves SSH access on the configured port
12//! - Uses Tera template for dynamic SSH port resolution
13//! - Comprehensive SSH lockout prevention measures
14//! - Verification steps to ensure firewall is active and SSH is accessible
15//!
16//! ## Configuration Process
17//!
18//! The step executes the "configure-firewall" Ansible playbook which handles:
19//! - UFW installation and setup
20//! - Reset UFW to clean state
21//! - Set restrictive default policies
22//! - Allow SSH access BEFORE enabling firewall (critical for preventing lockout)
23//! - Enable UFW firewall
24//! - Verify firewall status and SSH access
25//!
26//! ## SSH Lockout Prevention
27//!
28//! This is a **high-risk operation** that could result in SSH lockout if not
29//! handled correctly. Safety measures include:
30//!
31//! 1. **Correct Sequencing**: SSH rules are added BEFORE enabling firewall
32//! 2. **Dual SSH Protection**: Both port-specific and service-name rules
33//! 3. **Port Configuration**: Uses actual SSH port from user configuration
34//! 4. **Verification Steps**: Ansible tasks verify SSH access is preserved
35//! 5. **Comprehensive Logging**: Detailed logging of each firewall step
36
37use std::sync::Arc;
38use tracing::{info, instrument, warn};
39
40use crate::adapters::ansible::AnsibleClient;
41use crate::application::traits::CommandProgressListener;
42use crate::shared::command::CommandError;
43
44/// Step that configures UFW firewall on a remote host via Ansible
45///
46/// This step configures a restrictive UFW firewall policy while ensuring
47/// SSH access is maintained. The SSH port is resolved during template rendering
48/// and embedded in the final Ansible playbook. The configuration follows the
49/// principle of "allow SSH BEFORE enabling firewall" to prevent lockout.
50pub struct ConfigureFirewallStep {
51    ansible_client: Arc<AnsibleClient>,
52}
53
54impl ConfigureFirewallStep {
55    /// Create a new firewall configuration step
56    ///
57    /// # Arguments
58    ///
59    /// * `ansible_client` - Ansible client for running playbooks
60    ///
61    /// # Note
62    ///
63    /// SSH port configuration is resolved during template rendering phase,
64    /// not at step execution time. The rendered playbook contains the
65    /// resolved SSH port value.
66    #[must_use]
67    pub fn new(ansible_client: Arc<AnsibleClient>) -> Self {
68        Self { ansible_client }
69    }
70
71    /// Execute the firewall configuration
72    ///
73    /// # Arguments
74    ///
75    /// * `listener` - Optional progress listener for reporting step-level details.
76    ///   When provided, reports debug information (Ansible commands, working directory)
77    ///   and detail information (firewall policies, SSH access preservation, status).
78    ///
79    /// # Safety
80    ///
81    /// This method is designed to prevent SSH lockout by:
82    /// 1. Resetting UFW to clean state
83    /// 2. Allowing SSH access BEFORE enabling firewall
84    /// 3. Using the correct SSH port from user configuration
85    ///
86    /// The SSH port is resolved during template rendering and embedded in the
87    /// playbook, so this method executes a playbook with pre-configured values.
88    ///
89    /// # Errors
90    ///
91    /// Returns `CommandError` if:
92    /// - Ansible playbook execution fails
93    /// - UFW commands fail
94    /// - SSH rules cannot be applied
95    /// - Firewall verification fails
96    #[instrument(
97        name = "configure_firewall",
98        skip_all,
99        fields(step_type = "system", component = "firewall", method = "ansible")
100    )]
101    pub fn execute(
102        &self,
103        listener: Option<&dyn CommandProgressListener>,
104    ) -> Result<(), CommandError> {
105        warn!(
106            step = "configure_firewall",
107            action = "configure_ufw",
108            "Configuring UFW firewall with variables from variables.yml"
109        );
110
111        // Report debug information about Ansible execution
112        if let Some(l) = listener {
113            l.on_debug(&format!(
114                "Ansible working directory: {}",
115                self.ansible_client.working_dir().display()
116            ));
117            l.on_debug("Executing playbook: ansible-playbook configure-firewall.yml -e @variables.yml -i inventory.ini");
118        }
119
120        // Run Ansible playbook with variables file
121        // Note: The @ symbol in Ansible means "load variables from this file"
122        // Equivalent to: ansible-playbook -e @variables.yml configure-firewall.yml
123        match self
124            .ansible_client
125            .run_playbook("configure-firewall", &["-e", "@variables.yml"])
126        {
127            Ok(_) => {
128                // Report configuration success with details
129                if let Some(l) = listener {
130                    l.on_detail("Configuring UFW with restrictive default policies");
131                    l.on_detail("Allowing SSH access before enabling firewall");
132                    l.on_detail("Firewall status: active");
133                }
134
135                info!(
136                    step = "configure_firewall",
137                    status = "success",
138                    "UFW firewall configured successfully with SSH access preserved"
139                );
140                Ok(())
141            }
142            Err(e) => {
143                // Propagate errors to the caller. Tests that run in container environments
144                // should explicitly opt-out of running this step (for example via an
145                // environment variable) instead of relying on runtime error detection.
146                Err(e)
147            }
148        }
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use std::path::PathBuf;
155    use std::sync::Arc;
156
157    use super::*;
158
159    #[test]
160    fn it_should_create_configure_firewall_step() {
161        let ansible_client = Arc::new(AnsibleClient::new(PathBuf::from("test_inventory.yml")));
162        let step = ConfigureFirewallStep::new(ansible_client);
163
164        // Test that the step can be created successfully
165        assert_eq!(
166            std::mem::size_of_val(&step),
167            std::mem::size_of::<Arc<AnsibleClient>>()
168        );
169    }
170}