├── playbooks ├── my.cnf ├── README.md ├── dbpass.sql ├── nginx │ ├── proxy-confs │ │ └── cloudworkstation.conf │ └── nginx.conf ├── httpd-ssl.conf ├── httpd.conf └── cloud_workstation_aws.yml ├── cloud_workstation_gnome.png ├── cloud_workstation_xfce.png ├── .gitignore ├── README.md └── aws ├── cw-output.tf ├── cw-ami.tf ├── cw-network.tf ├── cw-instance.tf ├── cw-security.tf ├── .terraform.lock.hcl ├── cw.tfvars ├── cw-s3.tf ├── cw-generic.tf ├── cw-iam.tf ├── cw-ssm.tf ├── cw-kmscmk.tf └── README.md /playbooks/my.cnf: -------------------------------------------------------------------------------- 1 | [client] 2 | user = root 3 | password = {{ guacdb_root_pass.stdout }} 4 | -------------------------------------------------------------------------------- /cloud_workstation_gnome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chadgeary/cloudworkstation/HEAD/cloud_workstation_gnome.png -------------------------------------------------------------------------------- /cloud_workstation_xfce.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chadgeary/cloudworkstation/HEAD/cloud_workstation_xfce.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tf-nifi-lambda-node-down.zip 2 | terraform.* 3 | *.terraform 4 | *.terraform*.tfstate*.lock*. 5 | pvars.* 6 | -------------------------------------------------------------------------------- /playbooks/README.md: -------------------------------------------------------------------------------- 1 | # Reference 2 | The Ansible playbook to install/configure a desktop (xfce), remote desktop (xrdp), web interface (Guacamole), and web proxy (Apache httpd). 3 | -------------------------------------------------------------------------------- /playbooks/dbpass.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE guacamole_db; 2 | CREATE USER 'guacamole_user'@'{{ guacnet_guacamole }}' IDENTIFIED BY '{{ guacdb_guacamole_pass.stdout }}'; 3 | GRANT SELECT,INSERT,UPDATE,DELETE ON guacamole_db.* TO 'guacamole_user'@'{{ guacnet_guacamole }}'; 4 | FLUSH PRIVILEGES; 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reference 2 | A browser-based linux desktop workstation in AWS. Built with Apache Guacamole, deployed automatically via Terraform and Ansible. Step-by-step instructions included! Desktops supported include gnome and XFCE: 3 | ![gnome](cloud_workstation_gnome.png) 4 | ![XFCE](cloud_workstation_xfce.png) 5 | 6 | # Instructions 7 | See the sub-directory of each cloud provider for specific instructions, including Windows users. 8 | 9 | # Discussion 10 | [Discord Room](https://discord.gg/vG3UKd2RRn) 11 | -------------------------------------------------------------------------------- /aws/cw-output.tf: -------------------------------------------------------------------------------- 1 | output "cloudworkstation-output" { 2 | value = < 13 | 14 | # proxy to pihole 15 | ProxyPreserveHost On 16 | ProxyPass / http://{{ guacnet_guacamole }}:8080/ 17 | ProxyPassReverse / http://{{ guacnet_guacamole }}:8080/ 18 | 19 | # vhost settings 20 | DocumentRoot "/usr/local/apache2/htdocs" 21 | ServerName {{ guacnet_webproxy }}:443 22 | ServerAdmin root@{{ guacnet_webproxy }} 23 | ErrorLog /proc/self/fd/2 24 | TransferLog /proc/self/fd/1 25 | Header always set Strict-Transport-Security "max-age=63072000" 26 | SSLEngine on 27 | 28 | # mounted via Docker 29 | SSLCertificateFile "/usr/local/apache2/conf/server.crt" 30 | SSLCertificateKeyFile "/usr/local/apache2/conf/server.key" 31 | 32 | 33 | SSLOptions +StdEnvVars 34 | 35 | 36 | SSLOptions +StdEnvVars 37 | 38 | 39 | # vhost log 40 | CustomLog /proc/self/fd/1 \ 41 | "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b" 42 | 43 | -------------------------------------------------------------------------------- /aws/cw-instance.tf: -------------------------------------------------------------------------------- 1 | # Instance Key 2 | resource "aws_key_pair" "cw-instance-key" { 3 | key_name = "${var.name_prefix}-instance-key-${random_string.cw-random.result}" 4 | public_key = var.instance_key 5 | tags = { 6 | Name = "${var.name_prefix}-instance-key" 7 | } 8 | } 9 | 10 | # Instance(s) 11 | resource "aws_instance" "cw-instance-1" { 12 | ami = aws_ami_copy.cw-latest-vendor-ami-with-cmk.id 13 | instance_type = var.instance_type 14 | iam_instance_profile = aws_iam_instance_profile.cw-instance-profile.name 15 | key_name = aws_key_pair.cw-instance-key.key_name 16 | subnet_id = aws_subnet.cw-net.id 17 | private_ip = var.net_instance_ip 18 | vpc_security_group_ids = [aws_security_group.cw-sg.id] 19 | tags = { 20 | Name = "${var.name_prefix}-workstation-1-${random_string.cw-random.result}", 21 | cw = "True" 22 | } 23 | user_data = < 45 | #LoadModule cgid_module modules/mod_cgid.so 46 | 47 | 48 | #LoadModule cgi_module modules/mod_cgi.so 49 | 50 | 51 | LoadModule dir_module modules/mod_dir.so 52 | LoadModule alias_module modules/mod_alias.so 53 | 54 | 55 | User daemon 56 | Group daemon 57 | 58 | 59 | ServerAdmin root@{{ guacnet_webproxy }} 60 | 61 | 62 | AllowOverride none 63 | Require all denied 64 | 65 | 66 | DocumentRoot "/usr/local/apache2/htdocs" 67 | 68 | 69 | Options Indexes FollowSymLinks 70 | AllowOverride None 71 | Require all granted 72 | 73 | 74 | 75 | DirectoryIndex index.html 76 | 77 | 78 | 79 | Require all denied 80 | 81 | 82 | ErrorLog /proc/self/fd/2 83 | LogLevel warn 84 | 85 | 86 | LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined 87 | LogFormat "%h %l %u %t \"%r\" %>s %b" common 88 | 89 | # You need to enable mod_logio.c to use %I and %O 90 | LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio 91 | 92 | CustomLog /proc/self/fd/1 common 93 | 94 | 95 | 96 | ScriptAlias /cgi-bin/ "/usr/local/apache2/cgi-bin/" 97 | 98 | 99 | 100 | 101 | 102 | 103 | AllowOverride None 104 | Options None 105 | Require all granted 106 | 107 | 108 | 109 | RequestHeader unset Proxy early 110 | 111 | 112 | 113 | TypesConfig conf/mime.types 114 | AddType application/x-compress .Z 115 | AddType application/x-gzip .gz .tgz 116 | 117 | 118 | 119 | Include conf/extra/proxy-html.conf 120 | 121 | 122 | Include conf/extra/httpd-ssl.conf 123 | 124 | 125 | SSLRandomSeed startup builtin 126 | SSLRandomSeed connect builtin 127 | 128 | -------------------------------------------------------------------------------- /aws/cw-ssm.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ssm_parameter" "cw-ssm-param-pass" { 2 | name = "${var.name_prefix}-cw-web-password-${random_string.cw-random.result}" 3 | type = "SecureString" 4 | key_id = aws_kms_key.cw-kmscmk-ssm.key_id 5 | value = var.cw_password 6 | } 7 | 8 | resource "aws_ssm_document" "cw-ssm-doc" { 9 | name = "${var.name_prefix}-ssm-doc-${random_string.cw-random.result}" 10 | document_type = "Command" 11 | content = <&2", 82 | " exit 2", 83 | "fi", 84 | "/usr/local/bin/ansible-playbook -i \"localhost,\" -c local -e \"{{ExtraVariables}}\" \"{{Verbose}}\" \"$${PlaybookFile}\"" 85 | ] 86 | } 87 | } 88 | ] 89 | } 90 | DOC 91 | } 92 | 93 | resource "aws_ssm_association" "cw-ssm-assoc" { 94 | association_name = "${var.name_prefix}-ssm-assoc-${random_string.cw-random.result}" 95 | name = aws_ssm_document.cw-ssm-doc.name 96 | targets { 97 | key = "tag:cw" 98 | values = ["True"] 99 | } 100 | output_location { 101 | s3_bucket_name = aws_s3_bucket.cw-bucket.id 102 | s3_key_prefix = "ssm" 103 | } 104 | parameters = { 105 | ExtraVariables = "name_prefix=${var.name_prefix} name_suffix=${random_string.cw-random.result} guacnet_cidr=${var.guacnet_cidr} guacnet_guacd=${var.guacnet_guacd} guacnet_guacdb=${var.guacnet_guacdb} guacnet_guacamole=${var.guacnet_guacamole} guacnet_webproxy=${var.guacnet_webproxy} guacnet_duckdnsupdater=${var.guacnet_duckdnsupdater} aws_region=${var.aws_region} desktop=${var.desktop} enable_duckdns=${var.enable_duckdns} duckdns_domain=${var.duckdns_domain} duckdns_token=${var.duckdns_token} letsencrypt_email=${var.letsencrypt_email}" 106 | PlaybookFile = "cloud_workstation_aws.yml" 107 | SourceInfo = "{\"path\":\"https://s3.${var.aws_region}.amazonaws.com/${aws_s3_bucket.cw-bucket.id}/playbook/\"}" 108 | SourceType = "S3" 109 | Verbose = "-v" 110 | } 111 | depends_on = [aws_iam_role_policy_attachment.cw-iam-attach-ssm, aws_iam_role_policy_attachment.cw-iam-attach-s3, aws_s3_bucket_object.cw-workstation-files] 112 | } 113 | -------------------------------------------------------------------------------- /aws/cw-kmscmk.tf: -------------------------------------------------------------------------------- 1 | resource "aws_kms_key" "cw-kmscmk-s3" { 2 | description = "Key for cw s3" 3 | key_usage = "ENCRYPT_DECRYPT" 4 | customer_master_key_spec = "SYMMETRIC_DEFAULT" 5 | enable_key_rotation = "true" 6 | tags = { 7 | Name = "cw-kmscmk-s3-${random_string.cw-random.result}" 8 | } 9 | policy = < Run as Administrator) 15 | 16 | # Enable Windows Subsystem Linux 17 | dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart 18 | 19 | # Reboot your Windows PC 20 | shutdown /r /t 5 21 | 22 | # After reboot, launch a REGULAR Powershell prompt (left click). 23 | # Do NOT proceed with an ELEVATED Powershell prompt. 24 | 25 | # Download the Ubuntu 1804 package from Microsoft 26 | curl.exe -L -o ubuntu-1804.appx https://aka.ms/wsl-ubuntu-1804 27 | 28 | # Rename the package 29 | Rename-Item ubuntu-1804.appx ubuntu-1804.zip 30 | 31 | # Expand the zip 32 | Expand-Archive ubuntu-1804.zip ubuntu-1804 33 | 34 | # Change to the zip directory 35 | cd ubuntu-1804 36 | 37 | # Execute the ubuntu 1804 installer 38 | .\ubuntu1804.exe 39 | 40 | # Create a username and password when prompted 41 | ``` 42 | Install Terraform, Git, and create an SSH key pair 43 | ``` 44 | ############################# 45 | ## Terraform + Git + SSH ## 46 | ############################# 47 | # Add terraform's apt key (enter previously created password at prompt) 48 | curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - 49 | 50 | # Add terraform's apt repository 51 | sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" 52 | 53 | # Install terraform and git 54 | sudo apt-get update && sudo apt-get -y install terraform git 55 | 56 | # Clone the cloud_workstation project 57 | git clone https://github.com/chadgeary/cloud_workstation 58 | 59 | # Create SSH key pair (RETURN for defaults) 60 | ssh-keygen 61 | ``` 62 | 63 | Install the AWS cli and create non-root AWS user 64 | ``` 65 | ############################# 66 | ## AWS ## 67 | ############################# 68 | # Open powershell and start WSL 69 | wsl 70 | 71 | # Change to home directory 72 | cd ~ 73 | 74 | # Install python3 pip 75 | sudo apt update && sudo DEBIAN_FRONTEND=noninteractive apt-get -q -y install python3-pip 76 | 77 | # Install awscli via pip 78 | pip3 install --user --upgrade awscli 79 | 80 | # Create a non-root AWS user in the AWS web console with admin permissions 81 | # This user must be the same user running terraform apply 82 | # Create the user at the AWS Web Console under IAM -> Users -> Add user -> Check programmatic access and AWS Management console -> Attach existing policies -> AdministratorAccess -> copy Access key ID and Secret Access key 83 | # See for more information: https://docs.aws.amazon.com/IAM/latest/UserGuide/getting-started_create-admin-group.html#getting-started_create-admin-group-console 84 | 85 | # Set admin user credentials 86 | ~/.local/bin/aws configure 87 | 88 | # Validate configuration 89 | ~/.local/bin/aws sts get-caller-identity 90 | ``` 91 | 92 | Customize the deployment - See variables section below 93 | ``` 94 | # Change to the project's aws directory in powershell 95 | cd ~/cloud_workstation/aws/ 96 | 97 | # Open File Explorer in a separate window 98 | # Navigate to aws project directory - change \chad\ to your WSL username 99 | %HOMEPATH%\ubuntu-1804\rootfs\home\chad\cloud_workstation\aws 100 | 101 | # Edit the cw.tfvars file using notepad and save 102 | ``` 103 | 104 | Deploy 105 | ``` 106 | # In powershell's WSL window, change to the project's aws directory 107 | cd ~/cloud_workstation/aws/ 108 | 109 | # Initialize terraform and apply the terraform state 110 | terraform init 111 | terraform apply -var-file="cw.tfvars" 112 | 113 | # Note the outputs from terraform after the apply completes 114 | 115 | # Wait for the virtual machine to become ready (Ansible will setup the services for us) 116 | ``` 117 | 118 | Want to watch Ansible setup the virtual machine? SSH to the cloud instance - see the terraform output. 119 | ``` 120 | # Connect to the virtual machine via ssh 121 | ssh ubuntu@ 122 | 123 | # Check the Ansible output (from AWS SSM) 124 | export ASSOC_ID=$(sudo bash -c 'ls -t /var/lib/amazon/ssm/*/document/orchestration/' | awk 'NR==1 { print $1 }') && sudo bash -c 'cat /var/lib/amazon/ssm/i-*/document/orchestration/'"$ASSOC_ID"'/awsrunShellScript/runShellScript/stdout' 125 | ``` 126 | 127 | Alternatively, check [AWS State Manager](https://console.aws.amazon.com/systems-manager/state-manager) though you'll need to be logged into AWS as the user created in the previous AWS steps. 128 | 129 | # Variables 130 | Edit the vars file (cw.tfvars) to customize the deployment, especially: 131 | ``` 132 | # instance_key 133 | # a public SSH key for SSH access to the instance via user `ubuntu`. 134 | # cat ~/.ssh/id_rsa.pub 135 | 136 | # mgmt_cidr 137 | # an IP range granted webUI, SSH access. 138 | # deploying from home? This should be your public IP address with a /32 suffix. 139 | 140 | # kms_manager 141 | # The AWS username (not root) granted access to various resources (including SSM logs in S3, SSM parameter store). 142 | 143 | # cw_password 144 | # password for guacadmin webUI account and ubuntu user 145 | 146 | # aws_region 147 | # region to build the services in 148 | 149 | # And as of July 2021, please use duckdns.org to generate a domain and token and set the appropriate variabels in aws.tfvars 150 | ``` 151 | 152 | # Post-Deployment 153 | - Wait for Ansible Playbook, watch [AWS State Manager](https://console.aws.amazon.com/systems-manager/state-manager) 154 | - See terraform output for WebUI link. Username: `guacadmin` 155 | 156 | # FAQs 157 | - Using an ISP with a dynamic IP (DHCP) and the IP address changed? Pihole webUI and SSH access will be blocked until the mgmt_cidr is updated. 158 | - Follow the steps below to quickly update the cloud firewall using terraform. 159 | 160 | ``` 161 | # Open Powershell and start WSL 162 | wsl 163 | 164 | # Change to the project directory 165 | cd ~/cloud_workstation/aws/ 166 | 167 | # Update the mgmt_cidr variable - be sure to replace change_me with your public IP address 168 | sed -i -e "s#^mgmt_cidr = .*#mgmt_cidr = \"change_me/32\"#" cw.tfvars 169 | 170 | # Rerun terraform apply, terraform will update the cloud firewall rules 171 | terraform apply -var-file="cw.tfvars" 172 | ``` 173 | 174 | - How do I update docker containers? 175 | - Containers must be removed manually first: 176 | - SSH to the cloud_workstation instance. 177 | - Remove the container(s) (local data is kept), e.g.: 178 | ``` 179 | sudo docker rm -f guacd 180 | ``` 181 | - Re-apply the AWS SSM association to re-run the Ansible playbook. Ansible will re-install the container(s). 182 | - Newer versions of cloud_workstation display an AWS CLI command to re-apply the AWS SSM association. 183 | -------------------------------------------------------------------------------- /playbooks/cloud_workstation_aws.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: workstation.yml 3 | hosts: localhost 4 | become: True 5 | become_user: root 6 | tasks: 7 | 8 | - name: if not defined, set duckdns_domain to blank value 9 | set_fact: 10 | duckdns_domain: "{% if duckdns_domain is not defined %}{% else %}{{ duckdns_domain }}{% endif %}" 11 | 12 | - name: if not defined, set duckdns_token to blank value 13 | set_fact: 14 | duckdns_token: "{% if duckdns_token is not defined %}{% else %}{{ duckdns_token }}{% endif %}" 15 | 16 | - name: if not defined, set letsencrypt_email to blank value 17 | set_fact: 18 | letsencrypt_email: "{% if letsencrypt_email is not defined %}{% else %}{{ letsencrypt_email }}{% endif %}" 19 | 20 | - name: if not defined, set docker_duckdnsupdater to blank value 21 | set_fact: 22 | guacnet_duckdnsupdater: "{% if guacnet_duckdnsupdater is not defined %}{% else %}{{ guacnet_duckdnsupdater }}{% endif %}" 23 | 24 | - name: docker pip3 ssl xfce and xrdp 25 | apt: 26 | pkg: 27 | - docker.io 28 | - python3-pip 29 | - ssl-cert 30 | - xfce4 31 | - xfce4-terminal 32 | - xrdp 33 | state: latest 34 | update_cache: yes 35 | retries: 3 36 | delay: 30 37 | register: packages_install 38 | until: packages_install is not failed 39 | when: desktop == "xfce" 40 | 41 | - name: docker gnome pip3 ssl and xrdp 42 | apt: 43 | pkg: 44 | - docker.io 45 | - gdm3 46 | - gnome-session 47 | - gnome-terminal 48 | - python3-pip 49 | - ssl-cert 50 | - xrdp 51 | state: latest 52 | update_cache: yes 53 | retries: 3 54 | delay: 30 55 | register: packages_install 56 | until: packages_install is not failed 57 | when: desktop == "gnome" 58 | 59 | - name: install boto3 botocore and docker python package for ansible 60 | pip: 61 | executable: /usr/bin/pip3 62 | name: 63 | - boto3 64 | - botocore 65 | - docker 66 | 67 | - name: add xrdp user to ssl-cert group 68 | user: 69 | name: xrdp 70 | groups: ssl-cert 71 | append: yes 72 | 73 | - name: set xrdp listen local (docker) only 74 | lineinfile: 75 | path: /etc/xrdp/xrdp.ini 76 | insertbefore: '^; tcp port to listen' 77 | line: address=172.17.0.1 78 | register: xrdp_ini 79 | 80 | - name: disable permit root rdp 81 | lineinfile: 82 | path: /etc/xrdp/sesman.ini 83 | regexp: '^AllowRootLogin=' 84 | line: 'AllowRootLogin=False' 85 | register: sesman_ini 86 | 87 | - name: xsession for ubuntu user 88 | blockinfile: 89 | path: /home/ubuntu/.xsession 90 | create: yes 91 | owner: xrdp 92 | group: xrdp 93 | mode: '0544' 94 | block: | 95 | # Enables the session for ubuntu user 96 | export LOGNAME=$USER 97 | export LIBGL_ALWAYS_INDIRECT=0 98 | unset SESSION_MANAGER 99 | unset DBUS_SESSION_BUS_ADDRESS 100 | {% if desktop == 'xfce' %}xfce4-session{% elif desktop == 'gnome' %}gnome-session{% endif %} 101 | 102 | - name: colord for gnome 103 | blockinfile: 104 | path: /etc/polkit-1/localauthority/50-local.d/allow-colord.pkla 105 | create: yes 106 | owner: root 107 | group: root 108 | mode: '0444' 109 | block: | 110 | [Allow colord for all users] 111 | Identity=unix-user:* 112 | Action=org.freedesktop.color-manager.create-device;org.freedesktop.color-manager.create-profile;org.freedesktop.color-manager.delete-device;org.freedesktop.color-manager.delete-profile;org.freedesktop.color-manager.modify-device;org.freedesktop.color-manager.modify-profile 113 | ResultAny=yes 114 | ResualtInactive=auth_admin 115 | ResultActive=yes 116 | when: desktop == 'gnome' 117 | 118 | - name: set xrdp sesman systemd unit to wait for docker 119 | lineinfile: 120 | path: /lib/systemd/system/xrdp-sesman.service 121 | regexp: '^After=network.target' 122 | line: 'After=network.target docker.service' 123 | 124 | - name: enable / start docker and xrdp 125 | systemd: 126 | name: "{{ item }}" 127 | state: started 128 | enabled: yes 129 | daemon_reload: yes 130 | with_items: 131 | - docker 132 | - xrdp 133 | 134 | - name: restart xrdp if inis changed 135 | systemd: 136 | name: xrdp 137 | state: restarted 138 | when: xrdp_ini.changed or sesman_ini.changed 139 | 140 | - name: Container dirs 141 | file: 142 | path: "/opt/{{ item }}" 143 | state: directory 144 | owner: root 145 | group: root 146 | mode: '0750' 147 | with_items: 148 | - guacamole 149 | - webproxy 150 | - webproxy/nginx 151 | - webproxy/nginx/proxy-confs 152 | 153 | - name: secure web proxy confs 154 | template: 155 | src: "{{ item }}" 156 | dest: "/opt/webproxy/{{ item }}" 157 | owner: root 158 | group: root 159 | mode: 0444 160 | with_items: 161 | - httpd-ssl.conf 162 | - httpd.conf 163 | - nginx/nginx.conf 164 | - nginx/proxy-confs/cloudworkstation.conf 165 | register: proxy_conf_files 166 | 167 | - name: Determine db passwords set (root) 168 | stat: 169 | path: /opt/guacamole/guacdb_root_file 170 | register: guacdb_root_file 171 | 172 | - name: Determine db passwords set (guacamole) 173 | stat: 174 | path: /opt/guacamole/guacdb_guacamole_file 175 | register: guacdb_guacamole_file 176 | 177 | - name: Create db passwords when not set (root) 178 | shell: | 179 | head /dev/urandom | tr -dc A-Za-z0-9 | head -c 20 > /opt/guacamole/guacdb_root_file 180 | when: guacdb_root_file.stat.exists|bool == False 181 | 182 | - name: Create db passwords when not set (guacamole) 183 | shell: | 184 | head /dev/urandom | tr -dc A-Za-z0-9 | head -c 20 > /opt/guacamole/guacdb_guacamole_file 185 | when: guacdb_guacamole_file.stat.exists|bool == False 186 | 187 | - name: Register db passwords 188 | shell: | 189 | cat /opt/guacamole/guacdb_root_file 190 | register: guacdb_root_pass 191 | 192 | - name: Register db pass (guacamole) 193 | shell: | 194 | cat /opt/guacamole/guacdb_guacamole_file 195 | register: guacdb_guacamole_pass 196 | 197 | - name: Docker Volume (db) 198 | docker_volume: 199 | name: guacdb 200 | 201 | - name: Docker Network 202 | docker_network: 203 | name: guacnet 204 | ipam_config: 205 | - subnet: "{{ guacnet_cidr }}" 206 | 207 | - name: Docker Container - guacd 208 | docker_container: 209 | name: guacd 210 | image: guacamole/guacd:1.4.0 211 | networks: 212 | - name: guacnet 213 | ipv4_address: "{{ guacnet_guacd }}" 214 | restart_policy: "always" 215 | 216 | - name: Docker Container - guacdb 217 | docker_container: 218 | name: guacdb 219 | env: 220 | MYSQL_ROOT_PASSWORD: "{{ guacdb_root_pass.stdout }}" 221 | image: mysql/mysql-server 222 | networks: 223 | - name: guacnet 224 | ipv4_address: "{{ guacnet_guacdb }}" 225 | purge_networks: yes 226 | restart_policy: "always" 227 | volumes: 228 | - guacdb:/var/lib/mysql 229 | 230 | - name: Docker Container - guacamole 231 | docker_container: 232 | name: guacamole 233 | env: 234 | MYSQL_HOSTNAME: "{{ guacnet_guacdb }}" 235 | MYSQL_PORT: "3306" 236 | MYSQL_DATABASE: "guacamole_db" 237 | MYSQL_USER: "guacamole_user" 238 | MYSQL_PASSWORD: "{{ guacdb_guacamole_pass.stdout }}" 239 | GUACD_HOSTNAME: "{{ guacnet_guacd }}" 240 | GUACD_PORT: "4822" 241 | GUACD_LOG_LEVEL: "debug" 242 | image: guacamole/guacamole:1.4.0 243 | links: 244 | - "guacd:guacd" 245 | - "guacdb:mysql" 246 | networks: 247 | - name: guacnet 248 | ipv4_address: "{{ guacnet_guacamole }}" 249 | purge_networks: yes 250 | restart_policy: "always" 251 | 252 | - name: web proxy container 253 | docker_container: 254 | name: web_proxy 255 | image: httpd:2.4 256 | networks: 257 | - name: guacnet 258 | ipv4_address: "{{ guacnet_webproxy }}" 259 | ports: 260 | - "443:443" 261 | volumes: 262 | - /opt/webproxy/httpd-ssl.conf:/usr/local/apache2/conf/extra/httpd-ssl.conf:ro 263 | - /opt/webproxy/httpd.conf:/usr/local/apache2/conf/httpd.conf:ro 264 | - /etc/ssl/certs/ssl-cert-snakeoil.pem:/usr/local/apache2/conf/server.crt:ro 265 | - /etc/ssl/private/ssl-cert-snakeoil.key:/usr/local/apache2/conf/server.key:ro 266 | purge_networks: yes 267 | restart_policy: "always" 268 | restart: "{% if proxy_conf_files.changed %}yes{% else %}no{% endif %}" 269 | when: duckdns_domain == "" 270 | 271 | - name: duckdnsupdater container 272 | docker_container: 273 | name: duckdnsupdater 274 | hostname: duckdnsupdater 275 | image: ghcr.io/linuxserver/duckdns 276 | networks: 277 | - name: guacnet 278 | ipv4_address: "{{ guacnet_duckdnsupdater }}" 279 | env: 280 | PUID: "1000" 281 | PGID: "1000" 282 | TZ: "UTC" 283 | SUBDOMAINS: "{{ duckdns_domain.split('.')[0] }}" 284 | TOKEN: "{{ duckdns_token }}" 285 | pull: yes 286 | purge_networks: yes 287 | restart_policy: "always" 288 | container_default_behavior: "compatibility" 289 | when: duckdns_domain != "" 290 | 291 | - name: duckdns web proxy container 292 | docker_container: 293 | name: web_proxy 294 | hostname: webproxy 295 | image: ghcr.io/linuxserver/swag 296 | networks: 297 | - name: guacnet 298 | ipv4_address: "{{ guacnet_webproxy }}" 299 | env: 300 | PUID: "1000" 301 | PGID: "1000" 302 | TZ: "UTC" 303 | URL: "{{ duckdns_domain }}" 304 | DUCKDNSTOKEN: "{{ duckdns_token }}" 305 | EMAIL: "{{ letsencrypt_email }}" 306 | VALIDATION: "duckdns" 307 | ports: 308 | - "443:443" 309 | volumes: "/opt/webproxy:/config" 310 | pull: yes 311 | purge_networks: yes 312 | restart_policy: "always" 313 | container_default_behavior: "compatibility" 314 | restart: "{% if proxy_conf_files.changed %}yes{% else %}no{% endif %}" 315 | when: duckdns_domain != "" 316 | 317 | - name: Determine if (One Time) was done 318 | stat: 319 | path: /opt/guacamole/db_conf_done 320 | register: guacdb_one_time_done 321 | 322 | - name: Set my.cnf and dbpass.sql 323 | template: 324 | src: "{{ item }}" 325 | dest: "/opt/guacamole/{{ item }}" 326 | owner: root 327 | group: root 328 | mode: '0400' 329 | with_items: 330 | - my.cnf 331 | - dbpass.sql 332 | when: guacdb_one_time_done.stat.exists|bool == False 333 | 334 | - name: Wait for mysqld on 3306 335 | shell: | 336 | docker logs guacdb 2>&1 | grep --quiet 'port: 3306' 337 | register: wait_for_mysqld 338 | until: wait_for_mysqld.rc == 0 339 | retries: 15 340 | delay: 15 341 | when: guacdb_one_time_done.stat.exists|bool == False 342 | 343 | - name: Configure DB (One Time) 344 | shell: | 345 | # credentials 346 | docker cp /opt/guacamole/my.cnf guacdb:/root/.my.cnf 347 | docker cp /opt/guacamole/dbpass.sql guacdb:dbpass.sql 348 | docker exec -i guacdb /bin/bash -c "mysql < dbpass.sql" 349 | touch /opt/guacamole/one_time_done 350 | # schema 351 | docker exec -i guacamole /bin/bash -c 'cat /opt/guacamole/mysql/schema/*.sql' > /opt/guacamole/dbschema.sql 352 | docker cp /opt/guacamole/dbschema.sql guacdb:dbschema.sql 353 | docker exec -i guacdb /bin/bash -c "mysql guacamole_db < dbschema.sql" 354 | when: guacdb_one_time_done.stat.exists|bool == False 355 | 356 | - name: Set One Time 357 | file: 358 | path: /opt/guacamole/db_conf_done 359 | state: touch 360 | 361 | # The following sets the default AMI user (ubuntu)'s password from a random string, 362 | # guacadmin's password to cw_password from AWS Parameter secret 363 | # and creates a default RDP connection for the Ubuntu user in Guacamole 364 | - name: Determine User and Session one-time setup complete 365 | stat: 366 | path: /opt/guacamole/user_session_done 367 | register: usersession_one_time_done 368 | 369 | - name: Get SSM parameter cw_password (One Time) 370 | set_fact: 371 | cw_password: "{{ lookup('aws_ssm', name_prefix + '-cw-web-password-' + name_suffix, decrypt=True, region=aws_region) }}" 372 | when: usersession_one_time_done.stat.exists|bool == False 373 | no_log: True 374 | 375 | - name: Set Ubuntu password (One Time) 376 | user: 377 | name: ubuntu 378 | password: "{{ cw_password | password_hash('sha512') }}" 379 | when: usersession_one_time_done.stat.exists|bool == False 380 | 381 | - name: Get Auth Token from Guacamole with Default Credentials (One Time) 382 | uri: 383 | url: "https://{% if duckdns_domain != '' %}{{ duckdns_domain }}{% else %}127.0.0.1{% endif %}:443/guacamole/api/tokens" 384 | method: POST 385 | body: "username=guacadmin&password=guacadmin" 386 | validate_certs: "{% if duckdns_domain == '' %}no{% else %}yes{% endif %}" 387 | register: GUAC_AUTH_RESPONSE 388 | retries: 60 389 | delay: 1 390 | until: "'authToken' in GUAC_AUTH_RESPONSE.json" 391 | when: usersession_one_time_done.stat.exists|bool == False 392 | 393 | - name: Add RDP Connection to Cloud Workstation (One Time) 394 | uri: 395 | url: https://{% if duckdns_domain != "" %}{{ duckdns_domain }}{% else %}127.0.0.1{% endif %}:443/guacamole/api/session/data/mysql/connections?token={{ GUAC_AUTH_RESPONSE.json.authToken }} 396 | # url: https://{% if duckdns_domain != "" %}{{ duckdns_domain }}{% else %}127.0.0.1{% endif %}:443/guacamole/api/session/data/mysql/connections?token=cheese 397 | method: POST 398 | body_format: json 399 | body: '{"parentIdentifier":"ROOT","name":"cloud_workstation","protocol":"rdp","parameters":{"port":"3389","read-only":"","swap-red-blue":"","cursor":"","color-depth":"","clipboard-encoding":"","disable-copy":"","disable-paste":"","dest-port":"","recording-exclude-output":"","recording-exclude-mouse":"","recording-include-keys":"","create-recording-path":"","enable-sftp":"","sftp-port":"","sftp-server-alive-interval":"","sftp-disable-download":"","sftp-disable-upload":"","enable-audio":"","wol-send-packet":"","wol-wait-time":"","security":"","disable-auth":"","ignore-cert":"true","gateway-port":"","server-layout":"","timezone":null,"console":"","width":"","height":"","dpi":"","resize-method":"","console-audio":"","disable-audio":"","enable-audio-input":"","enable-printing":"","enable-drive":"","disable-download":"","disable-upload":"","create-drive-path":"","enable-wallpaper":"","enable-theming":"","enable-font-smoothing":"","enable-full-window-drag":"","enable-desktop-composition":"","enable-menu-animations":"","disable-bitmap-caching":"","disable-offscreen-caching":"","disable-glyph-caching":"","preconnection-id":"","hostname":"172.17.0.1","username":"ubuntu","password":"{{ cw_password }}"},"attributes":{"max-connections":"5","max-connections-per-user":"5","weight":"","failover-only":"","guacd-port":"4822","guacd-encryption":"","guacd-hostname":"{{ guacnet_guacd }}"}}' 400 | validate_certs: "{% if duckdns_domain == '' %}no{% else %}yes{% endif %}" 401 | register: GUAC_ADD_RDP_RESPONSE 402 | failed_when: "'url' not in GUAC_ADD_RDP_RESPONSE" 403 | when: usersession_one_time_done.stat.exists|bool == False 404 | 405 | - name: Set guacadmin Password to cw_password (One Time) 406 | uri: 407 | url: https://{% if duckdns_domain != "" %}{{ duckdns_domain }}{% else %}127.0.0.1{% endif %}:443/guacamole/api/session/data/mysql/users/guacadmin/password?token={{ GUAC_AUTH_RESPONSE.json.authToken }} 408 | method: PUT 409 | body_format: json 410 | body: '{"oldPassword":"guacadmin","newPassword":"{{ cw_password }}"}' 411 | validate_certs: "{% if duckdns_domain == '' %}no{% else %}yes{% endif %}" 412 | register: GUAC_GUACADMIN_CHANGE_PASS 413 | failed_when: GUAC_GUACADMIN_CHANGE_PASS.status != 204 414 | when: usersession_one_time_done.stat.exists|bool == False 415 | 416 | - name: Set User and Session one-time setup complete 417 | file: 418 | path: /opt/guacamole/user_session_done 419 | state: touch 420 | --------------------------------------------------------------------------------