From 2638eac3c4878c570248778585f007b874f1676f Mon Sep 17 00:00:00 2001
From: Adar Nimrod <nimrod@shore.co.il>
Date: Fri, 15 Apr 2022 01:25:53 +0300
Subject: [PATCH] Ansible playbook: project_ci_aws_creds.yaml

An Ansible playbook to create an IAM user for GitLab projects' CI. Also,
rotates the IAM access keys, sets the CI variables for the access key,
attaches an inline policy to limit the user by IP and requested region.
Lastly, create a policy with full access to Resource Groups because I
usually create one for each deployment but there isn't such an AWS
managed policy.
---
 .gitignore                        |   1 +
 Ansible/ansible.cfg               |   1 +
 Ansible/collections/.gitkeep      |   0
 Ansible/project_ci_aws_creds.yaml | 208 ++++++++++++++++++++++++++++++
 Ansible/requirements.yml          |   6 +
 5 files changed, 216 insertions(+)
 create mode 100644 Ansible/collections/.gitkeep
 create mode 100644 Ansible/project_ci_aws_creds.yaml
 create mode 100644 Ansible/requirements.yml

diff --git a/.gitignore b/.gitignore
index 3377d94..387cc30 100644
--- a/.gitignore
+++ b/.gitignore
@@ -61,3 +61,4 @@ reports/
 Ansible/*.crt
 Ansible/*.csr
 Ansible/*.key
+Ansible/collections
diff --git a/Ansible/ansible.cfg b/Ansible/ansible.cfg
index 45588da..1c82790 100644
--- a/Ansible/ansible.cfg
+++ b/Ansible/ansible.cfg
@@ -1,5 +1,6 @@
 [defaults]
 callback_enabled = ansible.posix.profile_tasks, ansible.posix.timer
+collections_path = {{CWD}}/collections
 deprecation_warnings = True
 fact_caching = jsonfile
 fact_caching_connection = ~/.ansible/facts
diff --git a/Ansible/collections/.gitkeep b/Ansible/collections/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Ansible/project_ci_aws_creds.yaml b/Ansible/project_ci_aws_creds.yaml
new file mode 100644
index 0000000..8c52376
--- /dev/null
+++ b/Ansible/project_ci_aws_creds.yaml
@@ -0,0 +1,208 @@
+---
+- name: Generate AWS credentials for a GitLab project CI
+  hosts: localhost
+  gather_facts: false
+  become: false
+  vars_prompt:
+    - name: project_full_name
+      # yamllint disable-line rule:line-length
+      prompt: Full name for the GitLab project (namespace included, eg. group/project)
+      private: false
+    - name: aws_region
+      prompt: List of AWS regions to limit the IAM user (comma separated)
+      default: us-east-1
+      private: false
+  vars:
+    aws_username: "{{ project_name }}-ci"
+    aws_limit_policy:
+      Statement:
+        - Sid: LimitIP
+          Effect: Deny
+          Action: "*"
+          Resource: "*"
+          Condition:
+            ForAnyValue:NotIpAddress:
+              aws:SourceIp: '{{ shore_ip_addresses }}'
+        - Sid: LimitRegion
+          Effect: Deny
+          NotAction:
+            - cloudfront:*
+            - iam:*
+            - route53:*
+            - support:*
+          Resource: "*"
+          Condition:
+            ForAnyValue:StringNotEqualsIfExists:
+              aws:RequestedRegion: '{{ aws_region.split(",") }}'
+    aws_resourcegroup_policy:
+      Version: 2012-10-17
+      Statement:
+        - Sid: ResourceGroupFullAccess
+          Effect: Allow
+          Action: "resource-groups:*"
+          Resource: "*"
+    gitlab_base_url: "{{ lookup('env', 'GITLAB_BASE_URL') }}"
+    gitlab_token: "{{ lookup('env', 'GITLAB_TOKEN') }}"
+    project_id: '{{ project_dict["id"] }}'
+    project_name: '{{ project_dict["path"] }}'
+    shore_ip_addresses:
+      - "{{ lookup('community.general.dig', 'ns1.shore.co.il') }}"
+      - "{{ lookup('community.general.dig', 'ns4.shore.co.il') }}"
+  tasks:
+    - name: Search for GitLab projects with the same name
+      ansible.builtin.uri:
+        headers: &gitlab_headers
+          Authorization: Bearer {{ gitlab_token }}
+        method: GET
+        # yamllint disable-line rule:line-length
+        url: "{{ gitlab_base_url }}/search?scope=projects&search={{ project_full_name.split('/')[1] }}"
+      register: search_projects
+
+    - name: Find the right GitLab project
+      ansible.builtin.set_fact:
+        project_dict: >-
+          {{
+            search_projects.json|
+            selectattr("path_with_namespace", "equalto", project_full_name)|
+            first
+          }}
+
+    - name: Create a full access resource group IAM policy
+      community.aws.iam_managed_policy:
+        policy: '{{ aws_resourcegroup_policy|to_json }}'
+        policy_description: Provides full access to AWS Resource Groups
+        policy_name: &rg_policy_name AWSResourceGroupsFullAccess
+        state: present
+
+    - name: Create the IAM user
+      community.aws.iam_user:
+        managed_policies:
+          - *rg_policy_name
+        name: "{{ aws_username }}"
+        purge_policies: false
+        state: present
+        tags:
+          Project: '{{ project_name }}'
+
+    - name: Attach an inline policy to limit the IAM user
+      community.aws.iam_policy:
+        iam_name: '{{ aws_username }}'
+        iam_type: user
+        policy_json: '{{ aws_limit_policy|to_json }}'
+        policy_name: '{{ aws_username }}-limit'
+        skip_duplicates: true
+        state: present
+
+    # yamllint disable-line rule:line-length
+    - name: Create access key for the IAM user (a new one every time, task will always change)
+      community.aws.iam_access_key:
+        active: true
+        rotate_keys: true
+        state: present
+        user_name: '{{ aws_username }}'
+      register: aws_access_key
+
+    - name: Get the GitLab project CI variables
+      ansible.builtin.uri:
+        headers: *gitlab_headers
+        method: GET
+        url: "{{ gitlab_base_url }}/projects/{{ project_id }}/variables"
+      changed_when: false
+      register: project_vars
+
+    - name: Create the access key ID GitLab CI variable
+      # yamllint disable-line rule:line-length
+      when: project_vars.json|selectattr("key", "equalto", "AWS_ACCESS_KEY_ID")|length == 0
+      ansible.builtin.uri:
+        body:
+          environment_scope: "*"
+          key: AWS_ACCESS_KEY_ID
+          masked: false
+          protected: false
+          value: '{{ aws_access_key.access_key_id }}'
+          variable_key: env_var
+        body_format: form-urlencoded
+        headers: *gitlab_headers
+        method: POST
+        status_code: 201
+        url: "{{ gitlab_base_url }}/projects/{{ project_id }}/variables"
+
+    - name: Update the access key ID GitLab CI variable (will always change)
+      # yamllint disable-line rule:line-length
+      when: project_vars.json|selectattr("key", "equalto", "AWS_ACCESS_KEY_ID")|length == 1
+      changed_when: true
+      ansible.builtin.uri:
+        body:
+          environment_scope: "*"
+          masked: false
+          protected: false
+          value: '{{ aws_access_key.access_key_id }}'
+          variable_key: env_var
+        body_format: form-urlencoded
+        headers: *gitlab_headers
+        method: PUT
+        status_code: 200
+        # yamllint disable-line rule:line-length
+        url: "{{ gitlab_base_url }}/projects/{{ project_id }}/variables/AWS_ACCESS_KEY_ID"
+
+    - name: Create the secret access key GitLab CI variable
+      # yamllint disable-line rule:line-length
+      when: project_vars.json|selectattr("key", "equalto", "AWS_SECRET_ACCESS_KEY")|length == 0
+      ansible.builtin.uri:
+        body:
+          environment_scope: "*"
+          key: AWS_SECRET_ACCESS_KEY
+          masked: true
+          protected: false
+          value: '{{ aws_access_key.secret_access_key }}'
+          variable_key: env_var
+        body_format: form-urlencoded
+        headers: *gitlab_headers
+        method: POST
+        status_code: 201
+        url: "{{ gitlab_base_url }}/projects/{{ project_id }}/variables"
+
+    - name: Update the secret access key GitLab CI variable (will always change)
+      # yamllint disable-line rule:line-length
+      when: project_vars.json|selectattr("key", "equalto", "AWS_SECRET_ACCESS_KEY")|length == 1
+      changed_when: true
+      ansible.builtin.uri:
+        body:
+          environment_scope: "*"
+          masked: true
+          protected: false
+          value: '{{ aws_access_key.secret_access_key }}'
+          variable_key: env_var
+        body_format: form-urlencoded
+        headers: *gitlab_headers
+        method: PUT
+        status_code: 200
+        # yamllint disable-line rule:line-length
+        url: "{{ gitlab_base_url }}/projects/{{ project_id }}/variables/AWS_SECRET_ACCESS_KEY"
+
+    - name: Find all of the IAM access keys for the user
+      community.aws.iam_access_key_info:
+        user_name: '{{ aws_username }}'
+      register: aws_access_keys
+
+    - name: Remove the old IAM access keys
+      when: aws_access_keys.access_keys|length > 1
+      community.aws.iam_access_key:
+        id: '{{ access_key.access_key_id }}'
+        state: absent
+        user_name: '{{ aws_username }}'
+      vars:
+        # yamllint disable rule:line-length
+        access_key: >-
+          {{
+            aws_access_keys.access_keys|
+            rejectattr("access_key_id", "equalto", aws_access_key.access_key_id)|
+            first
+          }}
+        # yamllint enable rule:line-length
+
+    - name: AWS console URL for modifying the IAM user
+      debug:
+        # yamllint disable-line rule:line-length
+        msg: https://us-east-1.console.aws.amazon.com/iam/home#/users/{{ aws_username }}
+        verbosity: 0
diff --git a/Ansible/requirements.yml b/Ansible/requirements.yml
new file mode 100644
index 0000000..f56a5b3
--- /dev/null
+++ b/Ansible/requirements.yml
@@ -0,0 +1,6 @@
+---
+collections:
+  - name: amazon.aws
+    version: '>=3.2.0'
+  - name: community.aws
+    version: '>=3.2.0'
-- 
GitLab