<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Brute Force Attacks on Digi Hunch</title><link>https://www.digihunch.com/tag/brute-force-attacks/</link><description>Recent content in Brute Force Attacks on Digi Hunch</description><generator>Hugo -- gohugo.io</generator><language>en-US</language><lastBuildDate>Wed, 02 Apr 2025 10:34:44 -0400</lastBuildDate><atom:link href="https://www.digihunch.com/tag/brute-force-attacks/index.xml" rel="self" type="application/rss+xml"/><item><title>Managing EC2 instances across accounts with Ansible</title><link>https://www.digihunch.com/2024/05/managing-ec2-instances-across-aws-accounts-ssm/</link><pubDate>Mon, 27 May 2024 14:00:22 -0400</pubDate><guid>https://www.digihunch.com/2024/05/managing-ec2-instances-across-aws-accounts-ssm/</guid><description>&lt;img src="https://www.digihunch.com/wp-content/uploads/2025/04/feature-ec2-ssm.webp" alt="Featured image of post Managing EC2 instances across accounts with Ansible" /&gt;&lt;p class="wp-block-paragraph"&gt;I regard &lt;a href="https://aws.amazon.com/systems-manager/"&gt;AWS Systems Manager&lt;/a&gt; as &lt;a href="https://www.digihunch.com/2023/10/the-systems-manager-hodgepodge/"&gt;omnipotent&lt;/a&gt;. Nonetheless, there are a few reasons that makes &lt;a href="https://www.ansible.com/"&gt;Ansible&lt;/a&gt; still a prevalent VM (EC2) management tool over Systems Manager (SSM). First, organizations already vested in their custom Ansible &lt;a href="https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_reuse_roles.html"&gt;roles&lt;/a&gt; and &lt;a href="https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_intro.html"&gt;playbooks&lt;/a&gt; want to reuse, and expand their assets in Ansible. The benefit is consistency in the VM management, over time, and across platforms (AWS, on-prem, Azure, etc). Even for AWS shops, in the last few years many enterprises have adopted &lt;a href="https://www.digihunch.com/2022/12/landing-zone-in-aws/"&gt;AWS landing zone&lt;/a&gt; with the &lt;a href="https://docs.aws.amazon.com/whitepapers/latest/organizing-your-aws-environment/benefits-of-using-multiple-aws-accounts.html"&gt;multiple AWS account&lt;/a&gt; prescriptive pattern. However AWS Systems Manager still lacks integration with &lt;a href="https://docs.aws.amazon.com/organizations/latest/userguide/services-that-can-integrate-ssm.html"&gt;AWS Organization&lt;/a&gt; (except for a few non-core capabilities). This creates the demand of managing EC2 instances across AWS accounts. In this post, we propose a secure method to manage a fleet of EC2 instances from multiple AWS accounts, using Systems Manager . It also enables connectivity from an Ansible control node.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;Prerequisites&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;This proposal ties a few CLI tools together, including AWS CLI, SSH, Ansible, etc. It also requires the cloud engineer to understand how they work. I&amp;#8217;ll start with the choice of the tools.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Above, I discussed the benefit of Ansible. Since Ansible operates on SSH, we&amp;#8217;ll still have to use SSH tools. Even though SSM agent provides a way to connect to EC2 instance without requiring an RSA key pair, we still need SSH since it is a well-established &lt;a href="https://datatracker.ietf.org/doc/html/rfc4253"&gt;industry standard&lt;/a&gt; (RFC4253) and the foundation of Ansible. These two technologies are not mutually exclusive. In fact, the SSM agent provides a secure enhancement to the operation with SSH. Traditionally, on each EC2 instance we&amp;#8217;d have to run SSHD services which opens TCP port 22 (or alternative TCP port as configured). For authentication we favour key pair over password but the open port is still an attack surface vulnerable to brute force and DDoS attacks. For EC2 instances on private networks there is no reachability to the instance&amp;#8217;s SSH port unless the bastion is also in a connected network. &lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;As I cover in a &lt;a href="https://www.digihunch.com/2023/10/the-systems-manager-hodgepodge/"&gt;post&lt;/a&gt;, the SSM Session Manager comes in handy. The SSM agent operates from the instance and communicate outbound to AWS backend. Since the SSM agent runs under a privileged user on the OS, you can perform OS-level commands through SSM. Further, AWS developed a &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html"&gt;Session Manager plugin&lt;/a&gt; with AWS CLI, allowing AWS CLI as a proxy command when making an SSH connection. Therefore SSM enables SSH connection without requiring port 22 to be open. In addition we&amp;#8217;ll need to use RSA key pair as required for SSH, which is also an improvement to the security posture. &lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;That explains the dependent tools. On the Ansible &lt;a href="https://docs.ansible.com/ansible/latest/network/getting_started/basic_concepts.html#control-node"&gt;control node&lt;/a&gt;, apart from Ansible itself, we need the latest version of AWS CLI with the &lt;a href="https://docs.aws.amazon.com/organizations/latest/userguide/services-that-can-integrate-ssm.html"&gt;Session Manager plugin&lt;/a&gt;, we need to configure AWS CLI properly to connect to EC2 instances across multiple AWS accounts. &lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;Configure AWS CLI&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;This section discusses how to configure AWS CLI. I have a couple of handy aliases for productivity but they are not essential. For example, I often need to check the IAM identity making the call, and I often need to list out all profiles configured. So I added the following two entries in the &lt;code&gt;~/.aws/cli/alias&lt;/code&gt; file:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[toplevel]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;whoami = sts get-caller-identity --no-cli-pager --output yaml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;profile = configure list-profiles&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;With that I have an alias to check IAM identity and available profiles. Then we can start configuring the profiles for CLI (in the file &lt;code&gt;~/.aws/config&lt;/code&gt;). Since we&amp;#8217;ll be working with multiple AWS accounts, we have to manage multiple CLI profiles, which implies that:&lt;/p&gt;&#10;&lt;ol class="wp-block-list"&gt;&#10;&lt;li&gt;We better use the &lt;code&gt;--profile&lt;/code&gt; switch to explicitly specify profile being used, instead of relying on the &lt;code&gt;AWS_PROFILE&lt;/code&gt; environment variable;&lt;/li&gt;&#10;&lt;li&gt;As a security best practice, we should not configure profiles with &lt;a href="https://docs.aws.amazon.com/sdkref/latest/guide/access-iam-users.html"&gt;long-term IAM credential&lt;/a&gt; in the config file;&lt;/li&gt;&#10;&lt;li&gt;We must ensure the CLI doesn&amp;#8217;t prompt for log-in every time we switch profile&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p class="wp-block-paragraph"&gt;To satisfy #3 there are many ways but we&amp;#8217;ll discuss two: using cross-account IAM role, and using AWS SSO.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Bonus point if you enable &lt;a href="https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-completion.html"&gt;auto-complete&lt;/a&gt; for AWS CLI.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;Configure AWS CLI Profiles&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;With cross-account IAM role, the idea, is that the client start with one IAM identity, and use that IAM identity to assume roles on several other accounts. The configuration looks like this:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[profile jump_account]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;credential_process = /opt/bin/awscreds-custom --username helen&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[profile target_account_1]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;role_arn = arn:aws:iam::123456789011:role/OrganizationAccountAccessRole&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;source_profile = jump_account&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[profile target_account_2]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;role_arn = arn:aws:iam::123456789012:role/OrganizationAccountAccessRole&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;source_profile = jump_account&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;In this example, you start with an validated identity in the jump account, then assume a privileged IAM role named OrganizationAccountAccessRole on the target accounts. Typically such IAM roles are pre-configured (e.g. in an multi-account landing zone) with appropriate trust policy to allow principals from the jump account. Once you&amp;#8217;re validated as the IAM identity in the jump account, then you can use profiles for target accounts without being prompted for credentials again.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;If your have configured IAM Identity Center for the multi-account environment, consider an alternative approach using sso login. The configuration usually looks like this:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[sso-session sso]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sso_start_url = https://myorg.awsapps.com/start/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sso_region = us-east-1&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sso_registration_scopes = sso:account:access&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[profile target_account_1]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sso_session = sso&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sso_account_id = 123456789011&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sso_role_name = AWSAdministratorAccess&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[profile target_account_2]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sso_session = sso&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sso_account_id = 123456789012&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sso_role_name = AWSAdministratorAccess&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;This is often used by human users with SSO credential. In this example, to authenticate the sso session, start with command &amp;#8220;aws sso login&amp;#8221; . Then you can use all profiles by specifying &lt;code&gt;--profile&lt;/code&gt; switch without having to log in again.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;Configure SSH to EC2 via SSM&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;The EC2 instance must connect to Systems Manager &lt;a href="https://docs.aws.amazon.com/general/latest/gr/ssm.html"&gt;endpoint&lt;/a&gt;, before one can SSH to the instance using the plugin. Once connected, you should find the instance in &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/fleet.html"&gt;Fleet Manager&lt;/a&gt;. For this to happen, there are a few conditions. First, the instance must be able to reach the endpoint, either via public Internet, or via VPC interface endpoints if in a private subnet. Second, the instance profile must contain an IAM role with appropriate permissions. We can use AWS managed policy AmazonSSMManagedInstanceCore in the role. In addition, if we record the SSM session to an S3 bucket with encryption the instance profile must have permission to use the encryption key.&lt;/p&gt;&#10;&lt;div class="wp-block-image"&gt;&#10;&lt;figure class="aligncenter size-full"&gt;&lt;img loading="lazy" decoding="async" width="511" height="141" src="https://www.digihunch.com/wp-content/uploads/2024/05/ssh-ssm.webp" alt="" class="wp-image-12985" srcset="https://www.digihunch.com/wp-content/uploads/2024/05/ssh-ssm.webp 511w, https://www.digihunch.com/wp-content/uploads/2024/05/ssh-ssm-300x83.webp 300w" sizes="auto, (max-width: 511px) 100vw, 511px" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p class="wp-block-paragraph"&gt;SSM agent uses the IAM role. The agent runs as a service on Linux or Windows machines. Many AWS managed AMIs come with SSM agent pre-installed. If that is not the case, you&amp;#8217;d install the agent in your own AMI, or in user data which requires downloading the installer. With these configuration you&amp;#8217;d be able to connect to the instance via SSM. We can use AWS &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-sessions-start.html#sessions-start-cli"&gt;CLI SSM command&lt;/a&gt;, or AWS web console to start an SSH session. To use SSH CLI utility, we install the &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/plugin-version-history.html"&gt;session manager plugin&lt;/a&gt; along with AWS CLI on the SSH client machine. We also specify a public key for EC2 instance and run ssh command with the private key. The SSH configuration needs a configuration such as:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;host i-* mi-*&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ProxyCommand sh -c &amp;#34;aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters &amp;#39;portNumber=%p&amp;#39;&amp;#34;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; User ec2-user&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; IdentityFile ~/.ssh/id_rsa&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;With this entry, you may directly SSH by instance ID (usually starting with i-* and mi-*), and the specified Proxy Command with SSM session document AWS-StartSSHSession will be invoked. &lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;Configure Ansible Inventory&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;We can SSH to an instance (without port 22 open, on top of SSM) using the method above. Similarly, we can also configure Ansible to connect to the instance, without port 22. The inventory configuration looks like this:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mytest:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hosts:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; instance1:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ansible_host: i-00aabbffcc7755221&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ansible_user: ubuntu&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ansible_ssh_common_args: -o ProxyCommand=&amp;#34;aws ssm start-session --target %h --document-name AWS-StartSSHSession --profile target_account_1&amp;#34;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; instance2:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ansible_host: i-eedd88ff66aa22442&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ansible_user: ubuntu&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ansible_ssh_common_args: -o StrictHostKeyChecking=no -o ProxyCommand=&amp;#34;sh -c \&amp;#34;aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters &amp;#39;portNumber=%p&amp;#39; --profile target_account_2 \&amp;#34;&amp;#34;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;Note I used two patterns for ansible_ssh_common_args that are similar. Both works. Each entry references its own profile. This is necessary because Ansible does not have the knowledge which instance belongs to which profile&amp;#8217;s account.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Another way to get Ansible connect to instances is to use the connection plugin &lt;a href="https://docs.ansible.com/ansible/latest/collections/community/aws/aws_ssm_connection.html"&gt;community.aws.aws_ssm&lt;/a&gt;, by specifying ansible_connection: aws_ssm (e.g. as host variable) and other required variables (e.g. profile). This method does not require SSH channel but it requires an S3 bucket, and hence IAM permission on the controller node.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Ansible supports dynamic inventory, in three ways: inventory source file (with existing plugin), custom inventory plugin, and inventory script (in Python). Take source file as an example for EC2, add the followings as the content of aws_ec2.yaml:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;plugin: amazon.aws.aws_ec2&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# Attach the default AWS profile&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;aws_profile: target_account_1&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;compose:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ansible_host: instance_id&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ansible_user: &amp;#34;&amp;#39;ubuntu&amp;#39;&amp;#34;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ansible_ssh_common_args: &amp;#34;&amp;#39;-o ProxyCommand=\&amp;#34;aws ssm start-session --target %h --document-name AWS-StartSSHSession --profile target_account_1 \&amp;#34;&amp;#39;&amp;#34;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;Then we can display the rendered inventory list, and Ansible-ping the instances. &lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ansible-inventory -i aws_ec2.yaml --list -y | less&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ansible all -i aws_ec2.yaml -m ping&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;For more flexibility, for the composed variables, we could use jinja2 expression to generate the value. In both ways, we produce an inventory source per profile using the &lt;a href="https://vexpose.blog/2023/11/20/ansible-dynamic-inventory-tutorial/"&gt;built-in inventory plugin&lt;/a&gt; aws_ec2. For greater flexibility, such as consolidating instances from all accounts into a single inventory, consider writing your own&lt;a href="https://www.redhat.com/sysadmin/ansible-dynamic-inventories"&gt; inventory script&lt;/a&gt;, or even &lt;a href="https://www.redhat.com/sysadmin/ansible-plugin-inventory-files"&gt;own inventory plugin&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;Conclusion&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;In this post we propose a way to manage instances across AWS accounts. Two main challenges are establishing the communication channel (SSH on top of SSM) and generating inventory data in Ansible. Some AWS services can generate inventory data, such as &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-inventory-datasync.html"&gt;resource data sync&lt;/a&gt; in Systems Manager, or using AWS Config Aggregator. It is unfortunate that neither way produces the inventory data in a format that is directly compatible with Ansible inventory. Therefore, you might have to create a custom Ansible dynamic inventory script (in Python) that reads from the inventory data from AWS Config Aggregator (which supports AWS Organization). The side benefit of this script is that it is usually faster than the built-in &lt;a href="https://docs.ansible.com/ansible/latest/collections/amazon/aws/aws_ec2_inventory.html"&gt;aws_ec2&lt;/a&gt; inventory plugin.&lt;/p&gt;&#10;&lt;nav class="wp-post-navigation" aria-label="Post navigation"&gt;&#10;&lt;a rel="prev" href="https://www.digihunch.com/2024/03/public-key-infrastructure-3-of-3-use-cases/"&gt;&lt;span class="wp-post-navigation-label"&gt;Previous Post&lt;/span&gt;&lt;strong class="wp-post-navigation-title"&gt;Public Key Infrastructure 3 of 3 – PKI Implementation&lt;/strong&gt;&lt;/a&gt;&#10;&lt;a rel="next" href="https://www.digihunch.com/2024/07/iam-roles-for-any-workload/"&gt;&lt;span class="wp-post-navigation-label"&gt;Next Post&lt;/span&gt;&lt;strong class="wp-post-navigation-title"&gt;IAM Roles for any workload&lt;/strong&gt;&lt;/a&gt;&#10;&lt;/nav&gt;&#10;</description></item><item><title>WordPress Security Basics</title><link>https://www.digihunch.com/2023/11/wordpress-security/</link><pubDate>Fri, 17 Nov 2023 12:02:00 -0400</pubDate><guid>https://www.digihunch.com/2023/11/wordpress-security/</guid><description>&lt;img src="https://www.digihunch.com/wp-content/uploads/2025/04/feature-wp-basic.webp" alt="Featured image of post WordPress Security Basics" /&gt;&lt;h2 class="wp-block-heading"&gt;Background&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;In 2019, I moved &lt;a href="https://www.digihunch.com/2019/04/build-a-wordpress-site-in-one-hour-with-lightsail/"&gt;this site&lt;/a&gt; to WordPress hosted on an Amazon Lightsail instance. There were few visits at that time so I lived with the single-server architecture. The website traffic has since been in steady growth but I have been too busy to catch up with the WordPress security setup. In July 2023, a malware impacted this site as well as the web traffic. It took me several months to fix a few related issues but the traffic still has not fully recovered. This post is about the lessons.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;The Incident&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;I first noticed the issue when I clicked on links to my web page from Google result and got redirected to some spam site. It did not happen 100% of time, but it is annoying enough. In the mean time, from Google search analytics I noticed traffic volume going up with a lot of traffic going to URLs that I did not recognize or create. Somehow these URLs have a lot of clicks and impression counts. These are signs of artificial traffic.&lt;/p&gt;&#10;&lt;div class="wp-block-image"&gt;&#10;&lt;figure class="aligncenter size-full is-resized"&gt;&lt;img loading="lazy" decoding="async" width="730" height="536" src="https://www.digihunch.com/wp-content/uploads/2023/11/wp-traffic.webp" alt="" class="wp-image-12965" style="width:508px;height:auto" srcset="https://www.digihunch.com/wp-content/uploads/2023/11/wp-traffic.webp 730w, https://www.digihunch.com/wp-content/uploads/2023/11/wp-traffic-300x220.webp 300w" sizes="auto, (max-width: 730px) 100vw, 730px" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Obviously the site was hacked. The first thing to determine is whether the server access was compromised. From the audit log (/var/log/auth.log and auth.log.gz) I can see a lot of brute force attempts to connect but fortunately none was successful. That also prompt me to change the default SSH port and use ECDSA key pair. Since the OS access is safe, the hack happens at the WordPress level. I suspected the sideloaded plugins from a few days ago. So I immediately removed all sideloaded plugins. &lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;The attack is called malicious redirect. The plugin puts creepy pages in WordPress directory without my awareness and direct user traffic via my website. To clean up the damage, I looked into my WordPress directories at &lt;code&gt;/opt/bitnami/wordpress&lt;/code&gt; and found many suspicious signs:&lt;/p&gt;&#10;&lt;ol class="wp-block-list"&gt;&#10;&lt;li&gt;There are directories with weird names, such as &lt;code&gt;rexall-vitalmin&lt;/code&gt;, or &lt;code&gt;q4lee3&lt;/code&gt;, etc&lt;/li&gt;&#10;&lt;li&gt;In each of those directories there was an &lt;code&gt;index.php&lt;/code&gt; file and &lt;code&gt;.htaccess&lt;/code&gt; file; &lt;/li&gt;&#10;&lt;li&gt;Those directories also have other files which look like red herrings;&lt;/li&gt;&#10;&lt;li&gt;All those files have the same date time (from July 6);&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Other directories to look at are &lt;code&gt;/bitnami/wordpress/wp-content/plugins&lt;/code&gt;, where I noticed two directories (named &lt;code&gt;gokyfozaxy&lt;/code&gt; and &lt;code&gt;q199n071&lt;/code&gt;) that are not accounted for; and &lt;code&gt;/bitnami/wordpress/wp-content/themes/&lt;/code&gt;, which contains unknown directories.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;Clean up and hardening&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;In one of the .htaccess file I noticed segments of mojibake (garbled texts). I first tried to manually remove those files, but the problems stayed. Because the malicious redirect did not happen consistently on every single click, I sometime had false impression that the problem went away. &lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;However, the challenges with manual cleaning are: 1. there are too many bad files (.htaccess and php files containing mojibake segments); 2. some existing files are impacted with mojibake segments too. I found a free plugin called &lt;a href="https://www.wordfence.com/"&gt;WordFence&lt;/a&gt; to scan the file directory for malicious chagnes, and delete the bad files or bad segments. I also tried a paid scanner (&lt;a href="https://www.malcare.com/"&gt;Malcare&lt;/a&gt;) which found an bad file in /bitnami/wordpress/wp-content/themes/. However, it also blocked my site so I removed Malcare right away. Using the combination of WordFence and Malcare appears to have cleared up the offending files. After restarting apache, the bad URLs are no longer redirecting to spam sites.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;This time, I decided to harden the WordPress system given the evidence of brute force attack at different point of entries. At OS level, I mentioned the changes to SSH daemon configuration. At WordPress level, I used WordFence to perform several levels of scans for problems and and improved posture such as admin user&amp;#8217;s MFA. I also noticed a few unrecognized wordpress users and used wordpress CLI to delete those and other unused users.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;The wordpress.org website has some &lt;a href="https://wordpress.org/documentation/article/faq-my-site-was-hacked/"&gt;general guidance&lt;/a&gt; on what to do when a site is hacked, and a &lt;a href="https://wordpress.org/documentation/article/hardening-wordpress/"&gt;general guidance&lt;/a&gt; on hardening WordPress.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;Back Links&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Another clean up work I had to do is dealing with back links. Back links are URLs from other sites that references this site. There are several situations:&lt;/p&gt;&#10;&lt;ol class="wp-block-list"&gt;&#10;&lt;li&gt;If it&amp;#8217;s a made-up URL, then it returns 404. In my case, these are URLs that stopped working once I cleaned up my server from the incident. However, the sources are still using these bad URLs. They are bad back-links;&lt;/li&gt;&#10;&lt;li&gt;If it&amp;#8217;s a legit URL, look at if it&amp;#8217;s hot linking, such as another site directly access an image from my site. These are bad back-links;&lt;/li&gt;&#10;&lt;li&gt;If it&amp;#8217;s legit URLs, and the referrer site has a good &lt;a href="https://en.wikipedia.org/wiki/Domain_authority"&gt;domain authority&lt;/a&gt; score. These are likely to be good back links&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Generally, it is painful to deal with bad back links because I&amp;#8217;m not in control. I used a few free backlink checker tools (e.g. &lt;a href="https://search.google.com/search-console/links"&gt;Links report&lt;/a&gt; on Google Search Console, &lt;a href="https://seomator.com/free-backlink-checker-tool"&gt;SEOMATOR&lt;/a&gt;, SEMRush free) and found a lot of spammy sites that I had to request Google to &lt;a href="https://search.google.com/search-console/disavow-links"&gt;disavow&lt;/a&gt;. Otherwise, they may negatively impact the search performance.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;Repercussions&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;In the next few months, my pages are no longer a stop for their redirect. However, web request for those invalid URLs keep coming. The bad pages are still in Google&amp;#8217;s cache. There are a lot of page request with 404 return code, and we consider this an HTTP flood. The problem now is that the amount of 404 return code is impacting how my site ranks in search engine. To make it worse, the amount of these requests with invalid URL increase since August. &lt;/p&gt;&#10;&lt;figure class="wp-block-image size-full"&gt;&lt;img loading="lazy" decoding="async" width="1024" height="289" src="https://www.digihunch.com/wp-content/uploads/2023/11/wp-404.webp" alt="" class="wp-image-12966" srcset="https://www.digihunch.com/wp-content/uploads/2023/11/wp-404.webp 1024w, https://www.digihunch.com/wp-content/uploads/2023/11/wp-404-300x85.webp 300w, https://www.digihunch.com/wp-content/uploads/2023/11/wp-404-768x217.webp 768w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /&gt;&lt;/figure&gt;&#10;&lt;p class="wp-block-paragraph"&gt;To fix this, there are two measures. First, in Google search console, I have to tell Google to remove those URLs from its cache. I have identified a number of prefix patterns, and submitted a request for each URL pattern. It takes google a day to have them cleared. &lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;After that, the bad request will no longer come from Google users clicking on bad URL. In my case, the requests did not reduce significantly, suggesting that most of the requests come from bots. Therefore I had to figure out a way to prevent those bad request hitting my server, which is a typical web application firewall requirement. &lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Looking for such a solution for my WordPress Security I landed on Cloudflare. Cloudflare is pretty user-friendly with an easy-to-understand &lt;a href="https://developers.cloudflare.com/reference-architecture/cdn-reference-architecture/"&gt;reference architecture&lt;/a&gt;. When I started, Cloudflare can import my DNS records, and guided me to change my name servers so I delegate my DNS management it. When I first move to Cloudflare the website gives &lt;a href="https://developers.cloudflare.com/ssl/troubleshooting/too-many-redirects/"&gt;ERR_TOO_MANY_REDIRECTS&lt;/a&gt;. I ended up having to go to SSL/TLS and set encryption mode to Full (strict) to get rid of this error. I also have to re-configure email forwarding as a result of name server change.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;CloudFlare &lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Even for a self-hosted single-server site, it is very beneficial to place an Application Firewall upfront for WordPress security. I find CloudFlare are very useful service that provides everything else you need to host the web site. For example, it contains a domain registry itself. It manages DNS and allows email forwarding. In addition, it helps generate TLS certificate etc. The free tier covers everything for a small website, with the Application Firewall as the core feature. Within the free tier I can have these features:&lt;/p&gt;&#10;&lt;ul class="wp-block-list"&gt;&#10;&lt;li&gt;domain registrar and name servers (not for free but at a reasonable cost)&lt;/li&gt;&#10;&lt;li&gt;SSL certificate (not for free but at a reasonable cost)&lt;/li&gt;&#10;&lt;li&gt;Request event tracking&lt;/li&gt;&#10;&lt;li&gt;redirect rule: zone apex to www, and /status to uptime status page&lt;/li&gt;&#10;&lt;li&gt;return code 409 for obsolete URLs (using routes and workers)&lt;/li&gt;&#10;&lt;li&gt;email routing and forwarding&lt;/li&gt;&#10;&lt;li&gt;WAF rules (path, parameter, rate, etc)&lt;/li&gt;&#10;&lt;li&gt;DDoS protection and Bot Fight mode&lt;/li&gt;&#10;&lt;li&gt;hot-linking prevention (i.e. other sites references images on your site directly)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p class="wp-block-paragraph"&gt;I am still exploring features for CloudFlare. One stunning feature is routes and workers. Essentially you can serve a function in response to HTTP request at a specific route. This is particularly useful in scenarios where it is not straightforward to add web pages on the backend server. For example, I want requests with certain paths to return HTTP code 490 and do not want to mock with the WordPress server, we can make use of CloudFlare worker.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;Lessons Learned&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;For WordPress security, never use suspicious plugins. Keep an additional layer of defense in WordPress such as Wordfense. It helps block malicious traffic that went through the first layer. It also helps configure MFA for administrators. &lt;/p&gt;&#10;&lt;nav class="wp-post-navigation" aria-label="Post navigation"&gt;&#10;&lt;a rel="prev" href="https://www.digihunch.com/2023/10/the-systems-manager-hodgepodge/"&gt;&lt;span class="wp-post-navigation-label"&gt;Previous Post&lt;/span&gt;&lt;strong class="wp-post-navigation-title"&gt;AWS Systems Manager is an Omnipotent Hodgepodge&lt;/strong&gt;&lt;/a&gt;&#10;&lt;a rel="next" href="https://www.digihunch.com/2023/12/workload-identity-on-kubernetes-1-of-2-aks/"&gt;&lt;span class="wp-post-navigation-label"&gt;Next Post&lt;/span&gt;&lt;strong class="wp-post-navigation-title"&gt;Workload Identity on Kubernetes 1 of 2 – AKS&lt;/strong&gt;&lt;/a&gt;&#10;&lt;/nav&gt;&#10;</description></item></channel></rss>