diff --git a/flake.lock b/flake.lock index 1d1d66c..a560b71 100644 --- a/flake.lock +++ b/flake.lock @@ -74,6 +74,42 @@ "type": "github" } }, + "nixlib": { + "locked": { + "lastModified": 1636849918, + "narHash": "sha256-nzUK6dPcTmNVrgTAC1EOybSMsrcx+QrVPyqRdyKLkjA=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "28a5b0557f14124608db68d3ee1f77e9329e9dd5", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixos-generators": { + "inputs": { + "nixlib": "nixlib", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1660727616, + "narHash": "sha256-zYTIvdPMYMx/EYqXODAwIIU30RiEHqNHdgarIHuEYZc=", + "owner": "nix-community", + "repo": "nixos-generators", + "rev": "adccd191a0e83039d537e021f19495b7bad546a1", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixos-generators", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1664195620, @@ -124,6 +160,7 @@ "inputs": { "darwin": "darwin", "home-manager": "home-manager", + "nixos-generators": "nixos-generators", "nixpkgs": "nixpkgs", "nur": "nur", "wallpapers": "wallpapers", diff --git a/flake.nix b/flake.nix index 5ccbac8..7fd9403 100644 --- a/flake.nix +++ b/flake.nix @@ -32,9 +32,15 @@ flake = false; }; + # Used to generate NixOS images for other platforms + nixos-generators = { + url = "github:nix-community/nixos-generators"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; - outputs = { self, nixpkgs, darwin, wsl, home-manager, nur, wallpapers }: + outputs = { self, nixpkgs, ... }@inputs: let @@ -57,14 +63,14 @@ in { - nixosConfigurations = { + nixosConfigurations = with inputs; { desktop = import ./hosts/desktop { inherit nixpkgs home-manager nur globals wallpapers; }; wsl = import ./hosts/wsl { inherit nixpkgs wsl home-manager globals; }; }; - darwinConfigurations = { + darwinConfigurations = with inputs; { macbook = import ./hosts/macbook { inherit nixpkgs darwin home-manager nur globals; }; @@ -111,6 +117,14 @@ }); + # Package servers into images with a generator + packages.x86_64-linux = with inputs; { + aws = import ./generators/aws { + inherit nixpkgs nixos-generators home-manager globals; + system = "x86_64-linux"; + }; + }; + # Templates for starting other projects quickly templates = rec { default = basic; diff --git a/generators/aws/default.nix b/generators/aws/default.nix new file mode 100644 index 0000000..f648c18 --- /dev/null +++ b/generators/aws/default.nix @@ -0,0 +1,31 @@ +{ nixpkgs, system, nixos-generators, home-manager, globals, ... }: + +nixos-generators.nixosGenerate { + inherit system; + format = "amazon"; + modules = [ + home-manager.nixosModules.home-manager + { + user = globals.user; + fullName = globals.fullName; + dotfilesRepo = globals.dotfilesRepo; + gitName = globals.gitName; + gitEmail = globals.gitEmail; + networking.hostName = "sheep"; + gui.enable = false; + colorscheme = (import ../modules/colorscheme/gruvbox); + passwordHash = + "$6$PZYiMGmJIIHAepTM$Wx5EqTQ5GApzXx58nvi8azh16pdxrN6Qrv1wunDlzveOgawitWzcIxuj76X9V868fsPi/NOIEO8yVXqwzS9UF."; + publicKey = + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB+AbmjGEwITk5CK9y7+Rg27Fokgj9QEjgc9wST6MA3s"; + # AWS settings require this + permitRootLogin = "prohibit-password"; + } + ../hosts/common.nix + ../modules/nixos + ../modules/services/sshd.nix + ] ++ [ + # Required to fix diskSize errors during build + ({ ... }: { amazonImage.sizeMB = 16 * 1024; }) + ]; +} diff --git a/generators/aws/main.tf b/generators/aws/main.tf new file mode 100644 index 0000000..4fbb2ca --- /dev/null +++ b/generators/aws/main.tf @@ -0,0 +1,80 @@ +locals { + image_file = one(fileset(path.root, "result/nixos-amazon-image-*.vhd")) +} + +# Upload to S3 +resource "aws_s3_object" "image" { + bucket = "your_bucket_name" + key = basename(local.image_file) + source = local.image_file + etag = filemd5(local.image_file) +} + +# Setup IAM access for the VM Importer +data "aws_iam_policy_document" "vmimport_trust_policy" { + statement { + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["vmie.amazonaws.com"] + } + } +} + +data "aws_iam_policy_document" "vmimport" { + statement { + actions = [ + "s3:GetBucketLocation", + "s3:GetObject", + "s3:ListBucket", + ] + resources = [ + "arn:aws:s3:::${aws_s3_object.image.bucket}", + "arn:aws:s3:::${aws_s3_object.image.bucket}/*", + ] + } + statement { + actions = [ + "ec2:ModifySnapshotAttribute", + "ec2:CopySnapshot", + "ec2:RegisterImage", + "ec2:Describe*", + ] + resources = ["*"] + } +} + +resource "aws_iam_role" "vmimport" { + name = "vmimport" + assume_role_policy = data.aws_iam_policy_document.vmimport_trust_policy.json + inline_policy { + name = "vmimport" + policy = data.aws_iam_policy_document.vmimport.json + } +} + +# Import to EBS +resource "aws_ebs_snapshot_import" "image" { + disk_container { + format = "VHD" + user_bucket { + s3_bucket = aws_s3_object.image.bucket + s3_key = aws_s3_object.image.key + } + } + + role_name = aws_iam_role.vmimport.name +} + +# Convert to AMI +resource "aws_ami" "image" { + description = "Created with NixOS." + name = replace(basename(local.image_file), "/\\.vhd$/", "") + virtualization_type = "hvm" + + ebs_block_device { + device_name = "/dev/xvda" + snapshot_id = aws_ebs_snapshot_import.image.id + volume_size = 8 + } +} diff --git a/generators/aws/workflow.yml b/generators/aws/workflow.yml new file mode 100644 index 0000000..c0210e2 --- /dev/null +++ b/generators/aws/workflow.yml @@ -0,0 +1,260 @@ +name: 'Terraform' +env: + + + AWS_ACCOUNT_NUMBER: '' + AWS_PLAN_ROLE_NAME: github_actions_plan + AWS_APPLY_ROLE_NAME: github_actions_admin + + # Always required. Used for authenticating to AWS, but can also act as your + # default region if you don't want to specify in the provider configuration. + AWS_REGION: us-east-1 + + # You must change these to fit your project. + TF_VAR_project: change-me + TF_VAR_label: change-me + TF_VAR_owner: Your Name Here + + # If storing Terraform in a subdirectory, specify it here. + TERRAFORM_DIRECTORY: . + + # Pinned versions of tools to use. + # Check for new releases: + # - https://github.com/hashicorp/terraform/releases + # - https://github.com/fugue/regula/releases + # - https://github.com/terraform-linters/tflint/releases + TERRAFORM_VERSION: 1.2.6 + REGULA_VERSION: 2.9.0 + TFLINT_VERSION: 0.39.1 + + # Terraform configuration options + TERRAFORM_PARALLELISM: 10 + + # These variables are passed to Terraform based on GitHub information. + TF_VAR_repo: ${{ github.repository }} + +# This workflow is triggered in the following ways. +on: + + # Any push or merge to these branches. + push: + branches: + - dev + - prod + + # Any pull request targeting these branches (plan only). + pull_request: + branches: + - dev + - prod + + + # Any manual trigger on these branches. + workflow_dispatch: + branches: + - dev + - prod + +# ------------------------------------------------------------------- +# The rest of this workflow can operate without adjustments. Edit the +# below content at your own risk! +# ------------------------------------------------------------------- + +# Used to connect to AWS IAM +permissions: + id-token: write + contents: read + pull-requests: write + +# Only run one workflow at a time for each Terraform state. This prevents +# lockfile conflicts, especially during PR vs push. +concurrency: terraform-${{ github.base_ref || github.ref }} + +jobs: + terraform: + + name: 'Terraform' + + # Change this if you need to run your deployment on-prem. + runs-on: ubuntu-latest + + steps: + + # Downloads the current repo code to the runner. + - name: Checkout Repo Code + uses: actions/checkout@v2 + + # Install Nix + - name: Install Nix + uses: cachix/install-nix-action@v17 + + # Build the image + - name: Build Image + run: nix build .#aws + + # Login to AWS + - name: AWS Assume Role + uses: aws-actions/configure-aws-credentials@v1.6.1 + with: + role-to-assume: ${{ env.AWS_ROLE_ARN }} + aws-region: ${{ env.AWS_REGION }} + + # Exports all GitHub Secrets as environment variables prefixed by + # "TF_VAR_", which exposes them to Terraform. The name of each GitHub + # Secret must match its Terraform variable name exactly. + - name: Export Secrets to Terraform Variables + env: + ALL_SECRETS: ${{ toJson(secrets) }} + run: | + echo "$ALL_SECRETS" \ + | jq "to_entries | .[] | \"TF_VAR_\" + ( .key | ascii_downcase ) + \"=\" + .value" \ + | tr -d \" >> $GITHUB_ENV + + # Installs the Terraform binary and some other accessory functions. + - name: Setup Terraform + uses: hashicorp/setup-terraform@v2 + with: + terraform_version: ${{ env.TERRAFORM_VERSION }} + + # Checks whether Terraform is formatted properly. If this fails, you + # should install the pre-commit hook. + - name: Check Formatting + run: | + terraform fmt -no-color -check -diff -recursive + + # Downloads a Terraform code lint test. + - uses: terraform-linters/setup-tflint@v1 + name: Setup TFLint + with: + tflint_version: v${{ env.TFLINT_VERSION }} + + # Sets up linting with this codebase. + - name: Init TFLint + working-directory: ${{ env.TERRAFORM_DIRECTORY }} + run: tflint --init + + # Lints the current code. + - name: Run TFLint + working-directory: ${{ env.TERRAFORM_DIRECTORY }} + run: | + tflint -f compact + find ./modules/* -type d -maxdepth 0 | xargs -I __ tflint -f compact --disable-rule=terraform_required_providers --disable-rule=terraform_required_version __ + + # Connects to remote state backend and download providers. + - name: Terraform Init + working-directory: ${{ env.TERRAFORM_DIRECTORY }} + run: | + terraform init \ + -backend-config="role_arn=${{ env.AWS_STATE_ROLE_ARN }}" \ + -backend-config="region=us-east-1" \ + -backend-config="workspace_key_prefix=accounts/${{ env.AWS_ACCOUNT_NUMBER }}/${{ github.repository }}" \ + -backend-config="key=state.tfstate" \ + -backend-config="dynamodb_table=global-tf-state-lock" + + # Set the Terraform Workspace to the current branch name. + - name: Set Terraform Workspace + working-directory: ${{ env.TERRAFORM_DIRECTORY }} + shell: bash + run: | + export WORKSPACE=${{ github.base_ref || github.ref_name }} + terraform workspace select ${WORKSPACE} || terraform workspace new $WORKSPACE + echo "TF_WORKSPACE=$(echo ${WORKSPACE} | sed 's/\//_/g')" >> $GITHUB_ENV + + # Checks differences between current code and infrastructure state. + - name: Terraform Plan + id: plan + working-directory: ${{ env.TERRAFORM_DIRECTORY }} + run: | + terraform plan \ + -input=false \ + -no-color \ + -out=tfplan \ + -parallelism=${TERRAFORM_PARALLELISM} \ + -var-file=variables-${TF_WORKSPACE}.tfvars + + # Gets the results of the plan for pull requests. + - name: Terraform Show Plan + id: show + working-directory: ${{ env.TERRAFORM_DIRECTORY }} + run: terraform show -no-color tfplan + + # Adds the results of the plan to the pull request. + - name: Comment Plan + uses: actions/github-script@v6 + if: github.event_name == 'pull_request' + env: + STDOUT: "```terraform\n${{ steps.show.outputs.stdout }}```" + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + // 1. Retrieve existing bot comments for the PR + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }) + const botComment = comments.find(comment => { + return comment.user.type === 'Bot' && comment.body.includes('Terraform Format and Style') + }) + + // 2. Prepare format of the comment + const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\` + #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\` + #### Terraform Validation 🤖\`${{ steps.validate.outcome }}\` +
Validation Output + + \`\`\`\n + ${{ steps.validate.outputs.stdout }} + \`\`\` + +
+ + #### Terraform Plan 📖\`${{ steps.plan.outcome }}\` + +
Show Plan + + \`\`\`\n + ${process.env.PLAN} + \`\`\` + +
+ + *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`, Working Directory: \`${{ env.tf_actions_working_dir }}\`, Workflow: \`${{ github.workflow }}\`*`; + + // 3. If we have a comment, update it, otherwise create a new one + if (botComment) { + github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: output + }) + } else { + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + } + + # Downloads Regula and checks whether the plan meets compliance requirements. + - name: Regula Compliance Check + shell: bash + working-directory: ${{ env.TERRAFORM_DIRECTORY }} + run: | + REGULA_URL="https://github.com/fugue/regula/releases/download/v${REGULA_VERSION}/regula_${REGULA_VERSION}_Linux_x86_64.tar.gz" + curl -sL "$REGULA_URL" -o regula.tar.gz + tar xzf regula.tar.gz + terraform show -json tfplan | ./regula run + + # Deploys infrastructure or changes to infrastructure. + - name: Terraform Apply + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + working-directory: ${{ env.TERRAFORM_DIRECTORY }} + run: | + terraform apply \ + -auto-approve \ + -input=false \ + -parallelism=${TERRAFORM_PARALLELISM} \ + tfplan diff --git a/modules/services/sshd.nix b/modules/services/sshd.nix new file mode 100644 index 0000000..0161aba --- /dev/null +++ b/modules/services/sshd.nix @@ -0,0 +1,30 @@ +{ config, pkgs, lib, ... }: { + + options = { + publicKey = lib.mkOption { + type = lib.types.str; + description = "Public SSH key authorized for this system."; + }; + permitRootLogin = lib.mkOption { + type = lib.types.str; + description = "Root login settings."; + default = "no"; + }; + }; + + config = { + services.openssh = { + enable = true; + ports = [ 22 ]; + passwordAuthentication = false; + gatewayPorts = "no"; + forwardX11 = false; + allowSFTP = true; + permitRootLogin = config.permitRootLogin; + }; + + users.users.${config.user}.openssh.authorizedKeys.keys = + [ config.publicKey ]; + }; + +}