diff --git a/roles/acme/handlers/main.yaml b/roles/acme/handlers/main.yaml new file mode 100644 index 0000000..d2fbc06 --- /dev/null +++ b/roles/acme/handlers/main.yaml @@ -0,0 +1,5 @@ +- name: update_contact_info + command: dehydrated --account + +- name: query_certificates + command: dehydrated --cron diff --git a/roles/acme/tasks/main.yaml b/roles/acme/tasks/main.yaml new file mode 100644 index 0000000..b812b83 --- /dev/null +++ b/roles/acme/tasks/main.yaml @@ -0,0 +1,80 @@ +--- +- import_tasks: remove_conflicting.yaml + tags: [ never, acme_remove_conflicting ] + +- name: Install Dehydrated + tags: [ acme, acme_install ] + block: + - name: Install dependencies + apt: + name: ssl-cert + state: present + + - name: Install Dehydrated + apt: + name: dehydrated + state: present + default_release: "{{ ansible_distribution_release }}-backports" + + - name: Install config file + template: + src: config.sh + dest: /etc/dehydrated/conf.d/ansible.sh + owner: root + group: root + mode: 0755 + notify: update_contact_info + + - name: Install deploy hook + template: + src: deploy.sh + dest: /etc/dehydrated/conf.d/deploy.sh + owner: root + group: root + mode: 0755 + + - name: Install cronjob + template: + src: cron + dest: /etc/cron.d/dehydrated + owner: root + group: root + mode: 0644 + + - name: Create Nginx snippet snippets dir + file: + state: directory + path: /etc/nginx/snippets + owner: root + group: root + mode: 0755 + + - name: Install Nginx snippet + template: + src: nginx-snippet.conf + dest: /etc/nginx/snippets/acme.conf + owner: root + group: root + mode: 0644 + + - name: Register account + command: dehydrated --register --accept-terms + args: + creates: /var/lib/dehydrated/accounts + +- tags: [ acme, acme_certs ] + block: + - name: Configure certificates + template: + src: domains.txt + dest: /etc/dehydrated/domains.txt + owner: root + group: root + mode: 0644 + notify: query_certificates + + - name: Symlink SAN domains + include_tasks: san_domains_loop.yaml + loop: "{{ acme_san_domains|default([]) }}" + loop_control: + loop_var: domains diff --git a/roles/acme/tasks/remove_conflicting.yaml b/roles/acme/tasks/remove_conflicting.yaml new file mode 100644 index 0000000..2d233b8 --- /dev/null +++ b/roles/acme/tasks/remove_conflicting.yaml @@ -0,0 +1,30 @@ +--- +- name: Remove acmetool from apt + apt: + name: acmetool + state: absent + +- name: Remove files + file: + state: absent + path: "{{ item }}" + with_items: + - /etc/cron.d/acmetool + - /usr/local/bin/acmetool + - /var/lib/acme + +- name: Remove certbot from apt + apt: + name: [ letsencrypt, certbot ] + state: absent + autoremove: yes + +- name: Remove variable directories + file: + state: absent + path: /usr/local/bin/acmetool + with_items: + - /etc/letsencrypt + - /var/letsencrypt + - /var/lib/letsencrypt + - /var/log/letsencrypt diff --git a/roles/acme/tasks/san_domains_loop.yaml b/roles/acme/tasks/san_domains_loop.yaml new file mode 100644 index 0000000..4102cf4 --- /dev/null +++ b/roles/acme/tasks/san_domains_loop.yaml @@ -0,0 +1,11 @@ +--- +- stat: + path: "/var/lib/dehydrated/certs/{{ domains[0] }}" + register: cert_stat + +- file: + state: link + path: "/var/lib/dehydrated/certs/{{ item }}" + src: "/var/lib/dehydrated/certs/{{ domains[0] }}" + loop: "{{ domains[1:] }}" + when: cert_stat.stat.exists == True diff --git a/roles/acme/templates/config.sh b/roles/acme/templates/config.sh new file mode 100644 index 0000000..f51455d --- /dev/null +++ b/roles/acme/templates/config.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +# Managed by Ansible + +CONTACT_EMAIL={{ notify_email }} diff --git a/roles/acme/templates/cron b/roles/acme/templates/cron new file mode 100644 index 0000000..137d1eb --- /dev/null +++ b/roles/acme/templates/cron @@ -0,0 +1,6 @@ +# Managed by Ansible + +SHELL=/bin/sh +PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin + +49 6 * * * root /usr/bin/dehydrated --cron diff --git a/roles/acme/templates/deploy.sh b/roles/acme/templates/deploy.sh new file mode 100644 index 0000000..aa334d4 --- /dev/null +++ b/roles/acme/templates/deploy.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Managed by Ansible + +deploy_cert() { + local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}" + + systemctl reload nginx.service +} diff --git a/roles/acme/templates/domains.txt b/roles/acme/templates/domains.txt new file mode 100644 index 0000000..632b12b --- /dev/null +++ b/roles/acme/templates/domains.txt @@ -0,0 +1,9 @@ +# Managed by Ansible + +{% for domain in acme_domains|default([]) %} +{{ domain }} +{% endfor %} + +{% for domains in acme_san_domains|default([]) %} +{{ domains | join(' ') }} +{% endfor %} diff --git a/roles/acme/templates/nginx-snippet.conf b/roles/acme/templates/nginx-snippet.conf new file mode 100644 index 0000000..4d988bd --- /dev/null +++ b/roles/acme/templates/nginx-snippet.conf @@ -0,0 +1,6 @@ +# Managed by Ansible + +location /.well-known/acme-challenge { + allow all; + alias /var/lib/dehydrated/acme-challenges; +} diff --git a/roles/common/defaults/main.yaml b/roles/common/defaults/main.yaml new file mode 100644 index 0000000..9708849 --- /dev/null +++ b/roles/common/defaults/main.yaml @@ -0,0 +1,13 @@ +ssh_port: "22" +unattended_upgrades_auto_reboot_time: "04:00" +trusted_ranges: + - { v: ipv4, cidr: 127.0.0.1 } + - { v: ipv4, cidr: 10.0.0.0/8 } + - { v: ipv4, cidr: 192.168.0.0/16 } + - { v: ipv6, cidr: "::1" } + - { v: ipv6, cidr: "fe80::/10" } + +# All off: no touch config +network_br: off +network_dhcp: off +network_static: off diff --git a/roles/common/handlers/main.yaml b/roles/common/handlers/main.yaml new file mode 100644 index 0000000..48b73af --- /dev/null +++ b/roles/common/handlers/main.yaml @@ -0,0 +1,30 @@ +--- +- name: update grub + command: update-grub + +- name: reboot + reboot: + +- name: apt update + apt: + update_cache: true + +- name: daemon reload + systemd: + daemon_reload: true + +- name: reload sshd + systemd: + name: ssh + state: reloaded + +- name: reload nginx + systemd: + name: nginx + state: reloaded + +- name: persist iptables + shell: "{{ item.c }}-save > /etc/iptables/rules.{{ item.ip }}" + with_items: + - { c: iptables, ip: v4 } + - { c: ip6tables, ip: v6 } diff --git a/roles/common/tasks/debian-backports.yaml b/roles/common/tasks/debian-backports.yaml new file mode 100644 index 0000000..f9119a2 --- /dev/null +++ b/roles/common/tasks/debian-backports.yaml @@ -0,0 +1,18 @@ +--- +- name: Install backports source list + template: + src: backports-source.list + dest: /etc/apt/sources.list.d/backports.list + owner: root + group: root + mode: 0644 + notify: apt update + +- meta: flush_handlers + +- name: Install backports kernel + apt: + name: linux-image-amd64 + state: latest + default_release: "{{ ansible_facts['distribution_release'] }}-backports" + when: ansible_facts['architecture'] == "x86_64" diff --git a/roles/common/tasks/main.yaml b/roles/common/tasks/main.yaml new file mode 100644 index 0000000..14c461f --- /dev/null +++ b/roles/common/tasks/main.yaml @@ -0,0 +1,116 @@ +--- +- tags: debian_backports + import_tasks: debian-backports.yaml + +- tags: unattended_updates + import_tasks: unattended-updates.yaml + +- tags: network + import_tasks: network.yaml + +- tags: node-exporter + import_tasks: node-exporter.yaml + +- name: Install utilities + apt: + name: + - curl + - fzf + - git + - htop + - iptables + - iptables-persistent + - jq + - net-tools + - ripgrep + - rsync + - tree + - vim + +- name: Configure FZF for Bash + lineinfile: + path: /etc/bash.bashrc + insertafter: EOF + regexp: "^source /usr/share/doc/fzf/{{ item }}" + line: "source /usr/share/doc/fzf/examples/{{ item }} # Managed by Ansible" + with_items: + - key-bindings.bash + - completion.bash + +- name: Shorten Grub timeout + lineinfile: + path: /etc/default/grub + regexp: '^GRUB_TIMEOUT=' + line: "GRUB_TIMEOUT=1 # Managed by Ansible" + notify: update grub + +- name: Configure cron email + lineinfile: + path: /etc/crontab + insertafter: '^PATH' + line: 'MAILTO={{ notify_email }}' + +- name: Turn off SSH password auth + lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?PasswordAuthentication' + line: 'PasswordAuthentication no' + notify: reload sshd + +- name: Configure SSH port + lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?Port' + line: 'Port {{ ssh_port }}' + notify: reload sshd + +- name: Allow SSH + iptables: + chain: INPUT + protocol: tcp + destination_port: "{{ ssh_port }}" + ctstate: NEW + jump: ACCEPT + ip_version: "{{ item }}" + with_items: + - ipv4 + - ipv6 + notify: persist iptables + +- name: Allow IPv6 ICMP + iptables: + chain: INPUT + protocol: ipv6-icmp + jump: ACCEPT + ip_version: ipv6 + notify: persist iptables + +- name: Allow related and established connections + iptables: + chain: INPUT + ctstate: ESTABLISHED,RELATED + jump: ACCEPT + ip_version: "{{ item }}" + with_items: + - ipv4 + - ipv6 + notify: persist iptables + +- name: Allow local connections + iptables: + chain: INPUT + source: "{{ item.cidr }}" + jump: ACCEPT + ip_version: "{{ item.v }}" + with_items: "{{ trusted_ranges }}" + notify: persist iptables + +- name: Deny inbound connections + iptables: + chain: INPUT + policy: DROP + ip_version: "{{ item }}" + with_items: + - ipv4 + - ipv6 + notify: persist iptables diff --git a/roles/common/tasks/network.yaml b/roles/common/tasks/network.yaml new file mode 100644 index 0000000..29bf9c2 --- /dev/null +++ b/roles/common/tasks/network.yaml @@ -0,0 +1,42 @@ +--- +- name: Install bridge-utils + apt: + name: bridge-utils + state: present + when: network_br + +- lineinfile: + path: /etc/sysctl.conf + regexp: ^#?net.ipv4.ip_forward + line: "net.ipv4.ip_forward=1 # Managed by Ansible" + notify: reboot + when: network_br + +- lineinfile: + path: /etc/sysctl.conf + regexp: ^#?net.ipv6.conf.all.forwarding + line: "net.ipv6.conf.all.forwarding=1 # Managed by Ansible" + notify: reboot + when: network_br + +- name: Make network interfaces really predictable + lineinfile: + path: /etc/default/grub + regexp: ^GRUB_CMDLINE_LINUX + line: 'GRUB_CMDLINE_LINUX="net.ifnames=0 biosdevname=0" # Managed by Ansible' + notify: + - update grub + - reboot + when: network_br or network_dhcp or network_static + +- name: Configure network interfaces + template: + src: network-interfaces + dest: /etc/network/interfaces + owner: root + group: root + mode: 0644 + notify: reboot + when: network_br or network_dhcp or network_static + +- meta: flush_handlers diff --git a/roles/common/tasks/node-exporter.yaml b/roles/common/tasks/node-exporter.yaml new file mode 100644 index 0000000..e5c5c72 --- /dev/null +++ b/roles/common/tasks/node-exporter.yaml @@ -0,0 +1,5 @@ +--- +- name: Install node-exporter + apt: + name: prometheus-node-exporter + state: present diff --git a/roles/common/tasks/unattended-updates.yaml b/roles/common/tasks/unattended-updates.yaml new file mode 100644 index 0000000..ea09772 --- /dev/null +++ b/roles/common/tasks/unattended-updates.yaml @@ -0,0 +1,23 @@ +--- +- name: Install unattended-upgrades + apt: + name: + - unattended-upgrades + - apt-listchanges + state: present + +- name: Configure auto-upgrades + template: + src: auto-upgrades + dest: /etc/apt/apt.conf.d/20auto-upgrades + owner: root + group: root + mode: 0644 + +- name: Configure unattended-upgrades + template: + src: unattended-upgrades + dest: /etc/apt/apt.conf.d/50unattended-upgrades + owner: root + group: root + mode: 0644 diff --git a/roles/common/templates/auto-upgrades b/roles/common/templates/auto-upgrades new file mode 100644 index 0000000..e9fbe1b --- /dev/null +++ b/roles/common/templates/auto-upgrades @@ -0,0 +1,4 @@ +# Managed by Ansible + +APT::Periodic::Update-Package-Lists "1"; +APT::Periodic::Unattended-Upgrade "1"; diff --git a/roles/common/templates/backports-source.list b/roles/common/templates/backports-source.list new file mode 100644 index 0000000..dd30928 --- /dev/null +++ b/roles/common/templates/backports-source.list @@ -0,0 +1,4 @@ +# Managed by Ansible + +deb http://ftp.nl.debian.org/debian/ {{ ansible_facts.distribution_release }}-backports main +deb-src http://ftp.nl.debian.org/debian/ {{ ansible_facts.distribution_release }}-backports main diff --git a/roles/common/templates/network-interfaces b/roles/common/templates/network-interfaces new file mode 100644 index 0000000..b90590e --- /dev/null +++ b/roles/common/templates/network-interfaces @@ -0,0 +1,38 @@ +# Managed by Ansible + +# This file describes the network interfaces available on your system +# and how to activate them. For more information, see interfaces(5). + +source /etc/network/interfaces.d/* + +# The loopback network interface +auto lo +iface lo inet loopback + +# The primary network interface +auto eth0 +allow-hotplug eth0 + +{% if network_br %} +iface eth0 inet manual + +auto br0 +iface br0 inet static + address {{ network_static.address_v4 }} + gateway {{ network_static.gateway_v4 }} + bridge_ports eth0 + +iface br0 inet6 auto + up echo -n 0 > /sys/devices/virtual/net/br0/bridge/multicast_snooping + up ip -6 addr add {{ network_static.address_v6 }} dev br0 + up ip -6 route add default via {{ network_static.gateway_v6 }} dev br0 + bridge_stp on + +{% elif network_static %} +iface eth0 inet static + address {{ network_static.address_v4 }} + gateway {{ network_static.gateway_v4 }} + +{% else %} +iface eth0 inet dhcp +{% endif %} diff --git a/roles/common/templates/unattended-upgrades b/roles/common/templates/unattended-upgrades new file mode 100644 index 0000000..d994b78 --- /dev/null +++ b/roles/common/templates/unattended-upgrades @@ -0,0 +1,170 @@ +// Managed by Ansible + +// Unattended-Upgrade::Origins-Pattern controls which packages are +// upgraded. +// +// Lines below have the format "keyword=value,...". A +// package will be upgraded only if the values in its metadata match +// all the supplied keywords in a line. (In other words, omitted +// keywords are wild cards.) The keywords originate from the Release +// file, but several aliases are accepted. The accepted keywords are: +// a,archive,suite (eg, "stable") +// c,component (eg, "main", "contrib", "non-free") +// l,label (eg, "Debian", "Debian-Security") +// o,origin (eg, "Debian", "Unofficial Multimedia Packages") +// n,codename (eg, "jessie", "jessie-updates") +// site (eg, "http.debian.net") +// The available values on the system are printed by the command +// "apt-cache policy", and can be debugged by running +// "unattended-upgrades -d" and looking at the log file. +// +// Within lines unattended-upgrades allows 2 macros whose values are +// derived from /etc/debian_version: +// ${distro_id} Installed origin. +// ${distro_codename} Installed codename (eg, "buster") +Unattended-Upgrade::Origins-Pattern { + // Codename based matching: + // This will follow the migration of a release through different + // archives (e.g. from testing to stable and later oldstable). + // Software will be the latest available for the named release, + // but the Debian release itself will not be automatically upgraded. +// "origin=Debian,codename=${distro_codename}-updates"; +// "origin=Debian,codename=${distro_codename}-proposed-updates"; + "origin=Debian,codename=${distro_codename},label=Debian"; + "origin=Debian,codename=${distro_codename},label=Debian-Security"; + "origin=Debian,codename=${distro_codename}-security,label=Debian-Security"; + + // Archive or Suite based matching: + // Note that this will silently match a different release after + // migration to the specified archive (e.g. testing becomes the + // new stable). +// "o=Debian,a=stable"; +// "o=Debian,a=stable-updates"; +// "o=Debian,a=proposed-updates"; +// "o=Debian Backports,a=${distro_codename}-backports,l=Debian Backports"; +}; + +// Python regular expressions, matching packages to exclude from upgrading +Unattended-Upgrade::Package-Blacklist { + // The following matches all packages starting with linux- +// "linux-"; + + // Use $ to explicitely define the end of a package name. Without + // the $, "libc6" would match all of them. +// "libc6$"; +// "libc6-dev$"; +// "libc6-i686$"; + + // Special characters need escaping +// "libstdc\+\+6$"; + + // The following matches packages like xen-system-amd64, xen-utils-4.1, + // xenstore-utils and libxenstore3.0 +// "(lib)?xen(store)?"; + + // For more information about Python regular expressions, see + // https://docs.python.org/3/howto/regex.html +}; + +// This option allows you to control if on a unclean dpkg exit +// unattended-upgrades will automatically run +// dpkg --force-confold --configure -a +// The default is true, to ensure updates keep getting installed +Unattended-Upgrade::AutoFixInterruptedDpkg "true"; + +// Split the upgrade into the smallest possible chunks so that +// they can be interrupted with SIGTERM. This makes the upgrade +// a bit slower but it has the benefit that shutdown while a upgrade +// is running is possible (with a small delay) +//Unattended-Upgrade::MinimalSteps "true"; + +// Install all updates when the machine is shutting down +// instead of doing it in the background while the machine is running. +// This will (obviously) make shutdown slower. +// Unattended-upgrades increases logind's InhibitDelayMaxSec to 30s. +// This allows more time for unattended-upgrades to shut down gracefully +// or even install a few packages in InstallOnShutdown mode, but is still a +// big step back from the 30 minutes allowed for InstallOnShutdown previously. +// Users enabling InstallOnShutdown mode are advised to increase +// InhibitDelayMaxSec even further, possibly to 30 minutes. +//Unattended-Upgrade::InstallOnShutdown "false"; + +// Send email to this address for problems or packages upgrades +// If empty or unset then no email is sent, make sure that you +// have a working mail setup on your system. A package that provides +// 'mailx' must be installed. E.g. "user@example.com" +Unattended-Upgrade::Mail "{{ notify_email }}"; + +// Set this value to one of: +// "always", "only-on-error" or "on-change" +// If this is not set, then any legacy MailOnlyOnError (boolean) value +// is used to chose between "only-on-error" and "on-change" +Unattended-Upgrade::MailReport "only-on-error"; + +// Remove unused automatically installed kernel-related packages +// (kernel images, kernel headers and kernel version locked tools). +Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; + +// Do automatic removal of newly unused dependencies after the upgrade +Unattended-Upgrade::Remove-New-Unused-Dependencies "true"; + +// Do automatic removal of unused packages after the upgrade +// (equivalent to apt-get autoremove) +Unattended-Upgrade::Remove-Unused-Dependencies "true"; + +{% if unattended_upgrades_auto_reboot_time %} +// Automatically reboot *WITHOUT CONFIRMATION* if +// the file /var/run/reboot-required is found after the upgrade +Unattended-Upgrade::Automatic-Reboot "true"; +{% endif %} + +// Automatically reboot even if there are users currently logged in +// when Unattended-Upgrade::Automatic-Reboot is set to true +Unattended-Upgrade::Automatic-Reboot-WithUsers "true"; + +{% if unattended_upgrades_auto_reboot_time %} +// If automatic reboot is enabled and needed, reboot at the specific +// time instead of immediately +// Default: "now" +Unattended-Upgrade::Automatic-Reboot-Time "{{ unattended_upgrades_auto_reboot_time }}"; +{% endif %} + +// Use apt bandwidth limit feature, this example limits the download +// speed to 70kb/sec +//Acquire::http::Dl-Limit "70"; + +// Enable logging to syslog. Default is False +// Unattended-Upgrade::SyslogEnable "false"; + +// Specify syslog facility. Default is daemon +// Unattended-Upgrade::SyslogFacility "daemon"; + +// Download and install upgrades only on AC power +// (i.e. skip or gracefully stop updates on battery) +// Unattended-Upgrade::OnlyOnACPower "true"; + +// Download and install upgrades only on non-metered connection +// (i.e. skip or gracefully stop updates on a metered connection) +// Unattended-Upgrade::Skip-Updates-On-Metered-Connections "true"; + +// Verbose logging +// Unattended-Upgrade::Verbose "false"; + +// Print debugging information both in unattended-upgrades and +// in unattended-upgrade-shutdown +// Unattended-Upgrade::Debug "false"; + +// Allow package downgrade if Pin-Priority exceeds 1000 +// Unattended-Upgrade::Allow-downgrade "false"; + +// When APT fails to mark a package to be upgraded or installed try adjusting +// candidates of related packages to help APT's resolver in finding a solution +// where the package can be upgraded or installed. +// This is a workaround until APT's resolver is fixed to always find a +// solution if it exists. (See Debian bug #711128.) +// The fallback is enabled by default, except on Debian's sid release because +// uninstallable packages are frequent there. +// Disabling the fallback speeds up unattended-upgrades when there are +// uninstallable packages at the expense of rarely keeping back packages which +// could be upgraded or installed. +// Unattended-Upgrade::Allow-APT-Mark-Fallback "true"; diff --git a/roles/go/defaults/main.yaml b/roles/go/defaults/main.yaml new file mode 100644 index 0000000..e60db63 --- /dev/null +++ b/roles/go/defaults/main.yaml @@ -0,0 +1 @@ +go_arch: amd64 diff --git a/roles/go/tasks/main.yaml b/roles/go/tasks/main.yaml new file mode 100644 index 0000000..f027632 --- /dev/null +++ b/roles/go/tasks/main.yaml @@ -0,0 +1,64 @@ +--- +- name: go + tags: go,go_install + block: + - name: Remove Debian Go package + apt: + name: golang + autoremove: yes + state: absent + + - name: Install dependencies + apt: + name: curl + state: present + + - name: Fetch Go latest version + shell: "curl --silent --location https://go.dev/doc/devel/release | grep -Eo 'go[0-9]+(\\.[0-9]+)+' | sort -V | uniq | tail -1 | sed s/^go//" + args: + warn: false + changed_when: false + register: go_latest_version_shell + + - name: Format Go latest version variable + set_fact: + go_latest_version: "{{ go_latest_version_shell.stdout }}" + + - name: Detect installed Go version + shell: "go version | grep -Po '\\d\\.\\d+(\\.\\d+)?' || echo none" + register: go_installed_version_shell + ignore_errors: true + changed_when: false + + - name: Format Go version variable + set_fact: + go_installed_version: "{{ go_installed_version_shell.stdout }}" + + - name: Remove installed go + file: + state: absent + path: /usr/local/go + when: go_installed_version != go_latest_version + + - name: Install Go + unarchive: + src: https://go.dev/dl/go{{ go_latest_version }}.linux-{{ go_arch }}.tar.gz + dest: /usr/local + remote_src: yes + owner: root + group: root + when: go_installed_version != go_latest_version + + - name: Configure Go environment + template: + src: go.profile + dest: /etc/profile.d/go.sh + owner: root + group: root + mode: 0644 + + - name: Link go binary + file: + state: link + src: /usr/local/go/bin/go + dest: /usr/local/bin/go diff --git a/roles/go/templates/go.profile b/roles/go/templates/go.profile new file mode 100644 index 0000000..64ade17 --- /dev/null +++ b/roles/go/templates/go.profile @@ -0,0 +1,5 @@ +# Managed by Ansible + +export GOROOT=/usr/local/go +export GOPATH=$HOME/go +export PATH=$PATH:$GOROOT/bin:$GOPATH/bin