-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathconfigure_firewall.rs
More file actions
144 lines (134 loc) · 5.14 KB
/
configure_firewall.rs
File metadata and controls
144 lines (134 loc) · 5.14 KB
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
//! UFW firewall configuration step
//!
//! This module provides the `ConfigureFirewallStep` which handles configuration
//! of UFW (Uncomplicated Firewall) on remote hosts via Ansible playbooks.
//! This step ensures that the firewall is configured with restrictive default
//! policies while maintaining SSH access to prevent lockout.
//!
//! ## Key Features
//!
//! - Configures UFW with restrictive default policies (deny incoming, allow outgoing)
//! - Preserves SSH access on the configured port
//! - Uses Tera template for dynamic SSH port resolution
//! - Comprehensive SSH lockout prevention measures
//! - Verification steps to ensure firewall is active and SSH is accessible
//!
//! ## Configuration Process
//!
//! The step executes the "configure-firewall" Ansible playbook which handles:
//! - UFW installation and setup
//! - Reset UFW to clean state
//! - Set restrictive default policies
//! - Allow SSH access BEFORE enabling firewall (critical for preventing lockout)
//! - Enable UFW firewall
//! - Verify firewall status and SSH access
//!
//! ## SSH Lockout Prevention
//!
//! This is a **high-risk operation** that could result in SSH lockout if not
//! handled correctly. Safety measures include:
//!
//! 1. **Correct Sequencing**: SSH rules are added BEFORE enabling firewall
//! 2. **Dual SSH Protection**: Both port-specific and service-name rules
//! 3. **Port Configuration**: Uses actual SSH port from user configuration
//! 4. **Verification Steps**: Ansible tasks verify SSH access is preserved
//! 5. **Comprehensive Logging**: Detailed logging of each firewall step
use std::sync::Arc;
use tracing::{info, instrument, warn};
use crate::adapters::ansible::AnsibleClient;
use crate::shared::command::CommandError;
/// Step that configures UFW firewall on a remote host via Ansible
///
/// This step configures a restrictive UFW firewall policy while ensuring
/// SSH access is maintained. The SSH port is resolved during template rendering
/// and embedded in the final Ansible playbook. The configuration follows the
/// principle of "allow SSH BEFORE enabling firewall" to prevent lockout.
pub struct ConfigureFirewallStep {
ansible_client: Arc<AnsibleClient>,
}
impl ConfigureFirewallStep {
/// Create a new firewall configuration step
///
/// # Arguments
///
/// * `ansible_client` - Ansible client for running playbooks
///
/// # Note
///
/// SSH port configuration is resolved during template rendering phase,
/// not at step execution time. The rendered playbook contains the
/// resolved SSH port value.
#[must_use]
pub fn new(ansible_client: Arc<AnsibleClient>) -> Self {
Self { ansible_client }
}
/// Execute the firewall configuration
///
/// # Safety
///
/// This method is designed to prevent SSH lockout by:
/// 1. Resetting UFW to clean state
/// 2. Allowing SSH access BEFORE enabling firewall
/// 3. Using the correct SSH port from user configuration
///
/// The SSH port is resolved during template rendering and embedded in the
/// playbook, so this method executes a playbook with pre-configured values.
///
/// # Errors
///
/// Returns `CommandError` if:
/// - Ansible playbook execution fails
/// - UFW commands fail
/// - SSH rules cannot be applied
/// - Firewall verification fails
#[instrument(
name = "configure_firewall",
skip_all,
fields(step_type = "system", component = "firewall", method = "ansible")
)]
pub fn execute(&self) -> Result<(), CommandError> {
warn!(
step = "configure_firewall",
action = "configure_ufw",
"Configuring UFW firewall with variables from variables.yml"
);
// Run Ansible playbook with variables file
// Note: The @ symbol in Ansible means "load variables from this file"
// Equivalent to: ansible-playbook -e @variables.yml configure-firewall.yml
match self
.ansible_client
.run_playbook("configure-firewall", &["-e", "@variables.yml"])
{
Ok(_) => {
info!(
step = "configure_firewall",
status = "success",
"UFW firewall configured successfully with SSH access preserved"
);
Ok(())
}
Err(e) => {
// Propagate errors to the caller. Tests that run in container environments
// should explicitly opt-out of running this step (for example via an
// environment variable) instead of relying on runtime error detection.
Err(e)
}
}
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use std::sync::Arc;
use super::*;
#[test]
fn it_should_create_configure_firewall_step() {
let ansible_client = Arc::new(AnsibleClient::new(PathBuf::from("test_inventory.yml")));
let step = ConfigureFirewallStep::new(ansible_client);
// Test that the step can be created successfully
assert_eq!(
std::mem::size_of_val(&step),
std::mem::size_of::<Arc<AnsibleClient>>()
);
}
}