An Auditor’s Checklist for Securing C Backends on Linode
System Hardening: Kernel Parameters and sysctl Configuration
Securing a C backend on Linode begins with a robust foundation. A critical, often overlooked, aspect is the hardening of the Linux kernel itself. This involves tuning various `sysctl` parameters to reduce the attack surface and mitigate common network-based threats. For a C application, especially one handling network I/O or sensitive data, these settings are paramount.
We’ll focus on parameters that enhance network security, prevent information leakage, and improve system stability under load. The primary configuration file for these settings is typically /etc/sysctl.conf. Changes made here are persistent across reboots. For immediate effect without rebooting, you can use the sysctl -p command after editing the file.
Network Stack Hardening
The network stack is a prime target. We need to disable IP forwarding if the Linode is not acting as a router, mitigate SYN flood attacks, and prevent IP spoofing.
Add the following lines to /etc/sysctl.conf:
# Disable IP forwarding net.ipv4.ip_forward = 0 net.ipv6.conf.all.forwarding = 0 # Mitigate SYN flood attacks net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_max_syn_backlog = 2048 net.ipv4.tcp_synack_retries = 3 net.ipv4.tcp_syn_retries = 3 # Prevent IP spoofing net.ipv4.conf.all.rp_filter = 1 net.ipv4.conf.default.rp_filter = 1 net.ipv4.conf.all.accept_source_route = 0 net.ipv6.conf.all.accept_source_route = 0 net.ipv4.conf.all.accept_redirects = 0 net.ipv6.conf.all.accept_redirects = 0 net.ipv4.conf.all.secure_redirects = 0 net.ipv4.conf.default.secure_redirects = 0 # Disable ICMP redirects net.ipv4.conf.all.send_redirects = 0 net.ipv4.conf.default.send_redirects = 0
After saving /etc/sysctl.conf, apply the changes:
sudo sysctl -p
Memory and Process Hardening
To prevent certain types of memory exhaustion attacks and information disclosure, we can tune memory-related parameters.
Add these to /etc/sysctl.conf:
# Prevent kernel pointer leaks in error messages kernel.kptr_restrict = 2 # Reduce the likelihood of DoS by limiting memory allocation vm.overcommit_memory = 2 vm.overcommit_ratio = 50 # Limit the number of processes that can be created kernel.pid_max = 32768
Apply these changes as well:
sudo sysctl -p
User and Group Management: Principle of Least Privilege
Your C backend application should never run as the root user. This is a fundamental security principle. Create a dedicated, unprivileged user and group for your application. This minimizes the damage an exploited application could cause.
Creating a Dedicated User and Group
Let’s assume your application will be named ‘my_c_app’. We’ll create a user ‘my_c_app’ and a group ‘my_c_app’.
# Create the group sudo groupadd my_c_app # Create the user, assign to the group, and set home directory (optional, but good practice) # -r: create a system user # -g: primary group # -s: default shell (set to /usr/sbin/nologin for security if no shell access is needed) # -M: do not create home directory (if not needed) sudo useradd -r -g my_c_app -s /usr/sbin/nologin -M my_c_app
Ensure your application’s executable and any necessary data files are owned by this user and group, with restrictive permissions.
# Example: If your app binary is at /opt/my_c_app/bin/my_c_app sudo chown -R my_c_app:my_c_app /opt/my_c_app sudo chmod 750 /opt/my_c_app/bin/my_c_app # Execute for owner, read/execute for group, no access for others sudo chmod 750 /opt/my_c_app/data # Example data directory
Restricting Shell Access
As demonstrated with -s /usr/sbin/nologin, the user should not have interactive shell access. If you need to grant limited shell access for specific administrative tasks, consider using tools like sudo with carefully defined commands, or a restricted shell environment.
Firewall Configuration: Linode Cloud Firewall and iptables
A multi-layered approach to network access control is essential. Linode’s Cloud Firewall provides a convenient, managed layer of protection at the network edge. For finer-grained control or specific application-level filtering, iptables (or its successor nftables) on the Linode itself is indispensable.
Linode Cloud Firewall Rules
Access your Linode Cloud Firewall settings via the Linode Cloud Manager. The principle here is to deny all by default and explicitly allow only necessary traffic.
- Inbound Rules:
- Allow SSH (TCP port 22) only from trusted IP addresses (e.g., your office IP, bastion host IP).
- Allow the specific port(s) your C backend listens on (e.g., TCP port 8080) from anywhere (0.0.0.0/0) if it’s a public service, or from specific IPs if it’s internal.
- Deny all other inbound traffic.
- Outbound Rules:
- Allow essential outbound traffic (e.g., DNS on UDP/TCP port 53, NTP on UDP port 123).
- If your C backend needs to connect to external services (e.g., databases, APIs), explicitly allow those destination IPs and ports.
- Deny all other outbound traffic.
Auditor’s Note: Document all firewall rules, including the justification for each allowed port and protocol. Regularly review these rules for necessity and correctness.
iptables Configuration
While the Cloud Firewall is effective, iptables offers more granular control and can act as a fallback or supplementary layer. It’s crucial to manage iptables rules carefully to avoid locking yourself out.
A common strategy is to use a script to load a predefined set of rules. First, ensure the iptables-persistent package is installed for persistence across reboots.
sudo apt update && sudo apt install iptables-persistent -y # Or for RHEL/CentOS based systems: # sudo yum install iptables-services -y # sudo systemctl enable iptables
Here’s a sample iptables script that implements a secure baseline. This script assumes your C application listens on TCP port 8080.
#!/bin/bash # Flush all existing rules iptables -F iptables -X iptables -t nat -F iptables -t nat -X iptables -t mangle -F iptables -t mangle -X # Set default policies to DROP (deny all) iptables -P INPUT DROP iptables -P FORWARD DROP iptables -P OUTPUT ACCEPT # Typically allow outbound, but can be restricted further # Allow loopback interface traffic iptables -A INPUT -i lo -j ACCEPT iptables -A OUTPUT -o lo -j ACCEPT # Allow established and related connections iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT # Allow SSH from a specific trusted IP (replace YOUR_TRUSTED_IP) # If using Linode Cloud Firewall for SSH, this might be redundant or for a different IP range. # iptables -A INPUT -p tcp --dport 22 -s YOUR_TRUSTED_IP -j ACCEPT # Allow your C application's port (e.g., TCP 8080) iptables -A INPUT -p tcp --dport 8080 -j ACCEPT # Allow essential outbound traffic (DNS, NTP) iptables -A OUTPUT -p udp --dport 53 -j ACCEPT # DNS iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT # DNS iptables -A OUTPUT -p udp --dport 123 -j ACCEPT # NTP # Log dropped packets (optional, for debugging) # iptables -A INPUT -j LOG --log-prefix "IPTables-Dropped: " --log-level 4 # iptables -A OUTPUT -j LOG --log-prefix "IPTables-Dropped: " --log-level 4 # Save the rules # For iptables-persistent: sudo netfilter-persistent save # For iptables-services: # sudo service iptables save
Auditor’s Note: The OUTPUT policy is set to ACCEPT for simplicity. For a highly secure environment, this should be changed to DROP, and only necessary outbound connections explicitly allowed. This requires careful analysis of your application’s network dependencies.
Application-Level Security: Input Validation and Error Handling
Even with robust system-level security, vulnerabilities within the C application itself can be exploited. Input validation and secure error handling are critical to prevent common attacks like buffer overflows, SQL injection (if applicable), and information disclosure.
Secure Input Handling in C
C’s low-level nature makes it susceptible to buffer overflows if not handled with extreme care. Always use bounds-checking functions and avoid deprecated, unsafe functions like gets().
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAX_BUFFER_SIZE 256
int main() {
char input_buffer[MAX_BUFFER_SIZE];
char user_input[100]; // Assume user input is expected to be less than 100 chars
printf("Enter some text: ");
// Unsafe way (vulnerable to buffer overflow)
// gets(user_input);
// Safer way using fgets
if (fgets(user_input, sizeof(user_input), stdin) == NULL) {
perror("Error reading input");
return 1;
}
// Remove trailing newline character if present
user_input[strcspn(user_input, "\n")] = 0;
// --- Input Validation ---
// Check if the input length exceeds expected limits BEFORE processing
if (strlen(user_input) >= sizeof(user_input) - 1) {
fprintf(stderr, "Error: Input too long.\n");
// Handle error: truncate, reject, or log
return 1;
}
// Further validation based on expected data type/format
// Example: If expecting only alphanumeric characters
for (int i = 0; user_input[i]; i++) {
if (!isalnum(user_input[i])) {
fprintf(stderr, "Error: Input contains invalid characters.\n");
return 1;
}
}
// --- Secure Copying ---
// Use strncpy or snprintf for safe copying into fixed-size buffers
strncpy(input_buffer, user_input, sizeof(input_buffer) - 1);
input_buffer[sizeof(input_buffer) - 1] = '\\0'; // Ensure null termination
printf("You entered: %s\n", input_buffer);
return 0;
}
Auditor’s Note: Code reviews should specifically target string manipulation functions. Tools like Valgrind and static analysis tools (e.g., Clang Static Analyzer, Cppcheck) can help identify potential buffer overflows and memory errors.
Secure Error Handling and Logging
Error messages should not reveal sensitive system information (e.g., file paths, internal logic, stack traces). Log detailed errors to a secure, centralized logging system, but provide generic, non-revealing messages to the end-user.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// Function to log errors securely
void log_error(const char* message, const char* details) {
FILE* log_file = fopen("/var/log/my_c_app_errors.log", "a"); // Ensure this path is writable ONLY by the app user
if (!log_file) {
// Cannot log to file, consider stderr or a fallback mechanism
fprintf(stderr, "CRITICAL: Failed to open log file.\n");
return;
}
time_t now = time(NULL);
char timestamp[20];
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", localtime(&now));
// Log format: Timestamp | User Context (if applicable) | Error Message | Details
fprintf(log_file, "%s | %s | %s\n", timestamp, message, details);
fclose(log_file);
}
// Example of handling a file operation error
int process_file(const char* filename) {
FILE* fp = fopen(filename, "r");
if (fp == NULL) {
// DO NOT expose errno or filename directly to the user
char error_msg[100];
snprintf(error_msg, sizeof(error_msg), "Failed to open configuration file.");
log_error("File Open Error", filename); // Log the actual filename for debugging
fprintf(stderr, "%s\n", error_msg); // Show generic error to user/stderr
return -1; // Indicate failure
}
// ... process file ...
fclose(fp);
return 0; // Success
}
int main() {
// Example usage
if (process_file("non_existent_config.txt") == -1) {
printf("Application could not start due to configuration error.\n");
}
return 0;
}
Auditor’s Note: Ensure log files are protected by file permissions and that the application user has write access only to the log file itself, not the directory containing it if possible. Centralized logging solutions (e.g., rsyslog, ELK stack) are recommended for production environments.
Secure Deployment and Configuration Management
The security of your C backend is not just about the code and the OS; it extends to how the application is deployed and configured. Linode’s infrastructure provides tools, but best practices must be followed.
Configuration Files and Secrets Management
Sensitive information like API keys, database credentials, or private keys should never be hardcoded in the C source code or stored in plain text configuration files that are world-readable. Use environment variables or a dedicated secrets management system.
# Example: Setting environment variables for a systemd service # In /etc/systemd/system/my_c_app.service file: [Unit] Description=My C Application Service After=network.target [Service] User=my_c_app Group=my_c_app WorkingDirectory=/opt/my_c_app ExecStart=/opt/my_c_app/bin/my_c_app # Load environment variables from a file EnvironmentFile=/etc/my_c_app/app.env # Or define them directly (less secure for many secrets) # Environment="DB_HOST=localhost" # Environment="DB_USER=appuser" # Environment="DB_PASS=supersecretpassword" # Avoid this for sensitive secrets [Install] WantedBy=multi-user.target
The file /etc/my_c_app/app.env should have restrictive permissions:
sudo chown root:root /etc/my_c_app/app.env sudo chmod 600 /etc/my_c_app/app.env # Only root can read/write
Your C application would then read these variables using getenv().
#include <stdlib.h>
#include <stdio.h>
int main() {
const char* db_host = getenv("DB_HOST");
const char* db_user = getenv("DB_USER");
const char* db_pass = getenv("DB_PASS");
if (!db_host || !db_user || !db_pass) {
fprintf(stderr, "Error: Missing database credentials in environment variables.\n");
return 1;
}
printf("Connecting to DB host: %s as user: %s\n", db_host, db_user);
// Use db_pass for connection...
// IMPORTANT: Do not print db_pass to logs or stdout.
return 0;
}
Auditor’s Note: For more advanced secrets management, consider solutions like HashiCorp Vault, AWS Secrets Manager (if integrating with AWS services), or Linode’s own secrets management capabilities if available and suitable.
Automated Security Updates
Keep your Linode’s operating system and all installed packages up-to-date. Unpatched vulnerabilities are a primary vector for compromise.
# For Debian/Ubuntu based systems: sudo apt update && sudo apt upgrade -y && sudo apt autoremove -y # For RHEL/CentOS based systems: # sudo yum update -y && sudo yum autoremove -y
Consider setting up automatic security updates for critical packages. On Debian/Ubuntu, this can be managed with the unattended-upgrades package.
sudo apt install unattended-upgrades -y sudo dpkg-reconfigure --priority=low unattended-upgrades
Auditor’s Note: Verify that automatic updates are configured correctly and that logs from unattended-upgrades are monitored. A compromise could occur if critical security patches are not applied promptly.