├── .ansible-lint ├── .github └── workflows │ ├── ansible-lint.yaml │ ├── ansible-proxmox.yaml │ ├── ansible-vyos.yaml │ ├── tailscale-acl.yaml │ ├── terraform-lint-vyos.yaml │ └── terraform-vyos.yaml ├── .gitignore ├── .gitmodules ├── LICENSE ├── Makefile ├── README.md ├── ansible ├── Makefile ├── ansible.cfg ├── host_vars │ └── vyos.yml ├── inventory.yml ├── playbooks │ ├── files │ │ └── lab-astr0rack-net.zone │ ├── proxmox.yml │ ├── tailscale_setup.yml │ ├── templates │ │ ├── discord-init.sh │ │ └── named.conf │ ├── vyos.yml │ └── vyos_prep.yml ├── requirements.yml ├── roles │ └── proxmox │ │ ├── files │ │ ├── 80-shutdown-on-battery.rules │ │ ├── Caddyfile │ │ ├── apt-hook-nag │ │ ├── discord-notif.service │ │ ├── pve-nag-buster.sh │ │ ├── sshd_config │ │ └── vyos-cloud-config.yml │ │ ├── handlers │ │ └── main.yml │ │ ├── tasks │ │ ├── bw.yml │ │ ├── create_vyos_image.yml │ │ ├── deploy_containers.yml │ │ ├── env.yml │ │ ├── general_setup.yml │ │ ├── main.yml │ │ ├── patch_proxmox.yml │ │ ├── setup_nics.yml │ │ ├── setup_oidc.yml │ │ ├── setup_perms.yml │ │ └── setup_terraform.yml │ │ ├── templates │ │ ├── caddy.yaml │ │ ├── dex-config.yaml │ │ ├── dex.yaml │ │ ├── discord-init.sh │ │ ├── nics.conf │ │ ├── postgres.yaml │ │ └── watchtower.yaml │ │ └── vars │ │ ├── bw.yml │ │ ├── env.yml │ │ └── main.yml └── vars │ ├── bw.yml │ └── env.yml ├── docs └── img │ ├── lab.excalidraw │ └── lab.png ├── tailscale └── acl.hujson └── terraform └── vyos ├── .terraform.lock.hcl ├── Makefile ├── main.tf ├── scripts └── local.sh ├── variables.tf └── vyos.tf /.ansible-lint: -------------------------------------------------------------------------------- 1 | --- 2 | exclude_paths: 3 | - ansible/roles/*/files 4 | - ansible/roles/*/templates 5 | -------------------------------------------------------------------------------- /.github/workflows/ansible-lint.yaml: -------------------------------------------------------------------------------- 1 | name: "Ansible Lint" 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - 'ansible/**' 7 | workflow_dispatch: 8 | 9 | 10 | jobs: 11 | ansible-lint: 12 | name: "Ansible Lint" 13 | runs-on: ubuntu-latest 14 | container: 15 | image: ghcr.io/lab-astr0rack-net/ansible:latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | 20 | - name: Run ansible-lint 21 | env: 22 | ANSIBLE_ROLES_PATH: 'ansible/roles:$ANSIBLE_HOME/roles:/usr/share/ansible/roles:/etc/ansible/roles' 23 | run: | 24 | git config --global --add safe.directory /__w/core/core 25 | ansible-lint -x 'var-naming[no-role-prefix]' ansible/ 26 | -------------------------------------------------------------------------------- /.github/workflows/ansible-proxmox.yaml: -------------------------------------------------------------------------------- 1 | name: "Proxmox Ansible Configuration Apply" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'ansible/roles/proxmox/**' 9 | - 'ansible/playbooks/proxmox.yml' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | ansible: 14 | name: "Ansible" 15 | runs-on: ubuntu-latest 16 | container: 17 | image: ghcr.io/lab-astr0rack-net/ansible:latest 18 | options: --cap-add=NET_ADMIN --cap-add=NET_RAW --dns=1.1.1.1 --dns=100.100.100.100 --add-host proxmox.lab.astr0rack.net:192.168.55.2 19 | volumes: 20 | - /var/lib:/var/lib 21 | - /dev/net/tun:/dev/net/tun 22 | defaults: 23 | run: 24 | working-directory: ansible 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Setup ssh key 30 | env: 31 | SSH_KEY: ${{ secrets.ANSIBLE_SSH_KEY }} 32 | run: | 33 | mkdir -p ~/.ssh 34 | chmod 700 ~/.ssh 35 | echo "$SSH_KEY" > ~/.ssh/id_ed25519 36 | chmod 400 ~/.ssh/id_ed25519 37 | 38 | - name: Tailscale 39 | uses: tailscale/github-action@v1 40 | id: ts-login 41 | with: 42 | authkey: ${{ secrets.TAILSCALE_AUTHKEY }} 43 | 44 | - name: Ping proxmox 45 | run: ansible --ssh-extra-args '"-o StrictHostKeyChecking=accept-new"' -i inventory.yml -m ping proxmox 46 | 47 | - name: Run the proxmox playbook 48 | env: 49 | ANSIBLE_BECOME_PASSWORD: ${{ secrets.ANSIBLE_BECOME_PASSWORD }} 50 | TF_PROXMOX_PASSWORD: ${{ secrets.TF_PROXMOX_PASSWORD }} 51 | TF_POSTGRES_PASSWORD: ${{ secrets.TF_POSTGRES_PASSWORD }} 52 | PM_CLOUDFLARE_API: ${{ secrets.PM_CLOUDFLARE_API }} 53 | PM_DISCORD_WEBHOOK_URL: ${{ secrets.PM_DISCORD_WEBHOOK_URL }} 54 | WT_DISCORD_WEBHOOK_URL: ${{ secrets.WT_DISCORD_WEBHOOK_URL }} 55 | TF_KEYCLOAK_ADMIN_PASSWORD: ${{ secrets.TF_KEYCLOAK_ADMIN_PASSWORD }} 56 | ANSIBLE_DEX_PROXMOX_CLIENT_SECRET: ${{ secrets.ANSIBLE_DEX_PROXMOX_CLIENT_SECRET }} 57 | ANSIBLE_DEX_GITHUB_CLIENT_ID: ${{ secrets.ANSIBLE_DEX_GITHUB_CLIENT_ID }} 58 | ANSIBLE_DEX_GITHUB_CLIENT_SECRET: ${{ secrets.ANSIBLE_DEX_GITHUB_CLIENT_SECRET }} 59 | run: ansible-playbook playbooks/proxmox.yml --tags all,env --extra-vars "ansible_become_password=$ANSIBLE_BECOME_PASSWORD" 60 | 61 | - name: Logout of Tailscale 62 | id: ts-logout 63 | if: steps.ts-login.conclusion == 'success' 64 | run: sudo tailscale logout 65 | 66 | - name: Notify discord 67 | uses: up9cloud/action-notify@v0.4 68 | if: cancelled() == false 69 | env: 70 | GITHUB_JOB_STATUS: ${{ job.status }} 71 | DISCORD_WEBHOOK_URL: ${{ secrets.LAB_DISCORD_WEBHOOK }} 72 | -------------------------------------------------------------------------------- /.github/workflows/ansible-vyos.yaml: -------------------------------------------------------------------------------- 1 | name: "VyOS Ansible Configuration Apply" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'ansible/roles/vyos/**' 9 | - 'ansible/host_vars/vyos.yml' 10 | - 'ansible/playbooks/vyos.yml' 11 | workflow_dispatch: 12 | 13 | jobs: 14 | ansible: 15 | name: "Ansible" 16 | runs-on: ubuntu-latest 17 | container: 18 | image: ghcr.io/lab-astr0rack-net/ansible:latest 19 | options: --cap-add=NET_ADMIN --cap-add=NET_RAW --dns=1.1.1.1 --dns=100.100.100.100 --add-host vyos.lab.astr0rack.net:192.168.55.3 20 | volumes: 21 | - /var/lib:/var/lib 22 | - /dev/net/tun:/dev/net/tun 23 | defaults: 24 | run: 25 | working-directory: ansible 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v3 29 | with: 30 | submodules: recursive 31 | 32 | - name: Setup ssh key 33 | env: 34 | SSH_KEY: ${{ secrets.ANSIBLE_SSH_KEY }} 35 | run: | 36 | mkdir -p ~/.ssh 37 | chmod 700 ~/.ssh 38 | echo "$SSH_KEY" > ~/.ssh/id_ed25519 39 | chmod 400 ~/.ssh/id_ed25519 40 | 41 | - name: Tailscale 42 | uses: tailscale/github-action@v1 43 | id: ts-login 44 | with: 45 | authkey: ${{ secrets.TAILSCALE_AUTHKEY }} 46 | 47 | - name: Ping vyos 48 | run: ansible --ssh-extra-args '"-o StrictHostKeyChecking=accept-new"' -i inventory.yml -m ping vyos 49 | 50 | - name: Run the vyos playbook 51 | env: 52 | ANSIBLE_BECOME_PASSWORD: ${{ secrets.ANSIBLE_BECOME_PASSWORD }} 53 | VY_LAB_DNS_KEY: ${{ secrets.VY_LAB_DNS_KEY }} 54 | VY_DISCORD_WEBHOOK: ${{ secrets.VY_DISCORD_WEBHOOK }} 55 | VY_TAILSCALE_AUTH_KEY: ${{ secrets.VY_TAILSCALE_AUTH_KEY }} 56 | VY_USER_PASSWORD_HASH: ${{ secrets.VY_USER_PASSWORD_HASH }} 57 | run: ansible-playbook playbooks/vyos.yml --tags all,env --extra-vars "ansible_become_password=$ANSIBLE_BECOME_PASSWORD" 58 | 59 | - name: Logout of Tailscale 60 | if: steps.ts-login.conclusion == 'success' 61 | id: ts-logout 62 | run: sudo tailscale logout 63 | 64 | - name: Notify discord 65 | uses: up9cloud/action-notify@v0.4 66 | if: cancelled() == false 67 | env: 68 | GITHUB_JOB_STATUS: ${{ job.status }} 69 | DISCORD_WEBHOOK_URL: ${{ secrets.LAB_DISCORD_WEBHOOK }} 70 | -------------------------------------------------------------------------------- /.github/workflows/tailscale-acl.yaml: -------------------------------------------------------------------------------- 1 | name: Sync Tailscale ACLs 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | paths: 7 | - 'tailscale/**' 8 | pull_request: 9 | branches: [ "main" ] 10 | paths: 11 | - 'tailscale/**' 12 | 13 | jobs: 14 | acls: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Deploy ACL 21 | if: github.event_name == 'push' 22 | id: deploy-acl 23 | uses: tailscale/gitops-acl-action@v1 24 | with: 25 | api-key: ${{ secrets.TS_API_KEY }} 26 | tailnet: ${{ secrets.TS_TAILNET }} 27 | policy-file: tailscale/acl.hujson 28 | action: apply 29 | 30 | - name: Test ACL 31 | if: github.event_name == 'pull_request' 32 | id: test-acl 33 | uses: tailscale/gitops-acl-action@v1 34 | with: 35 | api-key: ${{ secrets.TS_API_KEY }} 36 | tailnet: ${{ secrets.TS_TAILNET }} 37 | policy-file: tailscale/acl.hujson 38 | action: test 39 | 40 | - name: Notify discord 41 | uses: up9cloud/action-notify@v0.4 42 | if: cancelled() == false 43 | env: 44 | GITHUB_JOB_STATUS: ${{ job.status }} 45 | DISCORD_WEBHOOK_URL: ${{ secrets.LAB_DISCORD_WEBHOOK }} 46 | -------------------------------------------------------------------------------- /.github/workflows/terraform-lint-vyos.yaml: -------------------------------------------------------------------------------- 1 | name: "Terraform Plan VyOS" 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - 'terraform/vyos/**' 7 | workflow_dispatch: 8 | 9 | concurrency: terraform 10 | 11 | jobs: 12 | terraform: 13 | name: "Terraform" 14 | runs-on: ubuntu-latest 15 | container: 16 | image: ghcr.io/lab-astr0rack-net/terraform:latest 17 | options: --cap-add=NET_ADMIN --cap-add=NET_RAW --dns=1.1.1.1 --dns=100.100.100.100 18 | volumes: 19 | - /var/lib:/var/lib 20 | - /dev/net/tun:/dev/net/tun 21 | defaults: 22 | run: 23 | working-directory: terraform/vyos 24 | permissions: 25 | contents: read 26 | issues: write 27 | pull-requests: write 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v3 31 | 32 | - name: Setup Terraform 33 | uses: hashicorp/setup-terraform@v1 34 | 35 | - name: Terraform Format 36 | id: fmt 37 | run: terraform fmt -check 38 | 39 | - name: Tailscale 40 | uses: tailscale/github-action@v1 41 | id: ts-login 42 | with: 43 | authkey: ${{ secrets.TAILSCALE_AUTHKEY }} 44 | 45 | - name: Terraform Init 46 | env: 47 | PG_CONN_STR: ${{ secrets.TF_POSTGRES_CRED }} 48 | PG_SCHEMA_NAME: vyos 49 | id: init 50 | run: terraform init 51 | 52 | - name: Terraform Validate 53 | id: validate 54 | run: terraform validate -no-color 55 | 56 | - name: Terraform Plan 57 | id: plan 58 | env: 59 | TF_VAR_pm_password: ${{ secrets.TF_PROXMOX_PASSWORD }} 60 | PG_CONN_STR: ${{ secrets.TF_POSTGRES_CRED }} 61 | PG_SCHEMA_NAME: vyos 62 | run: terraform plan -no-color -input=false 63 | continue-on-error: true 64 | 65 | - uses: actions/github-script@v6 66 | env: 67 | PLAN: "terraform\n${{ steps.plan.outputs.stdout }}" 68 | with: 69 | script: | 70 | const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\` 71 | #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\` 72 | #### Terraform Validation 🤖\`${{ steps.validate.outcome }}\` 73 | #### Terraform Plan 📖\`${{ steps.plan.outcome }}\` 74 | 75 |
Show Plan 76 | 77 | \`\`\`\n 78 | ${process.env.PLAN} 79 | \`\`\` 80 | 81 |
82 | 83 | *Pushed by: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`; 84 | 85 | github.rest.issues.createComment({ 86 | issue_number: context.issue.number, 87 | owner: context.repo.owner, 88 | repo: context.repo.repo, 89 | body: output 90 | }) 91 | 92 | - name: Terraform Plan Status 93 | if: steps.plan.outcome == 'failure' 94 | run: exit 1 95 | 96 | - name: Logout of Tailscale 97 | id: ts-logout 98 | if: steps.ts-login.conclusion == 'success' 99 | run: sudo tailscale logout 100 | 101 | -------------------------------------------------------------------------------- /.github/workflows/terraform-vyos.yaml: -------------------------------------------------------------------------------- 1 | name: "Terraform Apply VyOS" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'terraform/vyos/**' 9 | workflow_dispatch: 10 | 11 | concurrency: terraform 12 | 13 | jobs: 14 | terraform: 15 | name: "Terraform" 16 | runs-on: ubuntu-latest 17 | container: 18 | image: ghcr.io/lab-astr0rack-net/terraform:latest 19 | options: --cap-add=NET_ADMIN --cap-add=NET_RAW --dns=1.1.1.1 --dns=100.100.100.100 --add-host postgres.lab.astr0rack.net:192.168.55.2 --add-host proxmox.lab.astr0rack.net:192.168.55.2 20 | volumes: 21 | - /var/lib:/var/lib 22 | - /dev/net/tun:/dev/net/tun 23 | defaults: 24 | run: 25 | working-directory: terraform/vyos 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v3 29 | 30 | - name: Setup Terraform 31 | uses: hashicorp/setup-terraform@v1 32 | 33 | - name: Terraform Format 34 | id: fmt 35 | run: terraform fmt -check 36 | 37 | - name: Tailscale 38 | uses: tailscale/github-action@v1 39 | id: ts-login 40 | with: 41 | authkey: ${{ secrets.TAILSCALE_AUTHKEY }} 42 | 43 | - name: Terraform Init 44 | env: 45 | PG_CONN_STR: ${{ secrets.TF_POSTGRES_CRED }} 46 | PG_SCHEMA_NAME: vyos 47 | id: init 48 | run: terraform init 49 | 50 | - name: Terraform Validate 51 | id: validate 52 | run: terraform validate -no-color 53 | 54 | - name: Terraform Apply 55 | if: (github.ref == 'refs/heads/main' ) && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') 56 | env: 57 | TF_VAR_pm_password: ${{ secrets.TF_PROXMOX_PASSWORD }} 58 | PG_CONN_STR: ${{ secrets.TF_POSTGRES_CRED }} 59 | PG_SCHEMA_NAME: vyos 60 | run: terraform apply -auto-approve -input=false 61 | 62 | - name: Logout of Tailscale 63 | id: ts-logout 64 | if: steps.ts-login.conclusion == 'success' 65 | run: sudo tailscale logout 66 | 67 | - name: Notify discord 68 | uses: up9cloud/action-notify@v0.4 69 | if: cancelled() == false 70 | env: 71 | GITHUB_JOB_STATUS: ${{ job.status }} 72 | DISCORD_WEBHOOK_URL: ${{ secrets.LAB_DISCORD_WEBHOOK }} 73 | 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | # Crash log files 9 | crash.log 10 | crash.*.log 11 | 12 | # Exclude all .tfvars files, which are likely to contain sensitive data, such as 13 | # password, private keys, and other secrets. These should not be part of version 14 | # control as they are data points which are potentially sensitive and subject 15 | # to change depending on the environment. 16 | *.tfvars 17 | *.tfvars.json 18 | 19 | # Ignore override files as they are usually used to override resources locally and so 20 | # are not checked in 21 | override.tf 22 | override.tf.json 23 | *_override.tf 24 | *_override.tf.json 25 | 26 | # Include override files you do wish to add to version control using negated pattern 27 | # !example_override.tf 28 | 29 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 30 | # example: *tfplan* 31 | 32 | # Ignore CLI configuration files 33 | .terraformrc 34 | terraform.rc 35 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ansible/roles/vyos"] 2 | path = ansible/roles/vyos 3 | url = git@github.com:astr0n8t/vyos-ansible.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 lab-astr0rack-net 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: proxmox vyos 2 | proxmox: 3 | $(MAKE) -C ansible proxmox 4 | vyos: 5 | $(MAKE) -C terraform all 6 | $(MAKE) -C ansible vyos 7 | lint: 8 | terraform fmt 9 | ANSIBLE_ROLES_PATH='ansible/roles:$$ANSIBLE_HOME/roles:/usr/share/ansible/roles:/etc/ansible/roles' ansible-lint ansible/ 10 | clean: 11 | $(MAKE) -C terraform clean 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # core 2 | 3 | A Infastructure as Code network security/development lab built using [Proxmox](https://www.proxmox.com/), [VyOS](https://vyos.io/), and [Tailscale](https://tailscale.com/). 4 | 5 | ## Diagram 6 | 7 | This repository holds the configuration for the core of the lab: 8 | 9 | ![diagram](docs/img/lab.png) 10 | 11 | ## Proxmox 12 | 13 | The Proxmox host is provisioned using Ansible and is meant to turn a Debian 12 host into a Proxmox VE 8 server. 14 | 15 | The webui is hosted behind a Caddy container with Let's Encrypt TLS. 16 | 17 | This also builds a Postgres container for holding Terraform state. 18 | 19 | Dex is included for SSO with GitHub and adds a OIDC realm to Proxmox. 20 | It also patches Proxmox so that any user authenticated via GitHub (or any OIDC realm) is added to the github (or name of the oidc realm) group for permissions management. 21 | 22 | Watchtower is also included for automatic container updates. 23 | 24 | ## VyOS 25 | 26 | The VyOS VM is provisioned using Terraform and Ansible. It serves as the core firewall of the lab, performing routing and DNS functions. 27 | 28 | The DNS server is a Bind9 container which allows for DNS records to be updated on the fly with Terraform. This functionality will be utilized in other repositories for ephemeral VM deployment and destruction. 29 | 30 | The VyOS image being used is a custom one built with Github Actions here: https://github.com/astr0n8t/vyos-tailscale-gha/ (This image includes cloud-init support as well as Tailscale built in) 31 | 32 | ## Tailscale 33 | 34 | Tailscale is initialized from the VyOS core router and allows for external users to access the Lab and Dev networks. It also allows for GitHub Actions to connect to the tailnet via ephemeral nodes and apply configurations automatically. 35 | 36 | ## Contributors 37 | 38 | - [@astr0n8t](https://github.com/astr0n8t) 39 | -------------------------------------------------------------------------------- /ansible/Makefile: -------------------------------------------------------------------------------- 1 | proxmox: 2 | ansible-playbook playbooks/proxmox.yml --tags all,bw --ask-become-pass 3 | proxmox-setup: 4 | ansible-playbook playbooks/proxmox.yml --tags setup,bw --ask-become-pass 5 | proxmox-containers: 6 | ansible-playbook playbooks/proxmox.yml --tags containers,bw --ask-become-pass 7 | proxmox-nics: 8 | ansible-playbook playbooks/proxmox.yml --tags setup-nics,bw --ask-become-pass 9 | proxmox-oidc: 10 | ansible-playbook playbooks/proxmox.yml --tags setup-oidc,bw --ask-become-pass 11 | proxmox-patch: 12 | ansible-playbook playbooks/proxmox.yml --tags patch,bw --ask-become-pass 13 | proxmox-perms: 14 | ansible-playbook playbooks/proxmox.yml --tags perms,bw --ask-become-pass 15 | proxmox-vi: 16 | ansible-playbook playbooks/proxmox.yml --tags vyos-image,bw --ask-become-pass 17 | proxmox-tf: 18 | ansible-playbook playbooks/proxmox.yml --tags setup-terraform,bw --ask-become-pass 19 | 20 | vyos: 21 | ansible-playbook playbooks/vyos.yml --tags all 22 | vyos-dry-run: 23 | ansible-playbook playbooks/vyos.yml --tags dry-run,bw --skip-tags always 24 | vyos-prep: 25 | ansible-playbook playbooks/vyos.yml --tags prep,bw 26 | vyos-users: 27 | ansible-playbook playbooks/vyos.yml --tags users,bw 28 | vyos-vrf: 29 | ansible-playbook playbooks/vyos.yml --tags vrf,bw 30 | vyos-interfaces: 31 | ansible-playbook playbooks/vyos.yml --tags interfaces,bw 32 | vyos-services: 33 | ansible-playbook playbooks/vyos.yml --tags services,bw 34 | vyos-dhcp: 35 | ansible-playbook playbooks/vyos.yml --tags dhcp,bw 36 | vyos-routing: 37 | ansible-playbook playbooks/vyos.yml --tags routing,bw 38 | vyos-nat: 39 | ansible-playbook playbooks/vyos.yml --tags nat,bw 40 | vyos-firewall: 41 | ansible-playbook playbooks/vyos.yml --tags firewall,bw 42 | vyos-containers: 43 | ansible-playbook playbooks/vyos.yml --tags containers,bw 44 | 45 | lint: 46 | ansible-lint -x 'var-naming[no-role-prefix]' 47 | -------------------------------------------------------------------------------- /ansible/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | inventory = inventory.yml 3 | host_key_checking = False 4 | roles_path = roles 5 | [persistent_connection] 6 | command_timeout = 300 7 | -------------------------------------------------------------------------------- /ansible/host_vars/vyos.yml: -------------------------------------------------------------------------------- 1 | --- 2 | users: 3 | - name: vyos 4 | pw_hash: "{{ vyos_user_hash }}" 5 | public_keys: 6 | - name: astr0n8t@primary.yubikey 7 | type: sk-ssh-ed25519@openssh.com 8 | key: AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIFEhLIyleqCaN3lEJ77AYc/q1uZlqsDJ4PKhBu0dXUa7AAAABHNzaDo= 9 | - name: astr0n8t@secondary.yubikey 10 | type: sk-ssh-ed25519@openssh.com 11 | key: AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIHKBaozeIJ+Gz7+J2dK2VrQPaWerVWH8a9xYIjhvygLmAAAABHNzaDo= 12 | - name: astr0n8t@mobile 13 | type: ecdsa-sha2-nistp256 14 | key: AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGwVUk4wWGiJQjd7P2z2XZ9Gzia0GTy2faoAnyWVEF+jweR2q16C75oIzZbgE1mN3oc4BD9z8rIA1ElX 15 | - name: astr0n8t@laptop 16 | type: ecdsa-sha2-nistp256 17 | key: AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAS4p259ptqrLRvV5A8Ytx90IIyQkhGrbe3PBF4evUrrwVe1pHHUudh2tr00k4S1DTi6RtuZUEU+7aQS2ZLNmv4= 18 | - name: ghactions@github.com 19 | type: ssh-ed25519 20 | key: AAAAC3NzaC1lZDI1NTE5AAAAIH8ts84bR4nen8+Kk6I48MDkg8WA+gnuoUFuCmG9B6Xg 21 | interfaces: 22 | - name: eth0 23 | type: ethernet 24 | desc: Rack 25 | mac: 92:07:2b:55:57:c8 26 | ipv4_addr: dhcp 27 | zone: external-rack 28 | - name: eth1 29 | type: ethernet 30 | desc: LABDC 31 | mac: aa:06:c0:1f:48:d1 32 | ipv4_addr: 10.0.0.1/24 33 | dhcp: 34 | subnet: 10.0.0.0/24 35 | gw: 10.0.0.1 36 | dns_servers: 37 | - 10.0.0.1 38 | domain_name: lab.astr0rack.net 39 | start: 10.0.0.100 40 | stop: 10.0.0.200 41 | static_mappings: 42 | - name: dc-01 43 | mac: 22:ca:a4:90:bb:04 44 | ipv4: 10.0.0.50 45 | - name: dc-02 46 | mac: ea:87:14:1a:6d:7f 47 | ipv4: 10.0.0.51 48 | - name: eth2 49 | type: ethernet 50 | desc: LABDMZ 51 | mac: a6:7e:41:70:6b:ca 52 | ipv4_addr: 10.0.10.1/24 53 | dhcp: 54 | subnet: 10.0.10.0/24 55 | gw: 10.0.10.1 56 | dns_servers: 57 | - 10.0.10.1 58 | domain_name: lab.astr0rack.net 59 | start: 10.0.10.100 60 | stop: 10.0.10.200 61 | static_mappings: 62 | - name: dns-01 63 | mac: 36:aa:9e:ec:02:de 64 | ipv4: 10.0.10.10 65 | - name: web-01 66 | mac: 92:65:8b:40:2a:7e 67 | ipv4: 10.0.10.20 68 | - name: eth3 69 | type: ethernet 70 | desc: LABLAN 71 | mac: ba:e1:dc:d6:d5:80 72 | ipv4_addr: 10.0.20.1/24 73 | dhcp: 74 | subnet: 10.0.20.0/24 75 | gw: 10.0.20.1 76 | dns_servers: 77 | - 10.0.20.1 78 | domain_name: lab.astr0rack.net 79 | start: 10.0.20.10 80 | stop: 10.0.20.200 81 | - name: eth4 82 | type: ethernet 83 | desc: LABC2 84 | mac: a6:3e:71:ea:29:0d 85 | ipv4_addr: 10.0.255.1/24 86 | dhcp: 87 | subnet: 10.0.255.0/24 88 | gw: 10.0.255.1 89 | dns_servers: 90 | - 10.0.255.1 91 | domain_name: lab.astr0rack.net 92 | start: 10.0.255.150 93 | stop: 10.0.255.200 94 | static_mappings: 95 | - name: c2 96 | mac: 1a:f4:ef:e2:f6:fe 97 | ipv4: 10.0.255.100 98 | - name: eth5 99 | type: ethernet 100 | desc: TEMPLATENET 101 | mac: 86:bf:48:80:96:3b 102 | ipv4_addr: 10.0.1.1/24 103 | dhcp: 104 | subnet: 10.0.1.0/24 105 | gw: 10.0.1.1 106 | dns_servers: 107 | - 1.1.1.1 108 | domain_name: lab.astr0rack.net 109 | start: 10.0.1.10 110 | stop: 10.0.1.200 111 | - name: eth6 112 | type: ethernet 113 | desc: SWAN 114 | mac: d2:dc:b9:42:47:4f 115 | ipv4_addr: 192.168.230.1/24 116 | dhcp: 117 | subnet: 192.168.230.0/24 118 | gw: 192.168.230.1 119 | dns_servers: 120 | - 192.168.230.1 121 | domain_name: lab.astr0rack.net 122 | start: 192.168.230.10 123 | stop: 192.168.230.200 124 | - name: eth7 125 | type: ethernet 126 | desc: DEVNET 127 | mac: 56:e0:3a:73:1a:1f 128 | ipv4_addr: 10.100.0.1/24 129 | dhcp: 130 | subnet: 10.100.0.0/24 131 | gw: 10.100.0.1 132 | dns_servers: 133 | - 10.100.0.1 134 | domain_name: lab.astr0rack.net 135 | start: 10.100.0.10 136 | stop: 10.100.0.200 137 | static_mappings: 138 | - name: guacamole 139 | mac: 06:de:fb:c7:72:2d 140 | ipv4: 10.100.0.2 141 | - name: dev 142 | mac: fe:5f:66:7b:7a:65 143 | ipv4: 10.100.0.3 144 | - name: sidewinder 145 | mac: 8a:86:b4:cc:19:de 146 | ipv4: 10.100.0.4 147 | - name: frostbite 148 | mac: 1a:12:9d:5c:26:52 149 | ipv4: 10.100.0.5 150 | - name: sec660-win 151 | mac: bc:24:11:8a:69:96 152 | ipv4: 10.100.0.6 153 | - name: gpu-test 154 | mac: bc:24:11:96:7d:07 155 | ipv4: 10.100.0.7 156 | static_routes: 157 | - subnet: 0.0.0.0/0 158 | dhcp_interface: eth0 159 | source_nat: 160 | - outbound_interface: eth0 161 | source_address: 10.0.0.0/16 162 | translation_address: masquerade 163 | - outbound_interface: eth0 164 | source_address: 192.168.230.0/24 165 | translation_address: masquerade 166 | - source_address: 10.0.0.0/8 167 | destination_address: '192.168.55.2-192.168.55.254' 168 | translation_address: masquerade 169 | - outbound_interface: eth0 170 | source_address: 10.0.0.0/8 171 | destination_address: '!192.168.0.0/16' 172 | translation_address: masquerade 173 | dns: 174 | search_domain: lab.astr0rack.net 175 | servers: 176 | - 1.1.1.1 177 | - 1.0.0.1 178 | host_name: vyos-core 179 | domain_name: lab.astr0rack.net 180 | ntp: 181 | listen_address: 0.0.0.0 182 | servers: 183 | - 0.pool.ntp.org 184 | - 1.pool.ntp.org 185 | - 2.pool.ntp.org 186 | allowed_clients: 187 | - 10.0.0.0/8 188 | ssh: 189 | disable_password_authentication: true 190 | disable_host_validation: true 191 | listen_address: 0.0.0.0 192 | firewall_global_options: 193 | - name: all-ping 194 | setting: enable 195 | - name: state-policy related action 196 | setting: accept 197 | - name: state-policy established action 198 | setting: accept 199 | - name: state-policy invalid action 200 | setting: drop 201 | firewall_address_groups: 202 | - name: dc-01 203 | ip: 10.0.0.50 204 | groups: 205 | - dc-01 206 | - lab-dc 207 | - name: dc-02 208 | ip: 10.0.0.51 209 | groups: 210 | - dc-02 211 | - lab-dc 212 | - name: dns-01 213 | ip: 10.0.10.10 214 | groups: 215 | - dns-01 216 | - lab-server 217 | - name: web-01 218 | ip: 10.0.10.20 219 | groups: 220 | - web-01 221 | - lab-server 222 | - name: c2 223 | ip: 10.0.255.100 224 | groups: 225 | - c2 226 | - lab-c2 227 | - name: external-gw 228 | ip: 192.168.55.1 229 | groups: 230 | - external-gw 231 | - lab-external 232 | - name: external-pm 233 | ip: 192.168.55.2 234 | groups: 235 | - external-pm 236 | - lab-external 237 | - lab-reachable-tailscale 238 | - name: external-vyos 239 | ip: 192.168.55.3 240 | groups: 241 | - external-vyos 242 | - lab-external 243 | - lab-reachable-tailscale 244 | - name: rack-space 245 | ip: 10.64.1.2-10.64.1.3 246 | groups: 247 | - rack-ip-space 248 | - name: lab-space 249 | ip: 10.0.0.0-10.0.255.255 250 | groups: 251 | - lab-ip-space 252 | - name: slab-space 253 | ip: 192.168.230.0-192.168.230.255 254 | groups: 255 | - lab-ip-space 256 | - name: tailscale-dns 257 | ip: 100.100.100.100 258 | groups: 259 | - tailscale-dns 260 | - name: dev-space 261 | ip: 192.168.55.4-192.168.55.254 262 | groups: 263 | - dev-space 264 | - name: dev-ip-space 265 | ip: 10.100.0.2-10.100.0.254 266 | groups: 267 | - dev-ip-space 268 | firewall_zones: 269 | - name: external-rack 270 | description: rack 271 | interfaces: 272 | - eth0 273 | - name: external-tailscale 274 | description: lab 275 | interfaces: 276 | - tailscale0 277 | - name: internal-lab 278 | description: lab 279 | interfaces: 280 | - eth1 281 | - eth2 282 | - eth3 283 | - eth4 284 | - eth6 285 | - name: template-net 286 | description: template 287 | interfaces: 288 | - eth5 289 | - name: dev-net 290 | description: dev-net 291 | interfaces: 292 | - eth7 293 | - name: local 294 | description: local 295 | interfaces: 296 | - local-zone 297 | firewall_rules: 298 | - name: rack_to_dev_ipv4 299 | assignments: 300 | - from: external-rack 301 | to: dev-net 302 | ipv4: 303 | - protocol: tcp_udp 304 | destination_port: 22000 305 | destination_address: 10.100.0.3 306 | action: accept 307 | - protocol: tcp 308 | destination_port: 3389 309 | action: accept 310 | - protocol: tcp 311 | destination_port: 22 312 | action: accept 313 | - protocol: all 314 | action: drop 315 | - name: rack_to_local_ipv4 316 | assignments: 317 | - from: external-rack 318 | to: local 319 | ipv4: 320 | - protocol: tcp 321 | destination_port: 22 322 | action: accept 323 | - protocol: tcp 324 | destination_port: 53 325 | action: accept 326 | - protocol: udp 327 | destination_port: 53 328 | action: accept 329 | - protocol: udp 330 | destination_port: 41641 331 | action: accept 332 | - protocol: icmp 333 | action: accept 334 | - protocol: all 335 | action: drop 336 | - name: lab_to_rack_ipv4 337 | assignments: 338 | - from: internal-lab 339 | to: external-rack 340 | ipv4: 341 | - source_group: 342 | type: address 343 | name: lab-ip-space 344 | destination_group: 345 | type: address 346 | name: rack-ip-space 347 | protocol: all 348 | action: drop 349 | - source_group: 350 | type: address 351 | name: lab-ip-space 352 | protocol: tcp_udp 353 | action: accept 354 | - protocol: all 355 | action: drop 356 | - name: dev_to_rack_ipv4 357 | assignments: 358 | - from: dev-net 359 | to: external-rack 360 | ipv4: 361 | - source_group: 362 | type: address 363 | name: dev-ip-space 364 | destination_group: 365 | type: address 366 | name: external-pm 367 | destination_port: 443 368 | protocol: tcp 369 | action: accept 370 | - source_group: 371 | type: address 372 | name: dev-ip-space 373 | destination_group: 374 | type: address 375 | name: rack-ip-space 376 | protocol: all 377 | action: accept 378 | - source_group: 379 | type: address 380 | name: dev-ip-space 381 | destination_group: 382 | type: address 383 | name: lab-ip-space 384 | protocol: all 385 | action: drop 386 | - protocol: tcp_udp 387 | action: accept 388 | - protocol: all 389 | action: drop 390 | - name: local_to_rack_ipv4 391 | assignments: 392 | - from: local 393 | to: external-rack 394 | ipv4: 395 | - destination_group: 396 | type: address 397 | name: lab-external 398 | protocol: tcp_udp 399 | action: accept 400 | - destination_group: 401 | type: address 402 | name: lab-external 403 | protocol: icmp 404 | action: accept 405 | - destination_group: 406 | type: address 407 | name: rack-ip-space 408 | protocol: all 409 | action: drop 410 | - protocol: tcp_udp 411 | action: accept 412 | - protocol: icmp 413 | action: accept 414 | - protocol: all 415 | action: drop 416 | - name: local_to_lab_ipv4 417 | assignments: 418 | - from: local 419 | to: internal-lab 420 | ipv4: 421 | - protocol: all 422 | action: accept 423 | - name: lab_to_local_ipv4 424 | assignments: 425 | - from: internal-lab 426 | to: local 427 | - from: dev-net 428 | to: local 429 | ipv4: 430 | - protocol: udp 431 | destination_port: 53 432 | action: accept 433 | - protocol: udp 434 | destination_port: 123 435 | action: accept 436 | - protocol: all 437 | action: drop 438 | - name: tailscale_to_lab_ipv4 439 | assignments: 440 | - from: external-tailscale 441 | to: internal-lab 442 | ipv4: 443 | - protocol: all 444 | action: accept 445 | - name: tailscale_to_external_rack_ipv4 446 | assignments: 447 | - from: external-tailscale 448 | to: external-rack 449 | ipv4: 450 | - destination_group: 451 | type: address 452 | name: external-pm 453 | protocol: tcp 454 | destination_port: 22 455 | action: accept 456 | - destination_group: 457 | type: address 458 | name: external-pm 459 | protocol: tcp 460 | destination_port: 443 461 | action: accept 462 | - destination_group: 463 | type: address 464 | name: external-pm 465 | protocol: tcp 466 | destination_port: 5432 467 | action: accept 468 | - destination_group: 469 | type: address 470 | name: dev-space 471 | protocol: tcp_udp 472 | action: accept 473 | - destination_group: 474 | type: address 475 | name: lab-reachable-tailscale 476 | protocol: icmp 477 | action: accept 478 | - protocol: all 479 | action: drop 480 | - name: tailscale_to_local_ipv4 481 | assignments: 482 | - from: external-tailscale 483 | to: local 484 | ipv4: 485 | - destination_group: 486 | type: address 487 | name: external-vyos 488 | protocol: tcp_udp 489 | destination_port: 53 490 | action: accept 491 | - destination_group: 492 | type: address 493 | name: external-vyos 494 | protocol: tcp 495 | destination_port: 22 496 | action: accept 497 | - protocol: icmp 498 | action: accept 499 | - protocol: all 500 | action: drop 501 | - name: local_to_tailscale_ipv4 502 | assignments: 503 | - from: local 504 | to: external-tailscale 505 | ipv4: 506 | - destination_group: 507 | type: address 508 | name: tailscale-dns 509 | protocol: tcp_udp 510 | destination_port: 53 511 | action: accept 512 | - protocol: icmp 513 | action: accept 514 | - protocol: all 515 | action: drop 516 | - name: tailscale_to_template_net_ipv4 517 | assignments: 518 | - from: external-tailscale 519 | to: template-net 520 | ipv4: 521 | - protocol: tcp 522 | destination_port: 22 523 | action: accept 524 | - protocol: icmp 525 | action: accept 526 | - protocol: all 527 | action: drop 528 | - name: template_net_to_tailscale_ipv4 529 | assignments: 530 | - from: template-net 531 | to: external-tailscale 532 | ipv4: 533 | - protocol: tcp_udp 534 | action: accept 535 | - protocol: icmp 536 | action: accept 537 | - protocol: all 538 | action: drop 539 | - name: template_to_rack_ipv4 540 | assignments: 541 | - from: template-net 542 | to: external-rack 543 | ipv4: 544 | - destination_group: 545 | type: address 546 | name: rack-ip-space 547 | protocol: all 548 | action: drop 549 | - source_group: 550 | type: address 551 | name: lab-ip-space 552 | protocol: tcp_udp 553 | action: accept 554 | - protocol: all 555 | action: drop 556 | containers: 557 | - name: bind9 558 | image: docker.io/ubuntu/bind9:latest 559 | network: allow-host-networks 560 | environment: 561 | - key: BIND9_USER 562 | value: root 563 | volumes: 564 | - name: config 565 | destination: /etc/bind 566 | source: /config/user-data/bind9/etc 567 | - name: cache 568 | destination: /var/cache/bind 569 | source: /config/user-data/bind9/cache 570 | - name: records 571 | destination: /var/lib/bind 572 | source: /config/user-data/bind9/records 573 | -------------------------------------------------------------------------------- /ansible/inventory.yml: -------------------------------------------------------------------------------- 1 | all: 2 | hosts: 3 | proxmox: 4 | ansible_user: ghactions 5 | ansible_host: proxmox.lab.astr0rack.net 6 | ansible_ssh_private_key_file: ~/.ssh/id_ed25519 7 | vyos: 8 | ansible_user: vyos 9 | ansible_host: vyos.lab.astr0rack.net 10 | ansible_connection: network_cli 11 | ansible_network_os: vyos 12 | ansible_python_interpreter: /usr/bin/python3 13 | ansible_ssh_private_key_file: ~/.ssh/id_ed25519 14 | -------------------------------------------------------------------------------- /ansible/playbooks/files/lab-astr0rack-net.zone: -------------------------------------------------------------------------------- 1 | $ORIGIN lab.astr0rack.net. 2 | $TTL 30m 3 | @ IN SOA ns.lab.astr0rack.net. info.lab.astr0rack.net. ( 4 | 2023061600 ; serial 5 | 1h ; refresh 6 | 15m ; retry 7 | 10m ; expire 8 | 10m ; minimum ttl 9 | ) 10 | 11 | IN NS ns.lab.astr0rack.net. 12 | 13 | ns IN A 192.168.55.3 14 | vyos IN A 192.168.55.3 15 | 16 | 17 | proxmox IN A 192.168.55.2 18 | postgres IN A 192.168.55.2 19 | auth IN A 192.168.55.2 20 | keycloak IN A 192.168.55.2 21 | 22 | guac IN A 192.168.55.4 23 | dev IN A 10.100.0.3 24 | sidewinder IN A 10.100.0.4 25 | sec660-win IN A 10.100.0.6 26 | gpu-test IN A 10.100.0.7 27 | 28 | -------------------------------------------------------------------------------- /ansible/playbooks/proxmox.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Run the proxmox role 3 | hosts: proxmox 4 | become: true 5 | roles: 6 | - role: proxmox 7 | -------------------------------------------------------------------------------- /ansible/playbooks/tailscale_setup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Setup tailscale on vyos 3 | hosts: vyos 4 | gather_facts: false 5 | become: true 6 | tasks: 7 | - name: Check tailscale status 8 | vyos.vyos.vyos_command: 9 | commands: 10 | - tailscale status 11 | register: tailscale_status 12 | 13 | - name: Login to tailscale and start 14 | vyos.vyos.vyos_command: 15 | commands: 16 | - "sudo tailscale login --auth-key={{ tailscale_auth_key }}" 17 | - "sudo tailscale up --advertise-routes=10.0.0.0/16,192.168.55.0/24" 18 | when: tailscale_status.stdout is search('Logged out') 19 | -------------------------------------------------------------------------------- /ansible/playbooks/templates/discord-init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | while [ -z "$TAILSCALE_STATUS" ] 4 | do 5 | TAILSCALE_STATUS=$(tailscale status | grep vyos | awk '{print $2" "$1}') 6 | sleep 5 7 | done 8 | 9 | WEBHOOK='{{ discord_webhook_url }}' 10 | 11 | DATA="{ 12 | \"embeds\": [ 13 | { 14 | \"author\": { 15 | \"name\": \"root\" 16 | }, 17 | \"title\": \"VyOS Lab Status\", 18 | \"description\": \"Status of VyOS on boot.\", 19 | \"color\": 15258703, 20 | \"fields\": [ 21 | { 22 | \"name\": \"tailscale status\", 23 | \"value\": \"\`\`\`$TAILSCALE_STATUS\`\`\`\" 24 | } 25 | ] 26 | } 27 | ] 28 | }" 29 | 30 | curl -s -X POST -H "Content-Type: application/json" -d "$DATA" "$WEBHOOK" 31 | -------------------------------------------------------------------------------- /ansible/playbooks/templates/named.conf: -------------------------------------------------------------------------------- 1 | acl internal { 2 | 10.0.0.0/8; 3 | 100.64.0.0/10; 4 | 192.168.0.0/16; 5 | }; 6 | 7 | key "lab-astr0rack-key" { 8 | algorithm hmac-sha256; 9 | secret "{{ lab_astr0rack_dns_key }}"; 10 | }; 11 | 12 | options { 13 | directory "/var/cache/bind"; 14 | forwarders { 15 | 1.1.1.1; 16 | 1.0.0.1; 17 | }; 18 | allow-query { internal; }; 19 | }; 20 | 21 | zone "lab.astr0rack.net" IN { 22 | type master; 23 | file "/etc/bind/lab-astr0rack-net.zone"; 24 | update-policy { grant lab-astr0rack-key zonesub any; }; 25 | }; 26 | -------------------------------------------------------------------------------- /ansible/playbooks/vyos.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Get variables from bitwarden 3 | hosts: vyos 4 | gather_facts: false 5 | tags: [never, bw] 6 | tasks: 7 | - name: Include variables from Bitwarden 8 | ansible.builtin.include_vars: '../vars/bw.yml' 9 | 10 | - name: Get variables from env 11 | hosts: vyos 12 | gather_facts: false 13 | tags: [never, env] 14 | tasks: 15 | - name: Include variables from environment 16 | ansible.builtin.include_vars: '../vars/env.yml' 17 | 18 | - name: Run vyos prep 19 | import_playbook: 'vyos_prep.yml' 20 | 21 | - name: Run vyos tailscale setup 22 | import_playbook: 'tailscale_setup.yml' 23 | 24 | - name: Run the VyOS role 25 | hosts: vyos 26 | become: true 27 | gather_facts: false 28 | roles: 29 | - role: vyos 30 | -------------------------------------------------------------------------------- /ansible/playbooks/vyos_prep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Prep vyos 3 | hosts: vyos 4 | gather_facts: false 5 | become: true 6 | tasks: 7 | - name: Set facts for ansible ssh 8 | ansible.builtin.set_fact: 9 | ansible_connection: ssh 10 | ansible_user: vyos 11 | 12 | - name: Copy discord notify script 13 | ansible.builtin.template: 14 | src: discord-init.sh 15 | dest: /config/user-data/ 16 | mode: "0755" 17 | 18 | - name: Enable discord notify script 19 | ansible.builtin.lineinfile: 20 | path: '/config/scripts/vyos-postconfig-bootup.script' 21 | regexp: '^/config/user-data/discord-init.sh' 22 | line: '/config/user-data/discord-init.sh &1>/dev/null' 23 | 24 | - name: Create bind9 config directory 25 | ansible.builtin.file: 26 | path: /config/user-data/bind9/etc 27 | state: directory 28 | mode: '0775' 29 | owner: root 30 | group: vyattacfg 31 | recurse: true 32 | 33 | - name: Copy the named.conf 34 | ansible.builtin.template: 35 | src: named.conf 36 | dest: /config/user-data/bind9/etc/ 37 | owner: root 38 | group: vyattacfg 39 | mode: "0660" 40 | 41 | - name: Copy the Zone file 42 | ansible.builtin.copy: 43 | src: lab-astr0rack-net.zone 44 | dest: /config/user-data/bind9/etc/ 45 | owner: root 46 | group: vyattacfg 47 | mode: "0660" 48 | 49 | - name: Create bind9 cache directory 50 | ansible.builtin.file: 51 | path: /config/user-data/bind9/cache 52 | state: directory 53 | mode: '0775' 54 | owner: root 55 | group: vyattacfg 56 | 57 | - name: Create bind9 record directory 58 | ansible.builtin.file: 59 | path: /config/user-data/bind9/records 60 | state: directory 61 | mode: '0775' 62 | owner: root 63 | 64 | - name: Set facts for network connection 65 | ansible.builtin.set_fact: 66 | ansible_connection: ansible.netcommon.network_cli 67 | ansible_network_os: vyos.vyos.vyos 68 | ansible_user: vyos 69 | -------------------------------------------------------------------------------- /ansible/requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | collections: 3 | - ansible.posix 4 | - community.general 5 | - community.docker 6 | - vyos.vyos 7 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/files/80-shutdown-on-battery.rules: -------------------------------------------------------------------------------- 1 | KERNEL=="AC", SUBSYSTEM=="power_supply", ATTR{online}=="0", RUN+="/usr/sbin/shutdown -h now" 2 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/files/Caddyfile: -------------------------------------------------------------------------------- 1 | proxmox.{$DOMAIN} { 2 | log { 3 | level INFO 4 | } 5 | # Uncomment this if you want to get a cert via ACME (Let's Encrypt or ZeroSSL). 6 | tls { 7 | dns cloudflare {env.CF_API_TOKEN} 8 | } 9 | 10 | #Uncomment to improve security (WARNING: only use if you understand the implications!) 11 | header { 12 | # Enable HTTP Strict Transport Security (HSTS) 13 | Strict-Transport-Security "max-age=31536000;" 14 | # Enable cross-site filter (XSS) and tell browser to block detected attacks 15 | X-XSS-Protection "1; mode=block" 16 | # Prevent search engines from indexing (optional) 17 | X-Robots-Tag "none" 18 | # Server name removing 19 | -Server 20 | } 21 | 22 | # This setting may have compatibility issues with some browsers 23 | # (e.g., attachment downloading on Firefox). Try disabling this 24 | # if you encounter issues. 25 | encode gzip 26 | 27 | reverse_proxy https://{$PROXMOX_IP}:8006 { 28 | header_up X-Real-IP {remote_host} 29 | transport http { 30 | tls_insecure_skip_verify 31 | } 32 | } 33 | } 34 | 35 | auth.{$DOMAIN} { 36 | log { 37 | level INFO 38 | } 39 | # Uncomment this if you want to get a cert via ACME (Let's Encrypt or ZeroSSL). 40 | tls { 41 | dns cloudflare {env.CF_API_TOKEN} 42 | } 43 | 44 | # This setting may have compatibility issues with some browsers 45 | # (e.g., attachment downloading on Firefox). Try disabling this 46 | # if you encounter issues. 47 | encode gzip 48 | 49 | reverse_proxy dex:5556 { 50 | header_up X-Real-IP {remote_host} 51 | } 52 | } 53 | 54 | 55 | # Just TLS 56 | postgres.{$DOMAIN} { 57 | log { 58 | level INFO 59 | } 60 | # Uncomment this if you want to get a cert via ACME (Let's Encrypt or ZeroSSL). 61 | tls { 62 | dns cloudflare {env.CF_API_TOKEN} 63 | } 64 | } 65 | 66 | 67 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/files/apt-hook-nag: -------------------------------------------------------------------------------- 1 | DPkg::Pre-Install-Pkgs { 2 | "while read -r pkg; do case $pkg in *proxmox-widget-toolkit* | *pve-manager*) touch /tmp/.pve-nag-buster && exit 0; esac done < /dev/stdin"; 3 | }; 4 | 5 | DPkg::Post-Invoke { 6 | "[ -f /tmp/.pve-nag-buster ] && { /usr/share/pve-nag-buster.sh; rm -f /tmp/.pve-nag-buster; }; exit 0"; 7 | }; 8 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/files/discord-notif.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Discord status notifier 3 | Requires=network-online.target pveproxy.service pvedaemon.service 4 | After=pve-guests.service 5 | 6 | [Service] 7 | Type=oneshot 8 | User=root 9 | ExecStart=/usr/bin/discord_status_notify 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/files/pve-nag-buster.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # pve-nag-buster.sh (v04) https://github.com/foundObjects/pve-nag-buster 4 | # Copyright (C) 2019 /u/seaQueue (reddit.com/u/seaQueue) 5 | # 6 | # Removes Proxmox VE 6.x+ license nags automatically after updates 7 | # 8 | # This program is free software; you can redistribute it and/or 9 | # modify it under the terms of the GNU General Public License 10 | # as published by the Free Software Foundation; either version 2 11 | # of the License, or (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 21 | 22 | NAGTOKEN="data.status.toLowerCase() !== 'active'" 23 | NAGFILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js" 24 | SCRIPT="$(basename "$0")" 25 | 26 | # disable license nag: https://johnscs.com/remove-proxmox51-subscription-notice/ 27 | 28 | if grep -qs "$NAGTOKEN" "$NAGFILE" > /dev/null 2>&1; then 29 | echo "$SCRIPT: Removing Nag ..." 30 | sed -i.orig "s/$NAGTOKEN/false/g" "$NAGFILE" 31 | systemctl restart pveproxy.service 32 | fi 33 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/files/sshd_config: -------------------------------------------------------------------------------- 1 | # sshd config file permitting only astr0n8t to login via pubkey 2 | PermitRootLogin no 3 | 4 | PubkeyAuthentication no 5 | PasswordAuthentication no 6 | ChallengeResponseAuthentication no 7 | UsePAM yes 8 | AllowAgentForwarding no 9 | AllowTcpForwarding no 10 | GatewayPorts no 11 | X11Forwarding no 12 | 13 | # Allow client to pass locale environment variables 14 | AcceptEnv LANG LC_* 15 | 16 | # override default of no subsystems 17 | Subsystem sftp /usr/lib/openssh/sftp-server 18 | 19 | # Example of overriding settings on a per-user basis 20 | Match User astr0n8t 21 | PubkeyAuthentication yes 22 | 23 | # Example of overriding settings on a per-user basis 24 | Match User ghactions 25 | PubkeyAuthentication yes 26 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/files/vyos-cloud-config.yml: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | vyos_config_commands: 3 | - set system login user vyos authentication public-keys ghactions@github.com key 'AAAAC3NzaC1lZDI1NTE5AAAAIH8ts84bR4nen8+Kk6I48MDkg8WA+gnuoUFuCmG9B6Xg' 4 | - set system login user vyos authentication public-keys ghactions@github.com type 'ssh-ed25519' 5 | - set system login user vyos authentication public-keys astr0n8t@primary.yubikey key 'AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIFEhLIyleqCaN3lEJ77AYc/q1uZlqsDJ4PKhBu0dXUa7AAAABHNzaDo=' 6 | - set system login user vyos authentication public-keys astr0n8t@primary.yubikey type 'sk-ssh-ed25519@openssh.com' 7 | - set system login user vyos authentication public-keys astr0n8t@secondary.yubikey key 'AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIHKBaozeIJ+Gz7+J2dK2VrQPaWerVWH8a9xYIjhvygLmAAAABHNzaDo=' 8 | - set system login user vyos authentication public-keys astr0n8t@secondary.yubikey type 'sk-ssh-ed25519@openssh.com' 9 | - set system login user vyos authentication public-keys astr0n8t@mobile key 'AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGwVUk4wWGiJQjd7P2z2XZ9Gzia0GTy2faoAnyWVEF+jweR2q16C75oIzZbgE1mN3oc4BD9z8rIA1ElXapGI77I=' 10 | - set system login user vyos authentication public-keys astr0n8t@mobile type 'ecdsa-sha2-nistp256' 11 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Update system 3 | ansible.builtin.apt: 4 | upgrade: full 5 | update_cache: true 6 | listen: Fresh install 7 | 8 | - name: Restart sshd 9 | ansible.builtin.systemd: 10 | name: ssh.service 11 | state: restarted 12 | enabled: true 13 | listen: Restart sshd 14 | 15 | - name: Create mokutil hash file 16 | listen: Disable SB Validation 17 | ansible.builtin.copy: 18 | content: "{{ astr0n8t_user_hash }}" 19 | dest: /tmp/mok_hashfile 20 | mode: "0400" 21 | 22 | - name: Disable secure boot validation because proxmox does not support it in v8 for some reason 23 | listen: Disable SB Validation 24 | become: true 25 | ansible.builtin.shell: 26 | executable: /bin/bash 27 | cmd: | 28 | set -o pipefail 29 | STATE=$(mokutil --sb-state | grep 'SecureBoot validation is disabled in shim' | wc -l) 30 | if [ "$STATE" == "0" ] 31 | then 32 | mokutil --disable-validation --hash-file /tmp/mok_hashfile 33 | echo "SB validation disabled" 34 | fi 35 | register: disable_sb 36 | changed_when: "'SB validation disabled' in disable_sb.stdout" 37 | 38 | - name: Reboot to disable validation 39 | become: true 40 | listen: Disable SB Validation 41 | ansible.builtin.reboot: 42 | 43 | - name: Restart pveproxy 44 | become: true 45 | listen: Restart pveproxy 46 | ansible.builtin.systemd: 47 | name: pveproxy.service 48 | state: restarted 49 | 50 | - name: Restart guest nics 51 | become: true 52 | listen: Restart guest nics 53 | ansible.builtin.command: ifreload -a 54 | register: reload 55 | changed_when: "reload.rc == 0" 56 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/tasks/bw.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Include variables from Bitwarden 3 | ansible.builtin.include_vars: bw.yml 4 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/tasks/create_vyos_image.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Copy the Cloud Init file 3 | ansible.builtin.copy: 4 | src: vyos-cloud-config.yml 5 | dest: /var/lib/vz/snippets/vyos-cloud-config.yml 6 | mode: "0644" 7 | 8 | - name: Download latest VyOS Build 9 | ansible.builtin.get_url: 10 | url: https://github.com/astr0n8t/vyos-builds/releases/latest/download/vyos-qemu-1.5-tailscale.qcow2 11 | dest: /tmp/vyos-qemu-latest-amd64.qcow2 12 | mode: "0644" 13 | 14 | - name: Create new VM to serve as template 15 | ansible.builtin.shell: | 16 | qm destroy 999 17 | qm create 999 --name vyos-1.4-rolling-cloudinit --memory 1024 --bios ovmf \ 18 | --efidisk0 data:1,format=raw,efitype=4m,pre-enrolled-keys=0 \ 19 | -tpmstate0 data:1,version=v2.0 --pool template 20 | qm importdisk 999 /tmp/vyos-qemu-latest-amd64.qcow2 data --format qcow2 21 | qm set 999 --scsi0 data:vm-999-disk-2 22 | qm set 999 --boot c --bootdisk scsi0 23 | qm set 999 --ide2 media=cdrom,file=none 24 | qm template 999 25 | register: create 26 | changed_when: "create.rc == 0" 27 | 28 | - name: Remove disk image on tmp 29 | ansible.builtin.file: 30 | path: /tmp/vyos-qemu-latest-amd64.qcow2 31 | state: absent 32 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/tasks/deploy_containers.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Implement rack network 3 | community.docker.docker_network: 4 | name: rack 5 | 6 | - name: Create watchtower etc directory 7 | ansible.builtin.file: 8 | path: /etc/watchtower 9 | state: directory 10 | mode: '0755' 11 | 12 | - name: Create watchtower configuration directory 13 | ansible.builtin.file: 14 | path: /etc/compose/watchtower 15 | state: directory 16 | mode: '0755' 17 | 18 | - name: Copy the compose file 19 | ansible.builtin.template: 20 | src: watchtower.yaml 21 | dest: /etc/compose/watchtower/docker-compose.yaml 22 | mode: "0600" 23 | 24 | - name: Create dex data volume 25 | community.docker.docker_volume: 26 | name: dex_data_vol 27 | 28 | - name: Create dex configuration directory 29 | ansible.builtin.file: 30 | path: /etc/compose/dex 31 | state: directory 32 | mode: '0755' 33 | 34 | - name: Copy the compose file 35 | ansible.builtin.template: 36 | src: dex.yaml 37 | dest: /etc/compose/dex/docker-compose.yaml 38 | mode: '0600' 39 | 40 | - name: Create dex configuration directory 41 | ansible.builtin.file: 42 | path: /etc/dex 43 | state: directory 44 | mode: '0755' 45 | 46 | - name: Copy the dex config 47 | ansible.builtin.template: 48 | src: dex-config.yaml 49 | dest: /etc/dex/config.yaml 50 | mode: "0600" 51 | owner: 1001 52 | group: 1001 53 | 54 | - name: Create postgres data volume 55 | community.docker.docker_volume: 56 | name: postgres_data_vol 57 | 58 | - name: Create postgres configuration directory 59 | ansible.builtin.file: 60 | path: /etc/compose/postgres 61 | state: directory 62 | mode: '0755' 63 | 64 | - name: Copy the compose file 65 | ansible.builtin.template: 66 | src: postgres.yaml 67 | dest: /etc/compose/postgres/docker-compose.yaml 68 | mode: '0600' 69 | 70 | - name: Create caddy data volume 71 | community.docker.docker_volume: 72 | name: caddy_data_vol 73 | 74 | - name: Create caddy configuration directory 75 | ansible.builtin.file: 76 | path: /etc/compose/caddy 77 | state: directory 78 | mode: '0755' 79 | 80 | - name: Copy the compose file 81 | ansible.builtin.template: 82 | src: caddy.yaml 83 | dest: /etc/compose/caddy/docker-compose.yaml 84 | mode: '0600' 85 | 86 | - name: Create caddy configuration directory 87 | ansible.builtin.file: 88 | path: /etc/caddy 89 | state: directory 90 | mode: '0755' 91 | 92 | - name: Copy the Caddyfile 93 | ansible.builtin.copy: 94 | src: Caddyfile 95 | dest: /etc/caddy/Caddyfile 96 | mode: "0644" 97 | 98 | - name: Configure pveproxy 99 | ansible.builtin.blockinfile: 100 | create: true 101 | path: /etc/default/pveproxy 102 | block: | 103 | ALLOW_FROM="172.16.0.0/12" 104 | DENY_FROM="all" 105 | POLICY="allow" 106 | mode: "0644" 107 | notify: Restart pveproxy 108 | 109 | - name: Start caddy 110 | community.docker.docker_compose: 111 | project_src: /etc/compose/caddy 112 | 113 | - name: Start dex 114 | community.docker.docker_compose: 115 | project_src: /etc/compose/dex 116 | 117 | - name: Start postgres 118 | community.docker.docker_compose: 119 | project_src: /etc/compose/postgres 120 | 121 | - name: Start watchtower 122 | community.docker.docker_compose: 123 | project_src: /etc/compose/watchtower 124 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/tasks/env.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Include variables from environment 3 | ansible.builtin.include_vars: env.yml 4 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/tasks/general_setup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Change the hostname to our standard 3 | ansible.builtin.hostname: 4 | name: '{{ ansible_host }}' 5 | 6 | - name: "Add proxmox /etc/hosts" 7 | ansible.builtin.lineinfile: 8 | dest: /etc/hosts 9 | regexp: '.*proxmox.*$' 10 | line: "{{ hostvars[inventory_hostname]['ansible_default_ipv4']['address'] }} proxmox.lab.astr0rack.net proxmox auth.lab.astr0rack.net" 11 | state: present 12 | 13 | - name: Make sure proxmox enterprise repo is disabled 14 | ansible.builtin.copy: 15 | content: "" 16 | dest: /etc/apt/sources.list.d/pve-enterprise.list 17 | mode: "0644" 18 | force: true 19 | 20 | - name: Setup proxmox repo 21 | ansible.builtin.copy: 22 | content: "deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription" 23 | dest: /etc/apt/sources.list.d/pve-no-subscription.list 24 | mode: "0644" 25 | force: false 26 | notify: 27 | - Fresh install 28 | 29 | - name: Get Proxmox repo signing key 30 | ansible.builtin.get_url: 31 | url: https://enterprise.proxmox.com/debian/proxmox-release-bookworm.gpg 32 | dest: /etc/apt/trusted.gpg.d/proxmox-release-bookworm.gpg 33 | mode: "0644" 34 | 35 | - name: Enable repos 36 | ansible.builtin.lineinfile: 37 | path: '/etc/apt/sources.list' 38 | regexp: '^deb http://deb.debian.org/debian/ bookworm main' 39 | line: 'deb http://deb.debian.org/debian/ bookworm main contrib non-free non-free-firmware' 40 | 41 | - name: Install pve kernel packages 42 | ansible.builtin.apt: 43 | name: 44 | - pve-kernel-6.2 45 | - pve-headers 46 | - dkms 47 | state: present 48 | update_cache: true 49 | notify: 50 | - Disable SB Validation 51 | 52 | - name: Install pve packages 53 | ansible.builtin.apt: 54 | name: 55 | - vim 56 | - tmux 57 | - zsh 58 | - sudo 59 | - wget 60 | - unattended-upgrades 61 | - docker.io 62 | - docker-compose 63 | - python3-docker 64 | - python3-pip 65 | - hashcat 66 | - htop 67 | - nvidia-detect 68 | - nvidia-driver 69 | - openvswitch-switch 70 | - proxmox-ve 71 | state: present 72 | update_cache: true 73 | 74 | - name: Remove debian kernel 75 | ansible.builtin.apt: 76 | name: 77 | - linux-image-amd64 78 | - 'linux-image-6.1*' 79 | - os-prober 80 | state: absent 81 | 82 | - name: Enable Docker 83 | ansible.builtin.systemd: 84 | name: docker.service 85 | state: started 86 | enabled: true 87 | 88 | - name: Add ghactions user 89 | ansible.builtin.user: 90 | name: ghactions 91 | comment: GitHub Actions User 92 | groups: sudo 93 | password: "{{ ghactions_user_hash }}" 94 | shell: /bin/bash 95 | 96 | - name: Add astr0n8t user 97 | ansible.builtin.user: 98 | name: astr0n8t 99 | comment: astr0n8t 100 | groups: sudo 101 | password: "{{ astr0n8t_user_hash }}" 102 | shell: /bin/bash 103 | 104 | - name: Add ghactions ssh key 105 | ansible.posix.authorized_key: 106 | user: ghactions 107 | state: present 108 | key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH8ts84bR4nen8+Kk6I48MDkg8WA+gnuoUFuCmG9B6Xg ghactions@github.com" 109 | 110 | - name: Add primary ssh key 111 | ansible.posix.authorized_key: 112 | user: astr0n8t 113 | state: present 114 | key: "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIFEhLIyleqCaN3lEJ77AYc/q1uZlqsDJ4PKhBu0dXUa7AAAABHNzaDo= astr0n8t@primary.yubikey" # noqa: yaml[line-length] 115 | 116 | - name: Add secondary ssh key 117 | ansible.posix.authorized_key: 118 | user: astr0n8t 119 | state: present 120 | key: "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIHKBaozeIJ+Gz7+J2dK2VrQPaWerVWH8a9xYIjhvygLmAAAABHNzaDo= astr0n8t@secondary.yubikey" # noqa: yaml[line-length] 121 | 122 | - name: Add mobile ssh key 123 | ansible.posix.authorized_key: 124 | user: astr0n8t 125 | state: present 126 | key: "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGwVUk4wWGiJQjd7P2z2XZ9Gzia0GTy2faoAnyWVEF+jweR2q16C75oIzZbgE1mN3oc4BD9z8rIA1ElXapGI77I= astr0n8t@mobile" # noqa: yaml[line-length] 127 | 128 | - name: Copy sshd config 129 | ansible.builtin.copy: 130 | src: sshd_config 131 | dest: /etc/ssh/sshd_config 132 | mode: "0644" 133 | notify: 134 | - Restart sshd 135 | 136 | - name: Enable all updates automatically 137 | ansible.builtin.lineinfile: 138 | path: '/etc/apt/apt.conf.d/50unattended-upgrades' 139 | regexp: ^ "${distro_id}:${distro_codename}-updates";' 140 | line: ' "${distro_id}:${distro_codename}-updates";' 141 | insertafter: "Unattended-Upgrade::Allowed-Origins {" 142 | 143 | - name: Enable all proposed updates automatically 144 | ansible.builtin.lineinfile: 145 | path: '/etc/apt/apt.conf.d/50unattended-upgrades' 146 | regexp: '^ "${distro_id}:${distro_codename}-proposed";' 147 | line: ' "${distro_id}:${distro_codename}-proposed";' 148 | insertafter: "Unattended-Upgrade::Allowed-Origins {" 149 | 150 | - name: Enable all backport updates automatically 151 | ansible.builtin.lineinfile: 152 | path: '/etc/apt/apt.conf.d/50unattended-upgrades' 153 | regexp: '^ "${distro_id}:${distro_codename}-backport";' 154 | line: ' "${distro_id}:${distro_codename}-backport";' 155 | insertafter: "Unattended-Upgrade::Allowed-Origins {" 156 | 157 | - name: Copy discord notify script 158 | ansible.builtin.template: 159 | src: discord-init.sh 160 | dest: /usr/bin/discord_status_notify 161 | mode: "0755" 162 | 163 | - name: Copy discord notifier 164 | ansible.builtin.copy: 165 | src: discord-notif.service 166 | dest: /etc/systemd/system/ 167 | mode: "0644" 168 | 169 | - name: Enable discord-notif 170 | ansible.builtin.systemd: 171 | name: discord-notif.service 172 | daemon_reload: true 173 | enabled: true 174 | 175 | - name: Copy discord notifier 176 | ansible.builtin.copy: 177 | src: 80-shutdown-on-battery.rules 178 | dest: /etc/udev/rules.d/80-shutdown-on-battery.rules 179 | mode: "0644" 180 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Include secrets from environment 3 | ansible.builtin.include_tasks: 4 | file: env.yml 5 | apply: 6 | tags: 7 | - env 8 | tags: 9 | - env 10 | - never 11 | - name: Include secrets from bitwarden 12 | ansible.builtin.include_tasks: 13 | file: bw.yml 14 | apply: 15 | tags: 16 | - bw 17 | tags: 18 | - bw 19 | - never 20 | - name: Apply general setup and hardening 21 | ansible.builtin.include_tasks: 22 | file: general_setup.yml 23 | apply: 24 | tags: 25 | - setup 26 | tags: 27 | - setup 28 | - name: Configure Proxmox network interfaces 29 | ansible.builtin.include_tasks: 30 | file: setup_nics.yml 31 | apply: 32 | tags: 33 | - setup-nics 34 | tags: 35 | - setup-nics 36 | - name: Setup Proxmox Docker containers 37 | ansible.builtin.include_tasks: 38 | file: deploy_containers.yml 39 | apply: 40 | tags: 41 | - containers 42 | tags: 43 | - containers 44 | - name: Configure Proxmox for use with Terraform 45 | ansible.builtin.include_tasks: 46 | file: setup_terraform.yml 47 | apply: 48 | tags: 49 | - setup-terraform 50 | tags: 51 | - setup-terraform 52 | - name: Configure Proxmox for use with OIDC 53 | ansible.builtin.include_tasks: 54 | file: setup_oidc.yml 55 | apply: 56 | tags: 57 | - setup-oidc 58 | tags: 59 | - setup-oidc 60 | - name: Patch proxmox functions 61 | ansible.builtin.include_tasks: 62 | file: patch_proxmox.yml 63 | apply: 64 | tags: 65 | - patch 66 | tags: 67 | - patch 68 | - name: Configure Proxmox Pools and Permissions 69 | ansible.builtin.include_tasks: 70 | file: setup_perms.yml 71 | apply: 72 | tags: 73 | - perms 74 | tags: 75 | - perms 76 | - name: Configure VyOS VM Template 77 | ansible.builtin.include_tasks: 78 | file: create_vyos_image.yml 79 | apply: 80 | tags: 81 | - vyos-image 82 | tags: 83 | - vyos-image 84 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/tasks/patch_proxmox.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Patch OpenID Connect perl func 3 | ansible.builtin.blockinfile: 4 | path: /usr/share/perl5/PVE/API2/OpenId.pm 5 | insertafter: ' \$usercfg\-\>\{users\}\-\>\{\$username\} \= \$entry\;' # noqa: no-tabs 6 | block: | 7 | # Add user to realm group 8 | if ($usercfg->{groups}->{$realm}) { 9 | PVE::AccessControl::add_user_group($username, $usercfg, $realm); 10 | } 11 | 12 | - name: Copy apt nag remove hook 13 | ansible.builtin.copy: 14 | src: apt-hook-nag 15 | dest: /etc/apt/apt.conf.d/86pve-nags 16 | mode: "0644" 17 | 18 | - name: Copy apt nag remove script 19 | ansible.builtin.copy: 20 | src: pve-nag-buster.sh 21 | dest: /usr/share/pve-nag-buster.sh 22 | owner: root 23 | group: root 24 | mode: "0550" 25 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/tasks/setup_nics.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install nic file 3 | ansible.builtin.template: 4 | src: nics.conf 5 | dest: /etc/network/interfaces 6 | mode: '0644' 7 | notify: Restart guest nics 8 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/tasks/setup_oidc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add Keycloak OIDC Realm 3 | ansible.builtin.shell: 4 | executable: /bin/bash 5 | cmd: | 6 | set -o pipefail 7 | REALM_EXIST=$(pveum realm list | grep github | wc -l) 8 | if [ "$REALM_EXIST" == "0" ] 9 | then 10 | pveum realm add github --type openid --issuer-url \ 11 | https://auth.lab.astr0rack.net --client-id proxmox \ 12 | --client-key {{ dex_proxmox_client_secret }} \ 13 | --username-claim username --autocreate --default true 14 | echo "Added realm" 15 | fi 16 | register: realm 17 | changed_when: "'Added' in realm.stdout" 18 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/tasks/setup_perms.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add github group 3 | ansible.builtin.shell: 4 | executable: /bin/bash 5 | cmd: | 6 | set -o pipefail 7 | EXIST=$(pveum group list | grep github | wc -l) 8 | if [ "$EXIST" == "0" ] 9 | then 10 | pveum group add github --comment "GitHub Realm Group" 11 | echo "Added pool" 12 | fi 13 | register: command 14 | changed_when: "'Added' in command.stdout" 15 | 16 | - name: Add infra resource pool 17 | ansible.builtin.shell: 18 | executable: /bin/bash 19 | cmd: | 20 | set -o pipefail 21 | EXIST=$(pveum pool list | grep infra | wc -l) 22 | if [ "$EXIST" == "0" ] 23 | then 24 | pveum pool add infra --comment "Infrastructure Resource Pool" 25 | echo "Added pool" 26 | fi 27 | register: command 28 | changed_when: "'Added' in command.stdout" 29 | 30 | - name: Add lab resource pool 31 | ansible.builtin.shell: 32 | executable: /bin/bash 33 | cmd: | 34 | set -o pipefail 35 | EXIST=$(pveum pool list | grep lab | wc -l) 36 | if [ "$EXIST" == "0" ] 37 | then 38 | pveum pool add lab --comment "Lab Resource Pool" 39 | echo "Added pool" 40 | fi 41 | register: command 42 | changed_when: "'Added' in command.stdout" 43 | 44 | - name: Add template resource pool 45 | ansible.builtin.shell: 46 | executable: /bin/bash 47 | cmd: | 48 | set -o pipefail 49 | EXIST=$(pveum pool list | grep template | wc -l) 50 | if [ "$EXIST" == "0" ] 51 | then 52 | pveum pool add template --comment "Template Resource Pool" 53 | echo "Added pool" 54 | fi 55 | register: command 56 | changed_when: "'Added' in command.stdout" 57 | 58 | - name: Allow github to use lab and template pools 59 | ansible.builtin.shell: 60 | executable: /bin/bash 61 | cmd: | 62 | pveum aclmod /pool/lab/ -group github -role PVEVMAdmin 63 | pveum aclmod /pool/template/ -group github -role PVEVMUser 64 | register: command 65 | changed_when: "command.rc == 0" 66 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/tasks/setup_terraform.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Configure roles 3 | ansible.builtin.shell: | 4 | pveum role add TerraformProv -privs "Datastore.Allocate \ 5 | Datastore.AllocateSpace Datastore.AllocateTemplate Datastore.Audit \ 6 | Group.Allocate Permissions.Modify Pool.Allocate Pool.Audit SDN.Allocate \ 7 | SDN.Audit SDN.Use Sys.Audit Sys.Console Sys.Modify VM.Allocate VM.Audit \ 8 | VM.Backup VM.Clone VM.Config.CDROM VM.Config.CPU VM.Config.Cloudinit \ 9 | VM.Config.Disk VM.Config.HWType VM.Config.Memory VM.Config.Network \ 10 | VM.Config.Options VM.Console VM.Migrate VM.Monitor VM.PowerMgmt \ 11 | VM.Snapshot VM.Snapshot.Rollback" 12 | pveum user add terraform@pve --password "{{ terraform_proxmox_password }}" 13 | pveum aclmod / -user terraform@pve -role TerraformProv 14 | register: command 15 | changed_when: "command.rc == 0" 16 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/templates/caddy.yaml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | init-caddy: 5 | image: docker.io/library/alpine:latest 6 | command: chown -R 999:999 /data /config 7 | volumes: 8 | - "caddy_config:/config" 9 | - "caddy_data_vol:/data" 10 | caddy: 11 | container_name: caddy 12 | image: ghcr.io/astr0n8t/caddy-cloudflare:latest 13 | restart: unless-stopped 14 | user: 999:999 15 | environment: 16 | DOMAIN: "lab.astr0rack.net" 17 | EMAIL: "contact@nathanhigley.com" 18 | CF_API_TOKEN: "{{ cloudflare_api_token }}" 19 | PROXMOX_IP: "{{ proxmox_ip }}" 20 | volumes: 21 | - "/etc/caddy/Caddyfile:/etc/caddy/Caddyfile:ro" 22 | - "caddy_config:/config" 23 | - "caddy_data_vol:/data" 24 | ports: 25 | - "80:80" 26 | - "443:443" 27 | depends_on: 28 | - init-caddy 29 | networks: 30 | - rack 31 | 32 | volumes: 33 | caddy_config: 34 | caddy_data_vol: 35 | external: true 36 | 37 | networks: 38 | rack: 39 | external: true 40 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/templates/dex-config.yaml: -------------------------------------------------------------------------------- 1 | issuer: https://auth.lab.astr0rack.net 2 | 3 | storage: 4 | type: sqlite3 5 | config: 6 | file: /var/sqlite/dex.db 7 | 8 | web: 9 | http: 0.0.0.0:5556 10 | 11 | oauth2: 12 | skipApprovalScreen: true 13 | 14 | staticClients: 15 | - id: proxmox 16 | redirectURIs: 17 | - 'https://proxmox.lab.astr0rack.net' 18 | name: 'proxmox.lab.astr0rack.net' 19 | secret: '{{ dex_proxmox_client_secret }}' 20 | 21 | connectors: 22 | - type: github 23 | # Required field for connector id. 24 | id: github 25 | # Required field for connector name. 26 | name: GitHub 27 | config: 28 | # Credentials can be string literals or pulled from the environment. 29 | clientID: '{{ dex_github_client_id }}' 30 | clientSecret: '{{ dex_github_client_secret }}' 31 | redirectURI: "https://auth.lab.astr0rack.net/callback" 32 | orgs: 33 | - name: lab-astr0rack-net 34 | loadAllGroups: false 35 | 36 | # Optional choice between 'name' (default), 'slug', or 'both'. 37 | # 38 | # As an example, group claims for member of 'Site Reliability Engineers' in 39 | # Acme organization would yield: 40 | # - ['acme:Site Reliability Engineers'] for 'name' 41 | # - ['acme:site-reliability-engineers'] for 'slug' 42 | # - ['acme:Site Reliability Engineers', 'acme:site-reliability-engineers'] for 'both' 43 | teamNameField: slug 44 | # flag which will switch from using the internal GitHub id to the users handle (@mention) as the user id. 45 | # It is possible for a user to change their own user name but it is very rare for them to do so 46 | useLoginAsID: true 47 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/templates/dex.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | init: 5 | image: alpine:latest 6 | command: ['sh', '-c', 'touch /var/sqlite/dex.db && chown 1001:1001 -R /var/sqlite'] 7 | volumes: 8 | - dex_data_vol:/var/sqlite/:rw 9 | 10 | dex: 11 | container_name: dex 12 | image: dexidp/dex 13 | volumes: 14 | - /etc/localtime:/etc/localtime:ro 15 | - /etc/dex/config.yaml:/config.yaml:ro 16 | - dex_data_vol:/var/sqlite/:rw 17 | networks: 18 | - rack 19 | command: ['dex', 'serve','/config.yaml'] 20 | depends_on: 21 | init: 22 | condition: service_completed_successfully 23 | 24 | networks: 25 | rack: 26 | external: true 27 | 28 | volumes: 29 | dex_data_vol: 30 | external: true 31 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/templates/discord-init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VM_STATUS=$(/usr/sbin/qm list | awk '{print $1, $2, $3}' | column -t | sed 's/$/\\n/g' | tr -d '\n') 4 | WEBHOOK='{{ discord_webhook_url }}' 5 | 6 | DATA="{ 7 | \"embeds\": [ 8 | { 9 | \"author\": { 10 | \"name\": \"root\" 11 | }, 12 | \"title\": \"Proxmox Lab Status\", 13 | \"description\": \"Status of VMs sent after Proxmox has tried to start all VMs selected for autoboot.\", 14 | \"color\": 15258703, 15 | \"fields\": [ 16 | { 17 | \"name\": \"qm list\", 18 | \"value\": \"\`\`\`$VM_STATUS\`\`\`\" 19 | } 20 | ] 21 | } 22 | ] 23 | }" 24 | 25 | curl -X POST -H "Content-Type: application/json" -d "$DATA" "$WEBHOOK" 26 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/templates/nics.conf: -------------------------------------------------------------------------------- 1 | # network interface settings; autogenerated 2 | # Please do NOT modify this file directly, unless you know what 3 | # you're doing. 4 | # 5 | # If you want to manage parts of the network configuration manually, 6 | # please utilize the 'source' or 'source-directory' directives to do 7 | # so. 8 | # PVE will preserve these directives, but will NOT read its network 9 | # configuration from sourced files, so do not attempt to move any of 10 | # the PVE managed interfaces into external files! 11 | 12 | source /etc/network/interfaces.d/* 13 | 14 | auto lo 15 | iface lo inet loopback 16 | 17 | auto enp60s0 18 | iface enp60s0 inet manual 19 | ovs_type OVSPort 20 | ovs_bridge vmbr0 21 | 22 | auto vmbr1 23 | iface vmbr1 inet manual 24 | ovs_type OVSBridge 25 | #LAB DC NIC 26 | 27 | auto vmbr0 28 | iface vmbr0 inet static 29 | address {{ proxmox_ip }}/24 30 | gateway {{ proxmox_gateway }} 31 | ovs_type OVSBridge 32 | ovs_ports enp60s0 33 | #RACK NIC 34 | 35 | auto vmbr2 36 | iface vmbr2 inet manual 37 | ovs_type OVSBridge 38 | #LAB DMZ NIC 39 | 40 | auto vmbr3 41 | iface vmbr3 inet manual 42 | ovs_type OVSBridge 43 | #LAB LAN NIC 44 | 45 | auto vmbr4 46 | iface vmbr4 inet manual 47 | ovs_type OVSBridge 48 | #LAB C2 NIC 49 | 50 | auto vmbr20 51 | iface vmbr20 inet manual 52 | ovs_type OVSBridge 53 | #DEV NIC 54 | 55 | auto vmbr99 56 | iface vmbr99 inet manual 57 | ovs_type OVSBridge 58 | #TEMPLATE NET NIC 59 | # 60 | auto vmbr1000 61 | iface vmbr1000 inet manual 62 | ovs_type OVSBridge 63 | #SWAN NIC 64 | auto vmbr1001 65 | iface vmbr1001 inet manual 66 | ovs_type OVSBridge 67 | #SLAN NIC 68 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/templates/postgres.yaml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | postgres: 5 | container_name: postgres 6 | image: docker.io/library/postgres:15 7 | restart: unless-stopped 8 | command: > 9 | -c ssl=on 10 | -c ssl_cert_file=/certs/caddy/certificates/acme-v02.api.letsencrypt.org-directory/postgres.lab.astr0rack.net/postgres.lab.astr0rack.net.crt 11 | -c ssl_key_file=/certs/caddy/certificates/acme-v02.api.letsencrypt.org-directory/postgres.lab.astr0rack.net/postgres.lab.astr0rack.net.key 12 | environment: 13 | POSTGRES_USER: terraform 14 | POSTGRES_DB: terraform 15 | POSTGRES_PASSWORD: {{ postgres_password }} 16 | volumes: 17 | - "postgres_data_vol:/var/lib/postgresql/data" 18 | - "caddy_data_vol:/certs:ro" 19 | ports: 20 | - "5432:5432" 21 | 22 | volumes: 23 | caddy_data_vol: 24 | external: true 25 | postgres_data_vol: 26 | external: true 27 | 28 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/templates/watchtower.yaml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | watchtower: 5 | container_name: watchtower 6 | image: containrrr/watchtower:latest 7 | restart: unless-stopped 8 | volumes: 9 | - /var/run/docker.sock:/var/run/docker.sock 10 | environment: 11 | WATCHTOWER_NOTIFICATION_URL: "discord://{{ watchtower_webhook_url }}" 12 | WATCHTOWER_CLEANUP: "true" 13 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/vars/bw.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ghactions_user_hash: "{{ lookup('community.general.bitwarden', 'ansible-proxmox-ghactions-user-hash', field='password')[0] }}" 3 | astr0n8t_user_hash: "{{ lookup('community.general.bitwarden', 'ansible-proxmox-astr0n8t-user-hash', field='password')[0] }}" 4 | terraform_proxmox_password: "{{ lookup('community.general.bitwarden', 'terraform-proxmox-credentials', field='password')[0] }}" 5 | postgres_password: "{{ lookup('community.general.bitwarden', 'ansible-proxmox-postgres-password', field='password')[0] }}" 6 | cloudflare_api_token: "{{ lookup('community.general.bitwarden', 'ansible-proxmox-cloudflare-api', field='password')[0] }}" 7 | discord_webhook_url: "{{ lookup('community.general.bitwarden', 'ansible-proxmox-discord-webhook-url', field='password')[0] }}" 8 | watchtower_webhook_url: "{{ lookup('community.general.bitwarden', 'ansible-proxmox-watchtower-webhook', field='password')[0] }}" 9 | dex_proxmox_client_secret: "{{ lookup('community.general.bitwarden', 'ansible-dex-proxmox-clientid', field='password')[0] }}" 10 | dex_github_client_id: "{{ lookup('community.general.bitwarden', 'ansible-dex-github-client', field='username')[0] }}" 11 | dex_github_client_secret: "{{ lookup('community.general.bitwarden', 'ansible-dex-github-client', field='password')[0] }}" 12 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/vars/env.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ghactions_user_hash: "{{ lookup('ansible.builtin.env', 'PM_GHACTIONS_USER_HASH') }}" 3 | astr0n8t_user_hash: "{{ lookup('ansible.builtin.env', 'PM_ASTR0N8T_USER_HASH') }}" 4 | terraform_proxmox_password: "{{ lookup('ansible.builtin.env', 'TF_PROXMOX_PASSWORD') }}" 5 | postgres_password: "{{ lookup('ansible.builtin.env', 'TF_POSTGRES_PASSWORD') }}" 6 | cloudflare_api_token: "{{ lookup('ansible.builtin.env', 'PM_CLOUDFLARE_API') }}" 7 | discord_webhook_url: "{{ lookup('ansible.builtin.env', 'PM_DISCORD_WEBHOOK_URL') }}" 8 | watchtower_webhook_url: "{{ lookup('ansible.builtin.env', 'WT_DISCORD_WEBHOOK_URL') }}" 9 | dex_proxmox_client_secret: "{{ lookup('ansible.builtin.env', 'ANSIBLE_DEX_PROXMOX_CLIENT_SECRET') }}" 10 | dex_github_client_id: "{{ lookup('ansible.builtin.env', 'ANSIBLE_DEX_GITHUB_CLIENT_ID') }}" 11 | dex_github_client_secret: "{{ lookup('ansible.builtin.env', 'ANSIBLE_DEX_GITHUB_CLIENT_SECRET') }}" 12 | -------------------------------------------------------------------------------- /ansible/roles/proxmox/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | proxmox_ip: "{{ hostvars[inventory_hostname]['ansible_default_ipv4']['address'] }}" 3 | proxmox_gateway: "{{ hostvars[inventory_hostname]['ansible_default_ipv4']['gateway'] }}" 4 | -------------------------------------------------------------------------------- /ansible/vars/bw.yml: -------------------------------------------------------------------------------- 1 | --- 2 | lab_astr0rack_dns_key: "{{ lookup('community.general.bitwarden', 'ansible-vyos-bind9-lab-astr0rack-key', field='password')[0] }}" 3 | discord_webhook_url: "{{ lookup('community.general.bitwarden', 'ansible-vyos-discord-webhook-url', field='password')[0] }}" 4 | tailscale_auth_key: "{{ lookup('community.general.bitwarden', 'ansible-vyos-tailscale-auth-key', field='password')[0] }}" 5 | vyos_user_hash: "{{ lookup('community.general.bitwarden', 'ansible-vyos-lab-user-hash', field='password')[0] }}" 6 | -------------------------------------------------------------------------------- /ansible/vars/env.yml: -------------------------------------------------------------------------------- 1 | --- 2 | lab_astr0rack_dns_key: "{{ lookup('ansible.builtin.env', 'VY_LAB_DNS_KEY') }}" 3 | discord_webhook_url: "{{ lookup('ansible.builtin.env', 'VY_DISCORD_WEBHOOK') }}" 4 | tailscale_auth_key: "{{ lookup('ansible.builtin.env', 'VY_TAILSCALE_AUTH_KEY') }}" 5 | vyos_user_hash: "{{ lookup('ansible.builtin.env', 'VY_USER_PASSWORD_HASH') }}" 6 | -------------------------------------------------------------------------------- /docs/img/lab.excalidraw: -------------------------------------------------------------------------------- 1 | { 2 | "type": "excalidraw", 3 | "version": 2, 4 | "source": "https://excalidraw.com", 5 | "elements": [ 6 | { 7 | "type": "text", 8 | "version": 230, 9 | "versionNonce": 1793340552, 10 | "isDeleted": false, 11 | "id": "pKbRDUe5swgO2lj7lVr_q", 12 | "fillStyle": "hachure", 13 | "strokeWidth": 1, 14 | "strokeStyle": "solid", 15 | "roughness": 1, 16 | "opacity": 100, 17 | "angle": 0, 18 | "x": 691.9256591796875, 19 | "y": 263.89971923828125, 20 | "strokeColor": "#1e1e1e", 21 | "backgroundColor": "transparent", 22 | "width": 170.87986755371094, 23 | "height": 25, 24 | "seed": 1621859320, 25 | "groupIds": [], 26 | "frameId": null, 27 | "roundness": null, 28 | "boundElements": [], 29 | "updated": 1687115231670, 30 | "link": null, 31 | "locked": false, 32 | "fontSize": 20, 33 | "fontFamily": 1, 34 | "text": "lab.astr0rack.net", 35 | "textAlign": "center", 36 | "verticalAlign": "top", 37 | "containerId": null, 38 | "originalText": "lab.astr0rack.net", 39 | "lineHeight": 1.25, 40 | "baseline": 18 41 | }, 42 | { 43 | "type": "rectangle", 44 | "version": 648, 45 | "versionNonce": 1275934712, 46 | "isDeleted": false, 47 | "id": "e1OxK6Nza-uanSVYzwSlz", 48 | "fillStyle": "hachure", 49 | "strokeWidth": 1, 50 | "strokeStyle": "solid", 51 | "roughness": 1, 52 | "opacity": 100, 53 | "angle": 0, 54 | "x": 337.210205078125, 55 | "y": 247.13119506835938, 56 | "strokeColor": "#e03131", 57 | "backgroundColor": "transparent", 58 | "width": 896.36279296875, 59 | "height": 522.7306518554688, 60 | "seed": 2000397960, 61 | "groupIds": [], 62 | "frameId": null, 63 | "roundness": { 64 | "type": 3 65 | }, 66 | "boundElements": [], 67 | "updated": 1687115231670, 68 | "link": null, 69 | "locked": false 70 | }, 71 | { 72 | "type": "rectangle", 73 | "version": 739, 74 | "versionNonce": 411492918, 75 | "isDeleted": false, 76 | "id": "Bi126oqn4ynOGDKCgzu76", 77 | "fillStyle": "hachure", 78 | "strokeWidth": 1, 79 | "strokeStyle": "solid", 80 | "roughness": 1, 81 | "opacity": 100, 82 | "angle": 0, 83 | "x": 659.8501680998199, 84 | "y": 339.0666963110741, 85 | "strokeColor": "#1e1e1e", 86 | "backgroundColor": "#ffec99", 87 | "width": 547, 88 | "height": 403, 89 | "seed": 1220847752, 90 | "groupIds": [], 91 | "frameId": null, 92 | "roundness": { 93 | "type": 3 94 | }, 95 | "boundElements": [ 96 | { 97 | "type": "text", 98 | "id": "XgD1lS_sjECDYv3D3a2bA" 99 | }, 100 | { 101 | "id": "mEmzErbo78TeyxNfEzBHi", 102 | "type": "arrow" 103 | }, 104 | { 105 | "id": "Ila3BcvLfw3__u01XibYe", 106 | "type": "arrow" 107 | } 108 | ], 109 | "updated": 1704308621061, 110 | "link": null, 111 | "locked": false 112 | }, 113 | { 114 | "type": "text", 115 | "version": 646, 116 | "versionNonce": 288797320, 117 | "isDeleted": false, 118 | "id": "XgD1lS_sjECDYv3D3a2bA", 119 | "fillStyle": "hachure", 120 | "strokeWidth": 1, 121 | "strokeStyle": "solid", 122 | "roughness": 1, 123 | "opacity": 100, 124 | "angle": 0, 125 | "x": 895.4702089933746, 126 | "y": 528.0666963110741, 127 | "strokeColor": "#1e1e1e", 128 | "backgroundColor": "#ffec99", 129 | "width": 75.75991821289062, 130 | "height": 25, 131 | "seed": 1597143176, 132 | "groupIds": [], 133 | "frameId": null, 134 | "roundness": null, 135 | "boundElements": [], 136 | "updated": 1687115231670, 137 | "link": null, 138 | "locked": false, 139 | "fontSize": 20, 140 | "fontFamily": 1, 141 | "text": "proxmox", 142 | "textAlign": "center", 143 | "verticalAlign": "middle", 144 | "containerId": "Bi126oqn4ynOGDKCgzu76", 145 | "originalText": "proxmox", 146 | "lineHeight": 1.25, 147 | "baseline": 18 148 | }, 149 | { 150 | "type": "ellipse", 151 | "version": 373, 152 | "versionNonce": 1989226538, 153 | "isDeleted": false, 154 | "id": "d_Bx9-NZIHpaV1NqLrI_Y", 155 | "fillStyle": "hachure", 156 | "strokeWidth": 1, 157 | "strokeStyle": "solid", 158 | "roughness": 1, 159 | "opacity": 100, 160 | "angle": 0, 161 | "x": 428.36846923828125, 162 | "y": 464.4199523925781, 163 | "strokeColor": "#1e1e1e", 164 | "backgroundColor": "#b2f2bb", 165 | "width": 131.98443603515625, 166 | "height": 126.36178588867188, 167 | "seed": 1520735736, 168 | "groupIds": [], 169 | "frameId": null, 170 | "roundness": { 171 | "type": 2 172 | }, 173 | "boundElements": [ 174 | { 175 | "type": "text", 176 | "id": "OfqzBbfVsh3Td781PxR7s" 177 | }, 178 | { 179 | "id": "Og0Lds5XGDn5Qu14nHcR6", 180 | "type": "arrow" 181 | }, 182 | { 183 | "id": "6U6yfnwWj97hk9wasjWhX", 184 | "type": "arrow" 185 | }, 186 | { 187 | "id": "mEmzErbo78TeyxNfEzBHi", 188 | "type": "arrow" 189 | }, 190 | { 191 | "id": "9snoe0Z3xTiXmUY9pMjxZ", 192 | "type": "arrow" 193 | }, 194 | { 195 | "id": "rs-_MFjixejG4hGhVXNMM", 196 | "type": "arrow" 197 | }, 198 | { 199 | "id": "LI-wrYtEDplvHS_UwzMKY", 200 | "type": "arrow" 201 | } 202 | ], 203 | "updated": 1704308464687, 204 | "link": null, 205 | "locked": false 206 | }, 207 | { 208 | "type": "text", 209 | "version": 207, 210 | "versionNonce": 600777096, 211 | "isDeleted": false, 212 | "id": "OfqzBbfVsh3Td781PxR7s", 213 | "fillStyle": "hachure", 214 | "strokeWidth": 1, 215 | "strokeStyle": "solid", 216 | "roughness": 1, 217 | "opacity": 100, 218 | "angle": 0, 219 | "x": 460.30916478999114, 220 | "y": 517.4252074945528, 221 | "strokeColor": "#1e1e1e", 222 | "backgroundColor": "#b2f2bb", 223 | "width": 67.77595520019531, 224 | "height": 20, 225 | "seed": 724340984, 226 | "groupIds": [], 227 | "frameId": null, 228 | "roundness": null, 229 | "boundElements": [], 230 | "updated": 1687115231670, 231 | "link": null, 232 | "locked": false, 233 | "fontSize": 16, 234 | "fontFamily": 1, 235 | "text": "tailscale", 236 | "textAlign": "center", 237 | "verticalAlign": "middle", 238 | "containerId": "d_Bx9-NZIHpaV1NqLrI_Y", 239 | "originalText": "tailscale", 240 | "lineHeight": 1.25, 241 | "baseline": 14 242 | }, 243 | { 244 | "type": "diamond", 245 | "version": 135, 246 | "versionNonce": 492018424, 247 | "isDeleted": false, 248 | "id": "CU0aRdbanhe6hMXuSvoLE", 249 | "fillStyle": "hachure", 250 | "strokeWidth": 1, 251 | "strokeStyle": "solid", 252 | "roughness": 1, 253 | "opacity": 100, 254 | "angle": 0, 255 | "x": 365.844970703125, 256 | "y": 630.8363037109375, 257 | "strokeColor": "#1e1e1e", 258 | "backgroundColor": "#a5d8ff", 259 | "width": 108, 260 | "height": 113, 261 | "seed": 116428424, 262 | "groupIds": [], 263 | "frameId": null, 264 | "roundness": { 265 | "type": 2 266 | }, 267 | "boundElements": [ 268 | { 269 | "type": "text", 270 | "id": "9jYHysA50-gUUt7E-Q3SQ" 271 | }, 272 | { 273 | "id": "Og0Lds5XGDn5Qu14nHcR6", 274 | "type": "arrow" 275 | } 276 | ], 277 | "updated": 1687115231670, 278 | "link": null, 279 | "locked": false 280 | }, 281 | { 282 | "type": "text", 283 | "version": 163, 284 | "versionNonce": 1199681672, 285 | "isDeleted": false, 286 | "id": "9jYHysA50-gUUt7E-Q3SQ", 287 | "fillStyle": "hachure", 288 | "strokeWidth": 1, 289 | "strokeStyle": "solid", 290 | "roughness": 1, 291 | "opacity": 100, 292 | "angle": 0, 293 | "x": 403.14098358154297, 294 | "y": 677.5863037109375, 295 | "strokeColor": "#1e1e1e", 296 | "backgroundColor": "#a5d8ff", 297 | "width": 33.40797424316406, 298 | "height": 20, 299 | "seed": 563176840, 300 | "groupIds": [], 301 | "frameId": null, 302 | "roundness": null, 303 | "boundElements": [], 304 | "updated": 1687115231670, 305 | "link": null, 306 | "locked": false, 307 | "fontSize": 16, 308 | "fontFamily": 1, 309 | "text": "user", 310 | "textAlign": "center", 311 | "verticalAlign": "middle", 312 | "containerId": "CU0aRdbanhe6hMXuSvoLE", 313 | "originalText": "user", 314 | "lineHeight": 1.25, 315 | "baseline": 14 316 | }, 317 | { 318 | "type": "diamond", 319 | "version": 310, 320 | "versionNonce": 789937144, 321 | "isDeleted": false, 322 | "id": "egFR60b0caWKOKes3taAg", 323 | "fillStyle": "hachure", 324 | "strokeWidth": 1, 325 | "strokeStyle": "solid", 326 | "roughness": 1, 327 | "opacity": 100, 328 | "angle": 0, 329 | "x": 500.51227744614005, 330 | "y": 645.2608621485604, 331 | "strokeColor": "#1e1e1e", 332 | "backgroundColor": "#a5d8ff", 333 | "width": 108, 334 | "height": 113, 335 | "seed": 149020152, 336 | "groupIds": [], 337 | "frameId": null, 338 | "roundness": { 339 | "type": 2 340 | }, 341 | "boundElements": [ 342 | { 343 | "type": "text", 344 | "id": "jY0dZhPOeHaeVZmY0edZ2" 345 | }, 346 | { 347 | "id": "6U6yfnwWj97hk9wasjWhX", 348 | "type": "arrow" 349 | } 350 | ], 351 | "updated": 1687115231671, 352 | "link": null, 353 | "locked": false 354 | }, 355 | { 356 | "type": "text", 357 | "version": 338, 358 | "versionNonce": 336224136, 359 | "isDeleted": false, 360 | "id": "jY0dZhPOeHaeVZmY0edZ2", 361 | "fillStyle": "hachure", 362 | "strokeWidth": 1, 363 | "strokeStyle": "solid", 364 | "roughness": 1, 365 | "opacity": 100, 366 | "angle": 0, 367 | "x": 537.808290324558, 368 | "y": 692.0108621485604, 369 | "strokeColor": "#1e1e1e", 370 | "backgroundColor": "#a5d8ff", 371 | "width": 33.40797424316406, 372 | "height": 20, 373 | "seed": 904093432, 374 | "groupIds": [], 375 | "frameId": null, 376 | "roundness": null, 377 | "boundElements": [], 378 | "updated": 1687115231671, 379 | "link": null, 380 | "locked": false, 381 | "fontSize": 16, 382 | "fontFamily": 1, 383 | "text": "user", 384 | "textAlign": "center", 385 | "verticalAlign": "middle", 386 | "containerId": "egFR60b0caWKOKes3taAg", 387 | "originalText": "user", 388 | "lineHeight": 1.25, 389 | "baseline": 14 390 | }, 391 | { 392 | "type": "arrow", 393 | "version": 622, 394 | "versionNonce": 1341340920, 395 | "isDeleted": false, 396 | "id": "Og0Lds5XGDn5Qu14nHcR6", 397 | "fillStyle": "hachure", 398 | "strokeWidth": 1, 399 | "strokeStyle": "solid", 400 | "roughness": 1, 401 | "opacity": 100, 402 | "angle": 0, 403 | "x": 438.67205035059794, 404 | "y": 641.5127504943249, 405 | "strokeColor": "#1e1e1e", 406 | "backgroundColor": "#a5d8ff", 407 | "width": 33.20602538648865, 408 | "height": 48.28536092745321, 409 | "seed": 1094415096, 410 | "groupIds": [], 411 | "frameId": null, 412 | "roundness": { 413 | "type": 2 414 | }, 415 | "boundElements": [], 416 | "updated": 1687115231671, 417 | "link": null, 418 | "locked": false, 419 | "startBinding": { 420 | "elementId": "CU0aRdbanhe6hMXuSvoLE", 421 | "focus": -0.23104958902281633, 422 | "gap": 6.233766748938521 423 | }, 424 | "endBinding": { 425 | "elementId": "d_Bx9-NZIHpaV1NqLrI_Y", 426 | "focus": -0.2866538006551794, 427 | "gap": 5.910040092815549 428 | }, 429 | "lastCommittedPoint": null, 430 | "startArrowhead": null, 431 | "endArrowhead": "arrow", 432 | "points": [ 433 | [ 434 | 0, 435 | 0 436 | ], 437 | [ 438 | 33.20602538648865, 439 | -48.28536092745321 440 | ] 441 | ] 442 | }, 443 | { 444 | "type": "arrow", 445 | "version": 762, 446 | "versionNonce": 1235562120, 447 | "isDeleted": false, 448 | "id": "6U6yfnwWj97hk9wasjWhX", 449 | "fillStyle": "hachure", 450 | "strokeWidth": 1, 451 | "strokeStyle": "solid", 452 | "roughness": 1, 453 | "opacity": 100, 454 | "angle": 0, 455 | "x": 556.1849625479102, 456 | "y": 644.4588740445731, 457 | "strokeColor": "#1e1e1e", 458 | "backgroundColor": "#a5d8ff", 459 | "width": 20.896834470221734, 460 | "height": 55.534868576705094, 461 | "seed": 1423653624, 462 | "groupIds": [], 463 | "frameId": null, 464 | "roundness": { 465 | "type": 2 466 | }, 467 | "boundElements": [], 468 | "updated": 1687115231671, 469 | "link": null, 470 | "locked": false, 471 | "startBinding": { 472 | "elementId": "egFR60b0caWKOKes3taAg", 473 | "focus": 0.4302677194126928, 474 | "gap": 1.763336283301058 475 | }, 476 | "endBinding": { 477 | "elementId": "d_Bx9-NZIHpaV1NqLrI_Y", 478 | "focus": -0.2545133845012975, 479 | "gap": 9.711153262010868 480 | }, 481 | "lastCommittedPoint": null, 482 | "startArrowhead": null, 483 | "endArrowhead": "arrow", 484 | "points": [ 485 | [ 486 | 0, 487 | 0 488 | ], 489 | [ 490 | -20.896834470221734, 491 | -55.534868576705094 492 | ] 493 | ] 494 | }, 495 | { 496 | "type": "arrow", 497 | "version": 1051, 498 | "versionNonce": 1903222264, 499 | "isDeleted": false, 500 | "id": "mEmzErbo78TeyxNfEzBHi", 501 | "fillStyle": "hachure", 502 | "strokeWidth": 1, 503 | "strokeStyle": "solid", 504 | "roughness": 1, 505 | "opacity": 100, 506 | "angle": 0, 507 | "x": 575.2574386247874, 508 | "y": 547.3965930199354, 509 | "strokeColor": "#1e1e1e", 510 | "backgroundColor": "#a5d8ff", 511 | "width": 123.05384096034356, 512 | "height": 1.9410922895549447, 513 | "seed": 702836728, 514 | "groupIds": [], 515 | "frameId": null, 516 | "roundness": { 517 | "type": 2 518 | }, 519 | "boundElements": [], 520 | "updated": 1687115231671, 521 | "link": null, 522 | "locked": false, 523 | "startBinding": { 524 | "elementId": "d_Bx9-NZIHpaV1NqLrI_Y", 525 | "focus": 0.29308136192904366, 526 | "gap": 17.457345844339997 527 | }, 528 | "endBinding": { 529 | "elementId": "dEfFkxYgaAnTckB9x3DSd", 530 | "focus": -0.27093439738616015, 531 | "gap": 8.932126994790558 532 | }, 533 | "lastCommittedPoint": null, 534 | "startArrowhead": null, 535 | "endArrowhead": "arrow", 536 | "points": [ 537 | [ 538 | 0, 539 | 0 540 | ], 541 | [ 542 | 123.05384096034356, 543 | 1.9410922895549447 544 | ] 545 | ] 546 | }, 547 | { 548 | "type": "arrow", 549 | "version": 922, 550 | "versionNonce": 1091444104, 551 | "isDeleted": false, 552 | "id": "9snoe0Z3xTiXmUY9pMjxZ", 553 | "fillStyle": "hachure", 554 | "strokeWidth": 1, 555 | "strokeStyle": "solid", 556 | "roughness": 1, 557 | "opacity": 100, 558 | "angle": 0, 559 | "x": 703.1050415039064, 560 | "y": 520.8175723184735, 561 | "strokeColor": "#1e1e1e", 562 | "backgroundColor": "#a5d8ff", 563 | "width": 127.0706080055694, 564 | "height": 2.762341722216206, 565 | "seed": 1876896904, 566 | "groupIds": [], 567 | "frameId": null, 568 | "roundness": { 569 | "type": 2 570 | }, 571 | "boundElements": [], 572 | "updated": 1687115231671, 573 | "link": null, 574 | "locked": false, 575 | "startBinding": { 576 | "elementId": "dEfFkxYgaAnTckB9x3DSd", 577 | "focus": 0.3365310751656015, 578 | "gap": 5.948886204681777 579 | }, 580 | "endBinding": { 581 | "elementId": "d_Bx9-NZIHpaV1NqLrI_Y", 582 | "focus": -0.17913918043437826, 583 | "gap": 16.277167155422063 584 | }, 585 | "lastCommittedPoint": null, 586 | "startArrowhead": null, 587 | "endArrowhead": "arrow", 588 | "points": [ 589 | [ 590 | 0, 591 | 0 592 | ], 593 | [ 594 | -127.0706080055694, 595 | -2.762341722216206 596 | ] 597 | ] 598 | }, 599 | { 600 | "type": "diamond", 601 | "version": 227, 602 | "versionNonce": 2129279466, 603 | "isDeleted": false, 604 | "id": "dzKvY-kxoG94RSNPsP5fi", 605 | "fillStyle": "hachure", 606 | "strokeWidth": 1, 607 | "strokeStyle": "solid", 608 | "roughness": 1, 609 | "opacity": 100, 610 | "angle": 0, 611 | "x": 347.21875, 612 | "y": 278.7151184082031, 613 | "strokeColor": "#1e1e1e", 614 | "backgroundColor": "#ffc9c9", 615 | "width": 154.74951171875, 616 | "height": 130.39566040039062, 617 | "seed": 1513634808, 618 | "groupIds": [], 619 | "frameId": null, 620 | "roundness": { 621 | "type": 2 622 | }, 623 | "boundElements": [ 624 | { 625 | "type": "text", 626 | "id": "Kc5U0dFCRgnkwFwWxS5rf" 627 | }, 628 | { 629 | "id": "rs-_MFjixejG4hGhVXNMM", 630 | "type": "arrow" 631 | } 632 | ], 633 | "updated": 1704308426538, 634 | "link": null, 635 | "locked": false 636 | }, 637 | { 638 | "type": "text", 639 | "version": 141, 640 | "versionNonce": 1780444330, 641 | "isDeleted": false, 642 | "id": "Kc5U0dFCRgnkwFwWxS5rf", 643 | "fillStyle": "hachure", 644 | "strokeWidth": 1, 645 | "strokeStyle": "solid", 646 | "roughness": 1, 647 | "opacity": 100, 648 | "angle": 0, 649 | "x": 394.80614471435547, 650 | "y": 323.8140335083008, 651 | "strokeColor": "#1e1e1e", 652 | "backgroundColor": "#ffc9c9", 653 | "width": 59.19996643066406, 654 | "height": 40, 655 | "seed": 1560320504, 656 | "groupIds": [], 657 | "frameId": null, 658 | "roundness": null, 659 | "boundElements": [], 660 | "updated": 1704308426538, 661 | "link": null, 662 | "locked": false, 663 | "fontSize": 16, 664 | "fontFamily": 1, 665 | "text": "GitHub \nActions", 666 | "textAlign": "center", 667 | "verticalAlign": "middle", 668 | "containerId": "dzKvY-kxoG94RSNPsP5fi", 669 | "originalText": "GitHub Actions", 670 | "lineHeight": 1.25, 671 | "baseline": 34 672 | }, 673 | { 674 | "type": "arrow", 675 | "version": 204, 676 | "versionNonce": 889946154, 677 | "isDeleted": false, 678 | "id": "rs-_MFjixejG4hGhVXNMM", 679 | "fillStyle": "hachure", 680 | "strokeWidth": 1, 681 | "strokeStyle": "solid", 682 | "roughness": 1, 683 | "opacity": 100, 684 | "angle": 0, 685 | "x": 444.28432077945064, 686 | "y": 396.9469914440822, 687 | "strokeColor": "#1e1e1e", 688 | "backgroundColor": "#ffc9c9", 689 | "width": 23.210619776699843, 690 | "height": 64.3235084408941, 691 | "seed": 2128704248, 692 | "groupIds": [], 693 | "frameId": null, 694 | "roundness": { 695 | "type": 2 696 | }, 697 | "boundElements": [], 698 | "updated": 1704308426689, 699 | "link": null, 700 | "locked": false, 701 | "startBinding": { 702 | "elementId": "dzKvY-kxoG94RSNPsP5fi", 703 | "focus": -0.007161364773601998, 704 | "gap": 3.386288696059168 705 | }, 706 | "endBinding": { 707 | "elementId": "d_Bx9-NZIHpaV1NqLrI_Y", 708 | "focus": -0.041979445988918306, 709 | "gap": 8.006766480860186 710 | }, 711 | "lastCommittedPoint": null, 712 | "startArrowhead": null, 713 | "endArrowhead": "arrow", 714 | "points": [ 715 | [ 716 | 0, 717 | 0 718 | ], 719 | [ 720 | 23.210619776699843, 721 | 64.3235084408941 722 | ] 723 | ] 724 | }, 725 | { 726 | "type": "rectangle", 727 | "version": 763, 728 | "versionNonce": 1224059528, 729 | "isDeleted": false, 730 | "id": "93PK33yAD1Eh6FYupGfMe", 731 | "fillStyle": "hachure", 732 | "strokeWidth": 1, 733 | "strokeStyle": "solid", 734 | "roughness": 1, 735 | "opacity": 100, 736 | "angle": 0, 737 | "x": 855.2684326171875, 738 | "y": 355.48388671875, 739 | "strokeColor": "#1e1e1e", 740 | "backgroundColor": "transparent", 741 | "width": 328, 742 | "height": 120, 743 | "seed": 1677153416, 744 | "groupIds": [], 745 | "frameId": null, 746 | "roundness": { 747 | "type": 3 748 | }, 749 | "boundElements": [ 750 | { 751 | "type": "text", 752 | "id": "NjhOFSnwlngqXQao8Wi03" 753 | }, 754 | { 755 | "id": "VQfCHZ8aJKGszcw5J2XEq", 756 | "type": "arrow" 757 | } 758 | ], 759 | "updated": 1687115231671, 760 | "link": null, 761 | "locked": false 762 | }, 763 | { 764 | "type": "text", 765 | "version": 667, 766 | "versionNonce": 712385016, 767 | "isDeleted": false, 768 | "id": "NjhOFSnwlngqXQao8Wi03", 769 | "fillStyle": "hachure", 770 | "strokeWidth": 1, 771 | "strokeStyle": "solid", 772 | "roughness": 1, 773 | "opacity": 100, 774 | "angle": 0, 775 | "x": 977.2204666137695, 776 | "y": 395.48388671875, 777 | "strokeColor": "#1e1e1e", 778 | "backgroundColor": "transparent", 779 | "width": 84.09593200683594, 780 | "height": 40, 781 | "seed": 1005127048, 782 | "groupIds": [], 783 | "frameId": null, 784 | "roundness": null, 785 | "boundElements": [], 786 | "updated": 1687115231671, 787 | "link": null, 788 | "locked": false, 789 | "fontSize": 16, 790 | "fontFamily": 1, 791 | "text": "Lab Space\n10.0.0.0/16", 792 | "textAlign": "center", 793 | "verticalAlign": "middle", 794 | "containerId": "93PK33yAD1Eh6FYupGfMe", 795 | "originalText": "Lab Space\n10.0.0.0/16", 796 | "lineHeight": 1.25, 797 | "baseline": 34 798 | }, 799 | { 800 | "type": "ellipse", 801 | "version": 962, 802 | "versionNonce": 2028935560, 803 | "isDeleted": false, 804 | "id": "dEfFkxYgaAnTckB9x3DSd", 805 | "fillStyle": "hachure", 806 | "strokeWidth": 1, 807 | "strokeStyle": "solid", 808 | "roughness": 1, 809 | "opacity": 100, 810 | "angle": 0, 811 | "x": 705.838134765625, 812 | "y": 491.21551513671875, 813 | "strokeColor": "#1e1e1e", 814 | "backgroundColor": "#a5d8ff", 815 | "width": 109, 816 | "height": 93, 817 | "seed": 1490823416, 818 | "groupIds": [], 819 | "frameId": null, 820 | "roundness": { 821 | "type": 2 822 | }, 823 | "boundElements": [ 824 | { 825 | "type": "text", 826 | "id": "ymB7_nsoIJkbnJohqOOgH" 827 | }, 828 | { 829 | "id": "mEmzErbo78TeyxNfEzBHi", 830 | "type": "arrow" 831 | }, 832 | { 833 | "id": "9snoe0Z3xTiXmUY9pMjxZ", 834 | "type": "arrow" 835 | }, 836 | { 837 | "id": "VQfCHZ8aJKGszcw5J2XEq", 838 | "type": "arrow" 839 | }, 840 | { 841 | "id": "ry5pO68gzwkZ4Vb9SMZ_J", 842 | "type": "arrow" 843 | }, 844 | { 845 | "id": "9nPAJ7PAd4hxJmjAIq02G", 846 | "type": "arrow" 847 | } 848 | ], 849 | "updated": 1687115231671, 850 | "link": null, 851 | "locked": false 852 | }, 853 | { 854 | "type": "text", 855 | "version": 668, 856 | "versionNonce": 609166072, 857 | "isDeleted": false, 858 | "id": "ymB7_nsoIJkbnJohqOOgH", 859 | "fillStyle": "hachure", 860 | "strokeWidth": 1, 861 | "strokeStyle": "solid", 862 | "roughness": 1, 863 | "opacity": 100, 864 | "angle": 0, 865 | "x": 741.7408252617589, 866 | "y": 527.8350498115443, 867 | "strokeColor": "#1e1e1e", 868 | "backgroundColor": "#a5d8ff", 869 | "width": 37.11997985839844, 870 | "height": 20, 871 | "seed": 2029108216, 872 | "groupIds": [], 873 | "frameId": null, 874 | "roundness": null, 875 | "boundElements": [], 876 | "updated": 1687115231671, 877 | "link": null, 878 | "locked": false, 879 | "fontSize": 16, 880 | "fontFamily": 1, 881 | "text": "VyOS", 882 | "textAlign": "center", 883 | "verticalAlign": "middle", 884 | "containerId": "dEfFkxYgaAnTckB9x3DSd", 885 | "originalText": "VyOS", 886 | "lineHeight": 1.25, 887 | "baseline": 14 888 | }, 889 | { 890 | "type": "rectangle", 891 | "version": 1168, 892 | "versionNonce": 1709353096, 893 | "isDeleted": false, 894 | "id": "ig1BpRhHNbLaFbYbmIZpi", 895 | "fillStyle": "hachure", 896 | "strokeWidth": 1, 897 | "strokeStyle": "solid", 898 | "roughness": 1, 899 | "opacity": 100, 900 | "angle": 0, 901 | "x": 857.4029636028104, 902 | "y": 604.5181579589844, 903 | "strokeColor": "#1e1e1e", 904 | "backgroundColor": "transparent", 905 | "width": 324, 906 | "height": 119, 907 | "seed": 145637112, 908 | "groupIds": [], 909 | "frameId": null, 910 | "roundness": { 911 | "type": 3 912 | }, 913 | "boundElements": [ 914 | { 915 | "type": "text", 916 | "id": "pvQ_JR5_rKah9DlwKsdYQ" 917 | }, 918 | { 919 | "id": "ry5pO68gzwkZ4Vb9SMZ_J", 920 | "type": "arrow" 921 | } 922 | ], 923 | "updated": 1687115231671, 924 | "link": null, 925 | "locked": false 926 | }, 927 | { 928 | "type": "text", 929 | "version": 879, 930 | "versionNonce": 1566573560, 931 | "isDeleted": false, 932 | "id": "pvQ_JR5_rKah9DlwKsdYQ", 933 | "fillStyle": "hachure", 934 | "strokeWidth": 1, 935 | "strokeStyle": "solid", 936 | "roughness": 1, 937 | "opacity": 100, 938 | "angle": 0, 939 | "x": 956.475023234158, 940 | "y": 644.0181579589844, 941 | "strokeColor": "#1e1e1e", 942 | "backgroundColor": "transparent", 943 | "width": 125.85588073730469, 944 | "height": 40, 945 | "seed": 323738872, 946 | "groupIds": [], 947 | "frameId": null, 948 | "roundness": null, 949 | "boundElements": [], 950 | "updated": 1687115231671, 951 | "link": null, 952 | "locked": false, 953 | "fontSize": 16, 954 | "fontFamily": 1, 955 | "text": "Dev Space\n192.168.55.0/24", 956 | "textAlign": "center", 957 | "verticalAlign": "middle", 958 | "containerId": "ig1BpRhHNbLaFbYbmIZpi", 959 | "originalText": "Dev Space\n192.168.55.0/24", 960 | "lineHeight": 1.25, 961 | "baseline": 34 962 | }, 963 | { 964 | "type": "arrow", 965 | "version": 116, 966 | "versionNonce": 730617736, 967 | "isDeleted": false, 968 | "id": "VQfCHZ8aJKGszcw5J2XEq", 969 | "fillStyle": "hachure", 970 | "strokeWidth": 1, 971 | "strokeStyle": "solid", 972 | "roughness": 1, 973 | "opacity": 100, 974 | "angle": 0, 975 | "x": 802.5665283203125, 976 | "y": 500.1767578125, 977 | "strokeColor": "#1e1e1e", 978 | "backgroundColor": "transparent", 979 | "width": 51.2691650390625, 980 | "height": 27.30865478515625, 981 | "seed": 244606712, 982 | "groupIds": [], 983 | "frameId": null, 984 | "roundness": { 985 | "type": 2 986 | }, 987 | "boundElements": [], 988 | "updated": 1687115231671, 989 | "link": null, 990 | "locked": false, 991 | "startBinding": { 992 | "elementId": "dEfFkxYgaAnTckB9x3DSd", 993 | "focus": -0.27446845213516713, 994 | "gap": 5.939868940089468 995 | }, 996 | "endBinding": { 997 | "elementId": "93PK33yAD1Eh6FYupGfMe", 998 | "focus": 0.20857085770712042, 999 | "gap": 1.4327392578125 1000 | }, 1001 | "lastCommittedPoint": null, 1002 | "startArrowhead": null, 1003 | "endArrowhead": "arrow", 1004 | "points": [ 1005 | [ 1006 | 0, 1007 | 0 1008 | ], 1009 | [ 1010 | 51.2691650390625, 1011 | -27.30865478515625 1012 | ] 1013 | ] 1014 | }, 1015 | { 1016 | "type": "arrow", 1017 | "version": 1072, 1018 | "versionNonce": 1672267000, 1019 | "isDeleted": false, 1020 | "id": "ry5pO68gzwkZ4Vb9SMZ_J", 1021 | "fillStyle": "hachure", 1022 | "strokeWidth": 1, 1023 | "strokeStyle": "solid", 1024 | "roughness": 1, 1025 | "opacity": 100, 1026 | "angle": 0, 1027 | "x": 807.4815569871761, 1028 | "y": 574.6720413788194, 1029 | "strokeColor": "#1e1e1e", 1030 | "backgroundColor": "transparent", 1031 | "width": 47.99281031808016, 1032 | "height": 35.14534996513237, 1033 | "seed": 1809806728, 1034 | "groupIds": [], 1035 | "frameId": null, 1036 | "roundness": { 1037 | "type": 2 1038 | }, 1039 | "boundElements": [], 1040 | "updated": 1687115231671, 1041 | "link": null, 1042 | "locked": false, 1043 | "startBinding": { 1044 | "elementId": "dEfFkxYgaAnTckB9x3DSd", 1045 | "gap": 8.838158037719406, 1046 | "focus": 0.040624196767589715 1047 | }, 1048 | "endBinding": { 1049 | "elementId": "ig1BpRhHNbLaFbYbmIZpi", 1050 | "gap": 1.928596297554344, 1051 | "focus": -0.3696381824635874 1052 | }, 1053 | "lastCommittedPoint": null, 1054 | "startArrowhead": null, 1055 | "endArrowhead": "arrow", 1056 | "points": [ 1057 | [ 1058 | 0, 1059 | 0 1060 | ], 1061 | [ 1062 | 47.99281031808016, 1063 | 35.14534996513237 1064 | ] 1065 | ] 1066 | }, 1067 | { 1068 | "type": "arrow", 1069 | "version": 161, 1070 | "versionNonce": 237512328, 1071 | "isDeleted": false, 1072 | "id": "9nPAJ7PAd4hxJmjAIq02G", 1073 | "fillStyle": "hachure", 1074 | "strokeWidth": 1, 1075 | "strokeStyle": "solid", 1076 | "roughness": 1, 1077 | "opacity": 100, 1078 | "angle": 0, 1079 | "x": 824.5851221339524, 1080 | "y": 545.0965934725904, 1081 | "strokeColor": "#1e1e1e", 1082 | "backgroundColor": "transparent", 1083 | "width": 61.33627772624686, 1084 | "height": 0.6121892958706212, 1085 | "seed": 1026308488, 1086 | "groupIds": [], 1087 | "frameId": null, 1088 | "roundness": { 1089 | "type": 2 1090 | }, 1091 | "boundElements": [], 1092 | "updated": 1687115231671, 1093 | "link": null, 1094 | "locked": false, 1095 | "startBinding": { 1096 | "elementId": "dEfFkxYgaAnTckB9x3DSd", 1097 | "focus": 0.14493282854590847, 1098 | "gap": 10.294481929745153 1099 | }, 1100 | "endBinding": { 1101 | "elementId": "V7qkmN0uiW0cuBR6l0YIz", 1102 | "focus": 2.577656556353779, 1103 | "gap": 14.920431413307995 1104 | }, 1105 | "lastCommittedPoint": null, 1106 | "startArrowhead": null, 1107 | "endArrowhead": "arrow", 1108 | "points": [ 1109 | [ 1110 | 0, 1111 | 0 1112 | ], 1113 | [ 1114 | 61.33627772624686, 1115 | 0.6121892958706212 1116 | ] 1117 | ] 1118 | }, 1119 | { 1120 | "type": "text", 1121 | "version": 367, 1122 | "versionNonce": 1689871864, 1123 | "isDeleted": false, 1124 | "id": "t27HBOw67Eako7AJvuisJ", 1125 | "fillStyle": "hachure", 1126 | "strokeWidth": 1, 1127 | "strokeStyle": "solid", 1128 | "roughness": 1, 1129 | "opacity": 100, 1130 | "angle": 0, 1131 | "x": 730.5825381609218, 1132 | "y": 559.794405610372, 1133 | "strokeColor": "#1e1e1e", 1134 | "backgroundColor": "transparent", 1135 | "width": 64.32262938329698, 1136 | "height": 13.384946977153058, 1137 | "seed": 1805339528, 1138 | "groupIds": [], 1139 | "frameId": null, 1140 | "roundness": null, 1141 | "boundElements": [], 1142 | "updated": 1687115231671, 1143 | "link": null, 1144 | "locked": false, 1145 | "fontSize": 10.707957581722459, 1146 | "fontFamily": 1, 1147 | "text": "192.168.55.3", 1148 | "textAlign": "center", 1149 | "verticalAlign": "top", 1150 | "containerId": null, 1151 | "originalText": "192.168.55.3", 1152 | "lineHeight": 1.25, 1153 | "baseline": 9 1154 | }, 1155 | { 1156 | "type": "text", 1157 | "version": 485, 1158 | "versionNonce": 1785861512, 1159 | "isDeleted": false, 1160 | "id": "V7qkmN0uiW0cuBR6l0YIz", 1161 | "fillStyle": "hachure", 1162 | "strokeWidth": 1, 1163 | "strokeStyle": "solid", 1164 | "roughness": 1, 1165 | "opacity": 100, 1166 | "angle": 0, 1167 | "x": 889.6696359847726, 1168 | "y": 560.629214181769, 1169 | "strokeColor": "#1e1e1e", 1170 | "backgroundColor": "transparent", 1171 | "width": 82.14831815704797, 1172 | "height": 17.007934694402934, 1173 | "seed": 1051576312, 1174 | "groupIds": [], 1175 | "frameId": null, 1176 | "roundness": null, 1177 | "boundElements": [ 1178 | { 1179 | "id": "9nPAJ7PAd4hxJmjAIq02G", 1180 | "type": "arrow" 1181 | } 1182 | ], 1183 | "updated": 1687115231671, 1184 | "link": null, 1185 | "locked": false, 1186 | "fontSize": 13.606347755522352, 1187 | "fontFamily": 1, 1188 | "text": "192.168.55.2", 1189 | "textAlign": "center", 1190 | "verticalAlign": "top", 1191 | "containerId": null, 1192 | "originalText": "192.168.55.2", 1193 | "lineHeight": 1.25, 1194 | "baseline": 12 1195 | }, 1196 | { 1197 | "type": "rectangle", 1198 | "version": 871, 1199 | "versionNonce": 631645320, 1200 | "isDeleted": false, 1201 | "id": "_PpGo8rWFPoQQl2K_qinc", 1202 | "fillStyle": "hachure", 1203 | "strokeWidth": 1, 1204 | "strokeStyle": "solid", 1205 | "roughness": 1, 1206 | "opacity": 100, 1207 | "angle": 0, 1208 | "x": 729.1979726490705, 1209 | "y": 502.9481415015766, 1210 | "strokeColor": "#1e1e1e", 1211 | "backgroundColor": "#ffc9c9", 1212 | "width": 62.480828706789914, 1213 | "height": 20, 1214 | "seed": 951780856, 1215 | "groupIds": [ 1216 | "ULsDiK59w5wToTTxtItFw" 1217 | ], 1218 | "frameId": null, 1219 | "roundness": { 1220 | "type": 3 1221 | }, 1222 | "boundElements": [ 1223 | { 1224 | "type": "text", 1225 | "id": "yT6CW5Ym0_rK7R8-_Ehl3" 1226 | } 1227 | ], 1228 | "updated": 1687115231671, 1229 | "link": null, 1230 | "locked": false 1231 | }, 1232 | { 1233 | "type": "text", 1234 | "version": 842, 1235 | "versionNonce": 178407416, 1236 | "isDeleted": false, 1237 | "id": "yT6CW5Ym0_rK7R8-_Ehl3", 1238 | "fillStyle": "hachure", 1239 | "strokeWidth": 1, 1240 | "strokeStyle": "solid", 1241 | "roughness": 1, 1242 | "opacity": 100, 1243 | "angle": 0, 1244 | "x": 739.9720402617428, 1245 | "y": 507.94996733902497, 1246 | "strokeColor": "#1e1e1e", 1247 | "backgroundColor": "#ffc9c9", 1248 | "width": 40.93269348144531, 1249 | "height": 9.996348325102836, 1250 | "seed": 1523517832, 1251 | "groupIds": [ 1252 | "ULsDiK59w5wToTTxtItFw" 1253 | ], 1254 | "frameId": null, 1255 | "roundness": null, 1256 | "boundElements": [], 1257 | "updated": 1687115231671, 1258 | "link": null, 1259 | "locked": false, 1260 | "fontSize": 7.99707866008227, 1261 | "fontFamily": 1, 1262 | "text": "Bind9 DNS", 1263 | "textAlign": "center", 1264 | "verticalAlign": "middle", 1265 | "containerId": "_PpGo8rWFPoQQl2K_qinc", 1266 | "originalText": "Bind9 DNS", 1267 | "lineHeight": 1.25, 1268 | "baseline": 7 1269 | }, 1270 | { 1271 | "type": "rectangle", 1272 | "version": 1229, 1273 | "versionNonce": 406626952, 1274 | "isDeleted": false, 1275 | "id": "yA0ERTKbrrWtwr2uiW-3n", 1276 | "fillStyle": "hachure", 1277 | "strokeWidth": 1, 1278 | "strokeStyle": "solid", 1279 | "roughness": 1, 1280 | "opacity": 100, 1281 | "angle": 0, 1282 | "x": 995.3146418610113, 1283 | "y": 496.1645781716638, 1284 | "strokeColor": "#1e1e1e", 1285 | "backgroundColor": "#ffc9c9", 1286 | "width": 64.11652475853727, 1287 | "height": 41.047166681750234, 1288 | "seed": 1013225720, 1289 | "groupIds": [ 1290 | "9SpBNLuqDbuiMZyR5D1Q_" 1291 | ], 1292 | "frameId": null, 1293 | "roundness": { 1294 | "type": 3 1295 | }, 1296 | "boundElements": [ 1297 | { 1298 | "type": "text", 1299 | "id": "BxT5Y28o0heKi9eLxXWcb" 1300 | } 1301 | ], 1302 | "updated": 1687115231671, 1303 | "link": null, 1304 | "locked": false 1305 | }, 1306 | { 1307 | "type": "text", 1308 | "version": 1606, 1309 | "versionNonce": 289057272, 1310 | "isDeleted": false, 1311 | "id": "BxT5Y28o0heKi9eLxXWcb", 1312 | "fillStyle": "hachure", 1313 | "strokeWidth": 1, 1314 | "strokeStyle": "solid", 1315 | "roughness": 1, 1316 | "opacity": 100, 1317 | "angle": 0, 1318 | "x": 1003.0814079634245, 1319 | "y": 501.226297295814, 1320 | "strokeColor": "#1e1e1e", 1321 | "backgroundColor": "#ffc9c9", 1322 | "width": 48.58299255371094, 1323 | "height": 30.923728433449853, 1324 | "seed": 2136390136, 1325 | "groupIds": [ 1326 | "9SpBNLuqDbuiMZyR5D1Q_" 1327 | ], 1328 | "frameId": null, 1329 | "roundness": null, 1330 | "boundElements": [], 1331 | "updated": 1687115231671, 1332 | "link": null, 1333 | "locked": false, 1334 | "fontSize": 8.246327582253294, 1335 | "fontFamily": 1, 1336 | "text": "Postgres \n(Terraform \nState)", 1337 | "textAlign": "center", 1338 | "verticalAlign": "middle", 1339 | "containerId": "yA0ERTKbrrWtwr2uiW-3n", 1340 | "originalText": "Postgres (Terraform State)", 1341 | "lineHeight": 1.25, 1342 | "baseline": 28 1343 | }, 1344 | { 1345 | "type": "rectangle", 1346 | "version": 1458, 1347 | "versionNonce": 1363323640, 1348 | "isDeleted": false, 1349 | "id": "Uv_oTSKNezIV8SqEM2vlW", 1350 | "fillStyle": "hachure", 1351 | "strokeWidth": 1, 1352 | "strokeStyle": "solid", 1353 | "roughness": 1, 1354 | "opacity": 100, 1355 | "angle": 0, 1356 | "x": 996.6738998058119, 1357 | "y": 544.4525845266157, 1358 | "strokeColor": "#1e1e1e", 1359 | "backgroundColor": "#ffc9c9", 1360 | "width": 64.11652475853727, 1361 | "height": 41.047166681750234, 1362 | "seed": 2022326520, 1363 | "groupIds": [ 1364 | "_OFBCenU1bXWiC-hvcl_Z" 1365 | ], 1366 | "frameId": null, 1367 | "roundness": { 1368 | "type": 3 1369 | }, 1370 | "boundElements": [ 1371 | { 1372 | "type": "text", 1373 | "id": "W2wo-tcFBd9S8YqKW8vDJ" 1374 | } 1375 | ], 1376 | "updated": 1687115231672, 1377 | "link": null, 1378 | "locked": false 1379 | }, 1380 | { 1381 | "type": "text", 1382 | "version": 1873, 1383 | "versionNonce": 1506517128, 1384 | "isDeleted": false, 1385 | "id": "W2wo-tcFBd9S8YqKW8vDJ", 1386 | "fillStyle": "hachure", 1387 | "strokeWidth": 1, 1388 | "strokeStyle": "solid", 1389 | "roughness": 1, 1390 | "opacity": 100, 1391 | "angle": 0, 1392 | "x": 1010.8390194848852, 1393 | "y": 549.5143036507659, 1394 | "strokeColor": "#1e1e1e", 1395 | "backgroundColor": "#ffc9c9", 1396 | "width": 35.786285400390625, 1397 | "height": 30.923728433449853, 1398 | "seed": 2022138360, 1399 | "groupIds": [ 1400 | "_OFBCenU1bXWiC-hvcl_Z" 1401 | ], 1402 | "frameId": null, 1403 | "roundness": null, 1404 | "boundElements": [], 1405 | "updated": 1687115231672, 1406 | "link": null, 1407 | "locked": false, 1408 | "fontSize": 8.246327582253294, 1409 | "fontFamily": 1, 1410 | "text": "Caddy \n(HTTPS \nWebUI)", 1411 | "textAlign": "center", 1412 | "verticalAlign": "middle", 1413 | "containerId": "Uv_oTSKNezIV8SqEM2vlW", 1414 | "originalText": "Caddy (HTTPS WebUI)", 1415 | "lineHeight": 1.25, 1416 | "baseline": 28 1417 | }, 1418 | { 1419 | "type": "rectangle", 1420 | "version": 1296, 1421 | "versionNonce": 1167340022, 1422 | "isDeleted": false, 1423 | "id": "hO1ZKq4ceRpTs5SNs9th3", 1424 | "fillStyle": "hachure", 1425 | "strokeWidth": 1, 1426 | "strokeStyle": "solid", 1427 | "roughness": 1, 1428 | "opacity": 100, 1429 | "angle": 0, 1430 | "x": 1071.3333391832314, 1431 | "y": 518.9729376552186, 1432 | "strokeColor": "#1e1e1e", 1433 | "backgroundColor": "#ffc9c9", 1434 | "width": 64.11652475853727, 1435 | "height": 41.047166681750234, 1436 | "seed": 45518134, 1437 | "groupIds": [ 1438 | "Dc1IOxbJC3tAeF3-_gJui" 1439 | ], 1440 | "frameId": null, 1441 | "roundness": { 1442 | "type": 3 1443 | }, 1444 | "boundElements": [ 1445 | { 1446 | "type": "text", 1447 | "id": "yT3IqTR0Rh1hA8x_Q3UBO" 1448 | }, 1449 | { 1450 | "id": "ThdjN4Wz5zWBpd0GAUhdh", 1451 | "type": "arrow" 1452 | } 1453 | ], 1454 | "updated": 1704308660815, 1455 | "link": null, 1456 | "locked": false 1457 | }, 1458 | { 1459 | "type": "text", 1460 | "version": 1698, 1461 | "versionNonce": 1847903222, 1462 | "isDeleted": false, 1463 | "id": "yT3IqTR0Rh1hA8x_Q3UBO", 1464 | "fillStyle": "hachure", 1465 | "strokeWidth": 1, 1466 | "strokeStyle": "solid", 1467 | "roughness": 1, 1468 | "opacity": 100, 1469 | "angle": 0, 1470 | "x": 1088.474934577942, 1471 | "y": 524.0346567793688, 1472 | "strokeColor": "#1e1e1e", 1473 | "backgroundColor": "#ffc9c9", 1474 | "width": 29.83333396911621, 1475 | "height": 30.923728433449853, 1476 | "seed": 840660598, 1477 | "groupIds": [ 1478 | "Dc1IOxbJC3tAeF3-_gJui" 1479 | ], 1480 | "frameId": null, 1481 | "roundness": null, 1482 | "boundElements": [], 1483 | "updated": 1704308415953, 1484 | "link": null, 1485 | "locked": false, 1486 | "fontSize": 8.246327582253294, 1487 | "fontFamily": 1, 1488 | "text": "dex \n(OIDC \nGitHub)", 1489 | "textAlign": "center", 1490 | "verticalAlign": "middle", 1491 | "containerId": "hO1ZKq4ceRpTs5SNs9th3", 1492 | "originalText": "dex \n(OIDC GitHub)", 1493 | "lineHeight": 1.25, 1494 | "baseline": 28 1495 | }, 1496 | { 1497 | "type": "diamond", 1498 | "version": 233, 1499 | "versionNonce": 856706230, 1500 | "isDeleted": false, 1501 | "id": "6qNaCztUjv0trKI6t8-sp", 1502 | "fillStyle": "hachure", 1503 | "strokeWidth": 1, 1504 | "strokeStyle": "solid", 1505 | "roughness": 1, 1506 | "opacity": 100, 1507 | "angle": 0, 1508 | "x": 509.016845703125, 1509 | "y": 278.29869079589844, 1510 | "strokeColor": "#1e1e1e", 1511 | "backgroundColor": "#ffc9c9", 1512 | "width": 154.74951171875, 1513 | "height": 130.39566040039062, 1514 | "seed": 297961974, 1515 | "groupIds": [], 1516 | "frameId": null, 1517 | "roundness": { 1518 | "type": 2 1519 | }, 1520 | "boundElements": [ 1521 | { 1522 | "type": "text", 1523 | "id": "L9ESPcB0DSYVUP2GBNWMf" 1524 | }, 1525 | { 1526 | "id": "LI-wrYtEDplvHS_UwzMKY", 1527 | "type": "arrow" 1528 | }, 1529 | { 1530 | "id": "Ila3BcvLfw3__u01XibYe", 1531 | "type": "arrow" 1532 | } 1533 | ], 1534 | "updated": 1704308621061, 1535 | "link": null, 1536 | "locked": false 1537 | }, 1538 | { 1539 | "type": "text", 1540 | "version": 158, 1541 | "versionNonce": 1951609590, 1542 | "isDeleted": false, 1543 | "id": "L9ESPcB0DSYVUP2GBNWMf", 1544 | "fillStyle": "hachure", 1545 | "strokeWidth": 1, 1546 | "strokeStyle": "solid", 1547 | "roughness": 1, 1548 | "opacity": 100, 1549 | "angle": 0, 1550 | "x": 556.437557220459, 1551 | "y": 323.3976058959961, 1552 | "strokeColor": "#1e1e1e", 1553 | "backgroundColor": "#ffc9c9", 1554 | "width": 59.53333282470703, 1555 | "height": 40, 1556 | "seed": 537516854, 1557 | "groupIds": [], 1558 | "frameId": null, 1559 | "roundness": null, 1560 | "boundElements": [], 1561 | "updated": 1704308455595, 1562 | "link": null, 1563 | "locked": false, 1564 | "fontSize": 16, 1565 | "fontFamily": 1, 1566 | "text": "GitHub \nOAuth2", 1567 | "textAlign": "center", 1568 | "verticalAlign": "middle", 1569 | "containerId": "6qNaCztUjv0trKI6t8-sp", 1570 | "originalText": "GitHub OAuth2", 1571 | "lineHeight": 1.25, 1572 | "baseline": 34 1573 | }, 1574 | { 1575 | "id": "LI-wrYtEDplvHS_UwzMKY", 1576 | "type": "arrow", 1577 | "x": 537.3916015625, 1578 | "y": 464.49652099609375, 1579 | "width": 35, 1580 | "height": 57, 1581 | "angle": 0, 1582 | "strokeColor": "#1e1e1e", 1583 | "backgroundColor": "transparent", 1584 | "fillStyle": "solid", 1585 | "strokeWidth": 2, 1586 | "strokeStyle": "solid", 1587 | "roughness": 1, 1588 | "opacity": 100, 1589 | "groupIds": [], 1590 | "frameId": null, 1591 | "roundness": { 1592 | "type": 2 1593 | }, 1594 | "seed": 923757942, 1595 | "version": 41, 1596 | "versionNonce": 906861290, 1597 | "isDeleted": false, 1598 | "boundElements": null, 1599 | "updated": 1704308464687, 1600 | "link": null, 1601 | "locked": false, 1602 | "points": [ 1603 | [ 1604 | 0, 1605 | 0 1606 | ], 1607 | [ 1608 | 35, 1609 | -57 1610 | ] 1611 | ], 1612 | "lastCommittedPoint": null, 1613 | "startBinding": { 1614 | "elementId": "d_Bx9-NZIHpaV1NqLrI_Y", 1615 | "focus": 0.055945031701886944, 1616 | "gap": 12.337181966430563 1617 | }, 1618 | "endBinding": { 1619 | "elementId": "6qNaCztUjv0trKI6t8-sp", 1620 | "focus": -0.32695735622111, 1621 | "gap": 8.105150145370978 1622 | }, 1623 | "startArrowhead": null, 1624 | "endArrowhead": "arrow" 1625 | }, 1626 | { 1627 | "id": "Ila3BcvLfw3__u01XibYe", 1628 | "type": "arrow", 1629 | "x": 656.3916015625, 1630 | "y": 464.49652099609375, 1631 | "width": 43, 1632 | "height": 73, 1633 | "angle": 0, 1634 | "strokeColor": "#1e1e1e", 1635 | "backgroundColor": "transparent", 1636 | "fillStyle": "solid", 1637 | "strokeWidth": 2, 1638 | "strokeStyle": "solid", 1639 | "roughness": 1, 1640 | "opacity": 100, 1641 | "groupIds": [], 1642 | "frameId": null, 1643 | "roundness": null, 1644 | "seed": 1334028714, 1645 | "version": 31, 1646 | "versionNonce": 890841974, 1647 | "isDeleted": false, 1648 | "boundElements": null, 1649 | "updated": 1704308621061, 1650 | "link": null, 1651 | "locked": false, 1652 | "points": [ 1653 | [ 1654 | 0, 1655 | 0 1656 | ], 1657 | [ 1658 | -43, 1659 | -73 1660 | ] 1661 | ], 1662 | "lastCommittedPoint": null, 1663 | "startBinding": { 1664 | "elementId": "Bi126oqn4ynOGDKCgzu76", 1665 | "focus": -0.5919300670164235, 1666 | "gap": 3.458566537319939 1667 | }, 1668 | "endBinding": { 1669 | "elementId": "6qNaCztUjv0trKI6t8-sp", 1670 | "focus": 0.01646496442657747, 1671 | "gap": 4.246480905699265 1672 | }, 1673 | "startArrowhead": null, 1674 | "endArrowhead": "arrow" 1675 | } 1676 | ], 1677 | "appState": { 1678 | "gridSize": null, 1679 | "viewBackgroundColor": "#ffffff" 1680 | }, 1681 | "files": {} 1682 | } -------------------------------------------------------------------------------- /docs/img/lab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lab-astr0rack-net/core/850473568d545451500c7644c37de2121a3aad2b/docs/img/lab.png -------------------------------------------------------------------------------- /tailscale/acl.hujson: -------------------------------------------------------------------------------- 1 | // ACL managed via GH actions @ https://github.com/lab-astr0rack-net/core 2 | { 3 | "tagOwners": { 4 | "tag:actions": ["astr0n8t@github"] 5 | }, 6 | // Access control lists. 7 | "acls": [ 8 | // Allow generic access to lab net 9 | { 10 | "action": "accept", 11 | "src": ["*"], 12 | "dst": ["10.0.0.0/16:*"] 13 | }, 14 | // Allow generic access to DNS 15 | { 16 | "action": "accept", 17 | "src": ["*"], 18 | "dst": ["192.168.55.3:53"], 19 | }, 20 | // Allow generic access to Proxmox WebUI 21 | { 22 | "action": "accept", 23 | "src": ["*"], 24 | "proto": "tcp", 25 | "dst": ["192.168.55.2:443"], 26 | }, 27 | // Allow template VMs to reach packer http port 28 | { 29 | "action": "accept", 30 | "src": ["10.0.1.0/24"], 31 | "proto": "tcp", 32 | "dst": ["tag:actions:8888"] 33 | }, 34 | // Allow Actions to reach Postgres 35 | // for Terraform state 36 | { 37 | "action": "accept", 38 | "src": ["tag:actions"], 39 | "proto": "tcp", 40 | "dst": ["192.168.55.2:5432"], 41 | }, 42 | // Allow Actions to reach Proxmox SSH 43 | // for Ansible 44 | { 45 | "action": "accept", 46 | "src": ["tag:actions"], 47 | "proto": "tcp", 48 | "dst": ["192.168.55.2:22"], 49 | }, 50 | // Allow Actions to reach VyOS SSH 51 | // for Ansible 52 | { 53 | "action": "accept", 54 | "src": ["tag:actions"], 55 | "proto": "tcp", 56 | "dst": ["192.168.55.3:22"], 57 | }, 58 | // Allow generic access to ephemeral 59 | { 60 | "action": "accept", 61 | "src": ["*"], 62 | "dst": ["192.168.55.0/24:*"] 63 | }, 64 | ], 65 | } 66 | 67 | -------------------------------------------------------------------------------- /terraform/vyos/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/telmate/proxmox" { 5 | version = "2.9.14" 6 | constraints = "2.9.14" 7 | hashes = [ 8 | "h1:KWPlH228/+BiH8OOY754MTTaUkLJLN/1krSSzqiyNsE=", 9 | "zh:0d049d33f705e5b814d30028770c084151218439424e99684ce31d7e26a720b5", 10 | "zh:20b1c64ed56d81de95f3f37b82b45b4654c0de26670c0e87a474c5cce13cd015", 11 | "zh:2946058abd1d8e50e475b9ec39781eb02576b40dbd80f4653fade4493a4514c6", 12 | "zh:29e50a25c456f040ce072f23ac57b5b82ebd3b916ca5ae6688332b5ec62adc4a", 13 | "zh:3612932306ce5f08db94868f526cbb8c56d0d3c6ebe1c11a83f92bbf94354296", 14 | "zh:42d1699b0abebaac82ea5a19f4393541d8bb2741bde204a8ac1028cdc29d1b14", 15 | "zh:5ffd5dc567262eb8aafdf2f6eac63f7f21361da9c5d75a3c36b479638a0001b0", 16 | "zh:6692ef323e3b89de99934ad731f6a1850525bf8142916ae28ea4e4048d73a787", 17 | "zh:a5afc98e9a4038516bb58e788cb77dea67a60dce780dfcd206d7373c5a56b776", 18 | "zh:bf902cded709d84fa27fbf91b589c241f2238a6c4924e4e479eebd74320b93a5", 19 | "zh:cab0e1e72c9cebcf669fc6f35ec28cb8ab2dffb0237afc8860aa40d23bf8a49f", 20 | "zh:e523b99a48beec83d9bc04b2d336266044f9f53514cefb652fe6768611847196", 21 | "zh:f593915e8a24829d322d2eaeedcb153328cf9042f0d84f66040dde1be70ede04", 22 | "zh:fba1aff541133e2129dfda0160369635ab48503d5c44b8407ce5922ecc15d0bd", 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /terraform/vyos/Makefile: -------------------------------------------------------------------------------- 1 | all: init apply 2 | clean: destroy 3 | 4 | apply: 5 | scripts/local.sh apply 6 | destroy: 7 | scripts/local.sh destroy 8 | plan: 9 | scripts/local.sh plan 10 | init: 11 | scripts/local.sh init 12 | fmt: 13 | terraform fmt 14 | -------------------------------------------------------------------------------- /terraform/vyos/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | backend "pg" {} 3 | required_providers { 4 | proxmox = { 5 | source = "Telmate/proxmox" 6 | version = "2.9.14" 7 | } 8 | } 9 | } 10 | 11 | provider "proxmox" { 12 | # Configuration options 13 | pm_api_url = var.pm_api_url 14 | pm_user = var.pm_user 15 | pm_password = var.pm_password 16 | pm_log_enable = true 17 | pm_log_file = "/tmp/terraform-plugin-proxmox.log" 18 | pm_debug = true 19 | pm_log_levels = { 20 | _default = "debug" 21 | _capturelog = "" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /terraform/vyos/scripts/local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | if [ -z "$BW_SESSION" ] 4 | then 5 | export BW_SESSION=$(bw unlock --raw) 6 | fi 7 | 8 | export PG_CONN_STR=$(bw get password terraform-proxmox-postgres) 9 | export PG_SCHEMA_NAME="vyos" 10 | export TF_VAR_pm_password=$(bw get password terraform-proxmox-credentials) 11 | 12 | terraform "$@" 13 | -------------------------------------------------------------------------------- /terraform/vyos/variables.tf: -------------------------------------------------------------------------------- 1 | variable "pm_api_url" { 2 | default = "https://proxmox.lab.astr0rack.net/api2/json" 3 | } 4 | 5 | variable "pm_user" { 6 | default = "terraform@pve" 7 | } 8 | 9 | variable "pm_password" { 10 | default = "CHANGEME" 11 | sensitive = true 12 | } 13 | 14 | variable "pm_node" { 15 | default = "proxmox" 16 | } 17 | 18 | -------------------------------------------------------------------------------- /terraform/vyos/vyos.tf: -------------------------------------------------------------------------------- 1 | resource "proxmox_vm_qemu" "vyos-core" { 2 | name = "vyos-core" 3 | desc = "VyOS Core Router" 4 | qemu_os = "l26" 5 | bios = "ovmf" 6 | vmid = 100 7 | pool = "infra" 8 | # Need to specify ide2 so the VM can see cloudinit CD 9 | boot = "order=scsi0;ide2" 10 | target_node = var.pm_node 11 | onboot = true 12 | startup = "order=1" 13 | 14 | sockets = 1 15 | cores = 2 16 | memory = 4096 17 | agent = 1 18 | clone = "vyos-1.4-rolling-cloudinit" 19 | full_clone = "true" 20 | scsihw = "virtio-scsi-single" 21 | 22 | network { 23 | bridge = "vmbr0" 24 | model = "e1000" 25 | macaddr = "92:07:2B:55:57:C8" 26 | } 27 | network { 28 | bridge = "vmbr1" 29 | model = "e1000" 30 | } 31 | network { 32 | bridge = "vmbr2" 33 | model = "e1000" 34 | } 35 | network { 36 | bridge = "vmbr3" 37 | model = "e1000" 38 | } 39 | network { 40 | bridge = "vmbr4" 41 | model = "e1000" 42 | } 43 | network { 44 | bridge = "vmbr20" 45 | model = "e1000" 46 | } 47 | network { 48 | bridge = "vmbr99" 49 | model = "e1000" 50 | } 51 | lifecycle { 52 | ignore_changes = [ 53 | disk, 54 | ] 55 | } 56 | 57 | os_type = "cloud-init" 58 | 59 | cicustom = "user=local:snippets/vyos-cloud-config.yml" 60 | cloudinit_cdrom_storage = "local" 61 | } 62 | 63 | output "vyos_default_ip" { 64 | value = proxmox_vm_qemu.vyos-core.default_ipv4_address 65 | } 66 | 67 | --------------------------------------------------------------------------------