Orchestrating Automated Deployments via GitHub Actions and AWS EC2 SSH Integration

The integration of GitHub Actions with Amazon Elastic Compute Cloud (EC2) via Secure Shell (SSH) represents a critical architectural bridge between continuous integration (CI) and continuous deployment (CD). By leveraging this connectivity, developers can transition from a code commit to a live production environment without manual intervention. This process involves the synchronization of cryptographic keys, the configuration of identity providers, and the precise calibration of network security layers to ensure that the automation runner can execute commands on a remote virtual machine securely. Whether using traditional SSH keys, the EC2 Instance Connect Proxy for private instances, or OpenID Connect (OIDC) for passwordless authentication, the goal is to establish a trusted communication channel that allows for the execution of shell scripts, dependency installation, and service restarts on the target server.

The Mechanics of SSH Key Generation and Authorization

Before a GitHub Action can communicate with an EC2 instance, a cryptographic trust relationship must be established. This is typically achieved through the generation of an RSA key pair, consisting of a public key and a private key.

To initiate this process, a user must generate the keys within the .ssh directory of their local environment or the server. The following command is utilized to create a high-entropy 4096-bit RSA key:

ssh-keygen -t rsa -b 4096 -C "[email protected]"

During the generation process, the user is prompted to enter a file name. Once confirmed, two files are created: the private key (e.g., key_name) and the public key (e.g., key_name.pub). The public key acts as the "lock," while the private key acts as the "key" that opens it.

For the EC2 instance to accept the connection, the public key must be appended to the authorized_keys file on the server. This is performed using the following command:

cat github-actions.pub >> ~/.ssh/authorized_keys

The impact of this step is the creation of a permanent authorization entry. Any entity possessing the corresponding private key can now authenticate via SSH without needing a password. This is a prerequisite for any automated pipeline, as GitHub Actions runners cannot interactively provide passwords during a deployment flow.

Implementing Secure Secret Management in GitHub

Storing sensitive credentials like private keys or IP addresses directly within a YAML workflow file is a catastrophic security risk. GitHub Secrets provide an encrypted environment to store these values.

The following secrets must be configured within the GitHub repository settings to enable a successful SSH handshake:

  • SSHPRIVATEKEY: The entire content of the private key file generated during the key-pair process.
  • HOSTNAME / IPADDRESS: The Elastic IP or the public IP address of the EC2 instance.
  • USER_NAME: The default user for the EC2 instance (e.g., ubuntu for Ubuntu images or ec2-user for Amazon Linux).

By mapping these secrets to environment variables within the workflow, the pipeline can dynamically inject the credentials at runtime. For example, using HOSTNAME: ${{secrets.SSH_HOST}} ensures that the server's identity is hidden from the public eye while remaining accessible to the runner.

Advanced AWS Identity and Access Management via OIDC

Modern DevOps practices move away from long-lived AWS Access Keys in favor of OpenID Connect (OIDC). This allows GitHub Actions to assume a temporary IAM role, significantly reducing the risk of credential leakage.

The configuration of the GitHub OIDC Provider in the AWS IAM console involves the following parameters:

  • Provider URL: https://token.actions.githubusercontent.com
  • Audience: sts.amazonaws.com

Once the provider is added, a specific IAM role must be created, such as Github-Action-Assume-Role. This role requires a trust relationship policy that allows the GitHub OIDC provider to assume the role only if the request originates from a specific repository. The trust relationship JSON is structured as follows:

json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::AWS_ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" }, "StringLike": { "token.actions.githubusercontent.com:sub": "repo:GITHUB_ORG/*" } } ] }

To give this role the power to manage network access, a policy named manage-security-group-ingress must be attached. This policy grants the action the ability to modify security group rules on the fly:

json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ec2:RevokeSecurityGroupIngress", "ec2:AuthorizeSecurityGroupIngress" ], "Resource": "arn:aws:ec2:AWS_REGION:AWS_ACCOUNT_ID:security-group/SECURITY_GROUP_ID" } ] }

The real-world consequence of this setup is "Just-in-Time" access. The pipeline can open port 22 for the runner's IP, execute the deployment, and then immediately close the port, minimizing the server's attack surface.

Network Security and Security Group Configuration

Security Groups act as a virtual firewall for EC2 instances. To allow SSH traffic, the security group must be configured to permit TCP traffic on port 22.

A dedicated security group, such as github-actions-ec2-ssh, should be created and attached to the instance. While the group can be created with no initial rules, it must be updated to allow the GitHub Action runner IP. This can be achieved via the AWS CLI using the following command:

aws ec2 authorize-security-group-ingress --group-id SECURITY_GROUP_ID --protocol tcp --port 22 --cidr GITHUB_ACTIONS_IP/32

The GITHUB_ACTIONS_IP must be sourced from the published list of GitHub Action runner IP ranges. This creates a narrow window of access, ensuring that only verified GitHub runners can attempt an SSH connection.

Workflow Implementation Strategies

Depending on the architectural requirements, different GitHub Actions can be used to facilitate the SSH connection.

Standard SSH Deployment Workflow

For instances with public IP addresses, a manual SSH setup within the workflow is common. This involves writing the secret private key to a temporary file and setting the correct permissions.

The following workflow demonstrates a full deployment cycle:

```yaml
name: Deploy
on:
push:
branches: [ dev ]

jobs:
Deploy:
name: Deploy to EC2
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build & Deploy
env:
PRIVATEKEY: ${{ secrets.SSHPRIVATEKEY }}
HOSTNAME: ${{secrets.SSH
HOST}}
USERNAME: ${{secrets.USERNAME}}
run: |
echo "$PRIVATEKEY" > privatekey && chmod 600 privatekey
ssh -o StrictHostKeyChecking=no -i private
key ${USER_NAME}@${HOSTNAME} '
cd /home/ubuntu/ &&
git checkout dev &&
git fetch --all &&
git reset --hard origin/dev &&
git pull origin dev &&
sudo npm i &&
sudo npm run build &&
sudo pm2 stop ./dist/index.js &&
sudo pm2 start ./dist/index.js
'
```

In this scenario, chmod 600 private_key is mandatory. SSH clients will reject a private key that is too accessible to other users on the system, resulting in a permission denied error. The StrictHostKeyChecking=no flag is used to bypass the interactive prompt that asks the user to verify the server's authenticity, which is essential for non-interactive CI/CD pipelines.

Utilizing Specialized GitHub Actions

For those seeking a more streamlined approach, third-party actions can abstract the complexity of SSH setup.

The omarhosny206/[email protected] action simplifies the configuration by taking the private key and URL as inputs. A sample implementation is as follows:

```yaml
name: Setup SSH for EC2 instance
on:
push:
branches:
- master

jobs:
setup-ssh:
runs-on: ubuntu-latest
env:
EC2SSHPRIVATEKEY: ${{ secrets.EC2SSHPRIVATEKEY }}
EC2URL: ${{ secrets.EC2URL }}
EC2USERNAME: ${{ secrets.EC2USERNAME }}
steps:
- name: Setup SSH for EC2
uses: omarhosny206/[email protected]
with:
EC2SSHPRIVATEKEY: $EC2SSHPRIVATEKEY
EC2URL: $EC2URL
- name: Create a new file on the EC2 instance with "hello-world"
run: ssh -o StrictHostKeyChecking=no $EC2USERNAME@$EC2URL "echo "hello-world" >> new_file.txt"
```

Handling Instances Without Public IPs

In high-security environments, EC2 instances are often placed in private subnets without public IP addresses. In such cases, traditional SSH is impossible. To resolve this, the "run-command-on-remote-ec2-instance-via-ssh" action can be used.

This action leverages the EC2 Instance Connect Proxy. The proxy allows SSH traffic to be tunneled through AWS APIs, meaning the runner does not need a direct network path to the instance, only the appropriate AWS permissions. This action is designed to work in conjunction with the configure-aws-credentials action. It is highly recommended to use the role-to-assume identity to assume the role via the GitHub OIDC provider, maintaining the security chain from the GitHub runner to the AWS API and finally to the private EC2 instance.

Comparative Analysis of SSH Integration Methods

The following table provides a technical comparison of the different methods for connecting GitHub Actions to EC2.

Method Connection Type Security Level Network Requirement Primary Tool/Action
Traditional SSH Public IP Medium Port 22 Open ssh command / Manual Key
OIDC + SSH Public IP High Dynamic Port 22 IAM Role + ssh
EC2 Instance Connect Proxy/API Very High No Public IP Needed EC2 Instance Connect Proxy
Simplified Action Public IP Medium Port 22 Open setup-ssh-for-ec2

Conclusion

The process of connecting GitHub Actions to an AWS EC2 instance via SSH is a multi-layered operation that spans local key management, AWS identity configuration, and network security orchestration. By moving from basic SSH key injection to advanced OIDC-based role assumption, organizations can eliminate the need for long-lived secrets and drastically reduce the risk of unauthorized server access. The use of the EC2 Instance Connect Proxy further enhances this by enabling the management of private instances without compromising the security of the VPC. Ultimately, the choice of implementation—whether via a manual shell script, a specialized GitHub Action, or an API-driven proxy—depends on the specific network topology of the AWS environment and the required balance between deployment speed and security rigor.

Sources

  1. Run Command on Remote EC2 Instance via SSH
  2. Setup SSH for EC2
  3. Allow GitHub Actions to SSH into EC2
  4. GitHub Action with EC2 using SSH
  5. How to handle SSH keys with ec2-github actions

Related Posts