diff --git a/Pulumi.buffed2929.yaml b/Pulumi.buffed2929.yaml new file mode 100644 index 0000000..f12608a --- /dev/null +++ b/Pulumi.buffed2929.yaml @@ -0,0 +1,30 @@ +config: + oci:region: eu-zurich-1 + oci:privateKeyPath: pulumi@pomail.fr_2024-03-05T14 33 04.969Z.pem + oci:fingerprint: + secure: AAABABAkZejiW7iEDzCS7EETpb0Cx9AbnpQ7V7ylq8xAIhCWjC0AGl1QyMBTVAGfvhaYugc/Ma48OrqM6udCb43z4/h/W4gm6p9Djg+leA== + oci:tenancyOcid: + secure: AAABAM/qyWPGpopb2T+6jEzaJAa6gvXrVT1XSKh9gn/Pt28tZimyPcR+jRmfBQSFOX/LSfp6TodpP28pgGUUDmnfbvv4bIYFCDMGEhmSb7DmlsedWBy5KZ370GmuHCcTBlYNbFT/rSTEWGIsvU2s + oci:userOcid: + secure: AAABAMHJ6fJebI9aL+4qESzJjU1D8FCAiBckvKfrK+iPCYQJn6qBcRc+Uwz+tsmtssAFnO2/fCtMSpigHYwYc+fbdq6FVj/KSdIlxWkT9HHEW3e2xw7QGhzWz4ZXiYlpj9SHQRmt7mjjOutP + infra-happyDomain:restic_aws_access_key_id: + secure: AAABABCLVz3G4b9//Xa/XBvyMzs9bjGEwnyPOBN7/srMcTeXItWq5+mN/j2upZXh4ogXMP1A38P4pA== + infra-happyDomain:restic_aws_secret_access_key: + secure: AAABAE1v6iqMWEJQ+S5neb2bGldrm1pkePEr1AlIoHFc5Vgi5aASgjdONzbAGNSZK0mT5kRQjTEK0P1I/zQenG3++IHk3zZQPMmZPE+TBaOSo38qEr0bRf4b1yyjWf9C + infra-happyDomain:listmonk_api_username: + secure: AAABAA1C9Rrzc/qN7+J95uRqmSmFGhqB7CJfLdMmhLe2qLKukDlPltg= + infra-happyDomain:listmonk_api_password: + secure: AAABADILUmQsC5STCrsZAa1iTpUZdODtaiMVfS3fTEg+oDAYjAMCZ7D1Qg3K9P8hi6nZ+3Qec8H4V6IcbV+QKQ== + infra-happyDomain:happydomain_ovh_application_secret: + secure: AAABAMByLL376Sd3SezK6+jpHBTBRhMjH00fNgNDyUudQA0blywYe5J7D4wkrgkAfHoSK/bW+p2Wa3sIK1nhBw== + infra-happyDomain:happydomain_ovh_application_key: + secure: AAABAKttTSDZWY1ybyA83+OtsD1TB9t5YLFuN2pL7pECpzfk1bcMVlO5g9EvFDx9 + infra-happyDomain:restic_password: + secure: AAABAGZDQtnRzrocx5zPobqdp2Xp7ABObbGM9+zjRXAOXmX8ioVzSSlQ3pKxqnwsrsquIRSIrUhKmuvfIhWd + infra-happyDomain:happydomain_smtp_user: + secure: AAABACdg/2/ZsMv9/blK4NhYulbdwq/sY32mdIMGqcI55NEUlVTtMZfnKgKbbE0xYVzH2O3euZiJNBvtm0qsmrxi3Wf17UE3PtUZJPZEGkKaaFTh8GF+jcQsaDbkexdc2cNBQFUV6UVkeNJZArtvrnoveLX7HRBFQM+VU5lOQJMAX4Fv1U+UrhBEZS5NES5VVrfDeJ9MXsc49tAwPOC+/6/vjMqNNLhhBXCsSqZbWjRWKw+9Mt2Vo4iOpm8Y6KNO7/BC + infra-happyDomain:happydomain_smtp_password: + secure: AAABAPY64j6ll6m41Rt7ykJ3Mbwy4P++AFl+m7TukGMurQPBHu5HR5M2BxdlWB1SMjGB5w== + infra-happyDomain:region_origin: uk-london-1 + infra-happyDomain:happydomain_beta_jwt_secret_key: + secure: AAABAFA8Cy/ll/KT+y4wRRM3QK2n4q7K00xW3rEobfWPyi45cgTOYtvr2Qu82lhODJFrrq5hAu5ujDjWkW5j8mrkaNu6x6eA+x8IulZc78Owbpdfeith6ysNLe2U/5CJ diff --git a/cloud-init-beta.yaml b/cloud-init-beta.yaml new file mode 100644 index 0000000..151d1c6 --- /dev/null +++ b/cloud-init-beta.yaml @@ -0,0 +1,290 @@ +#cloud-config +users: + - default +package_update: true + +packages: + - ca-certificates + - caddy + - cron + - docker.io + - docker-compose-v2 + - jq + - restic + - syslog-ng + +write_files: + - content: | + { + "ip6tables": true, + "ipv6": true, + "fixed-cidr-v6": "fd00:dead:beef::/64", + "userland-proxy": false + } + path: /etc/docker/daemon.json + - content: | + *filter + :INPUT DROP [0:0] + -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT + -A INPUT -p icmpv6 -j ACCEPT + -A INPUT -i lo -j ACCEPT + -A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT + -A INPUT -p tcp -m state --state NEW -m tcp --dport 80 -j ACCEPT + -A INPUT -p tcp -m state --state NEW -m tcp --dport 443 -j ACCEPT + -A INPUT -j REJECT --reject-with icmp-host-prohibited + :FORWARD ACCEPT [0:0] + -A FORWARD -j REJECT --reject-with icmp-host-prohibited + :OUTPUT ACCEPT [0:0] + COMMIT + path: /etc/iptables/rules.v6 + - content: | + { + #acme_ca https://acme-staging-v02.api.letsencrypt.org/directory + } + + beta.happydomain.org { + reverse_proxy 127.0.0.1:8081 { + flush_interval -1 + } + } + + betadm.happydomain.org { + basicauth { + nemunaire "$2a$14$niofv0BFHIB4mtpKkhWUy.L8iRDeSdnKsIcSPMk/QNNMPEoFZ94ee" + frederic "$2a$14$niofv0BFHIB4mtpKkhWUy.L8iRDeSdnKsIcSPMk/QNNMPEoFZ94ee" + } + reverse_proxy 127.0.0.1:8082 { + flush_interval -1 + } + } + path: /etc/caddy/Caddyfile + - content: | + @version:3.30 + @include "scl.conf" + + # syslog-ng configuration file. + # + # See syslog-ng(8) and syslog-ng.conf(5) for more information. + + options { + create_dirs(yes); + mark_freq(3600); + stats_freq(43200); + time_reopen(5); + use_dns(no); + dns-cache(no); + owner(root); + group(adm); + perm(0640); + dir_perm(0755); + }; + + source src { system(); internal(); }; + + filter f_auth { facility(auth, authpriv); }; + filter f_emergency { level(emerg); }; + + destination authlog { file("/var/log/auth.log"); }; + destination emergency { file("/var/log/emergency"); }; + + log { source(src); filter(f_auth); destination(authlog); }; + log { source(src); filter(f_emergency); destination(emergency); }; + + # Remote loghost + destination loghost1 { tcp6("geb.ra.nemunai.re"); }; + log { source(src); destination(loghost1); }; + destination loghost2 { tcp6("jizah.masr.nemunai.re"); }; + log { source(src); destination(loghost2); }; + + # Source additional configuration files (.conf extension only) + @include "/etc/syslog-ng/conf.d/*.conf" + path: /etc/syslog-ng/syslog-ng.conf + - content: | + #!/bin/sh + export AWS_ACCESS_KEY_ID=$(cloud-init query ds.metadata.RESTIC_AWS_ACCESS_KEY_ID) + export AWS_SECRET_ACCESS_KEY=$(cloud-init query ds.metadata.RESTIC_AWS_SECRET_ACCESS_KEY) + + export RESTIC_REPOSITORY=$(cloud-init query ds.metadata.RESTIC_REPOSITORY) + export RESTIC_PASSWORD=$(cloud-init query ds.metadata.RESTIC_PASSWORD) + export RESTIC_COMPRESSION=max + + mkdir -p /var/backups/happydomain-beta + + docker exec -i app-happydomain hadmin /api/backup.json -X POST > /var/backups/happydomain-beta/db.json + + restic snapshots > /dev/null 2>&1 || restic init + restic backup /var/backups/happydomain-beta + path: /etc/cron.daily/backup_happydomain + permissions: 0o755 + - content: | + # Static (non-secret) configuration for the happydomain container. + # Secrets injected through cloud-init metadata are written to + # /root/secrets.env by launch_container_app.sh. + HAPPYDOMAIN_BIND=0.0.0.0:8081 + HAPPYDOMAIN_ADMIN_BIND=0.0.0.0:8082 + HAPPYDOMAIN_DEFAULT_NS=172.28.0.53:53 + HAPPYDOMAIN_STORAGE_ENGINE=leveldb + + HAPPYDOMAIN_CAPTCHA_PROVIDER=altcha + HAPPYDOMAIN_DISABLE_REGISTRATION=true + HAPPYDOMAIN_OPT_OUT_INSIGHTS=true + HAPPYDOMAIN_EXTERNALURL=https://beta.happydomain.org + + HAPPYDOMAIN_MAIL_FROM=Fred from happyDomain (beta) + path: /root/beta.env + - content: | + services: + unbound: + image: alpinelinux/unbound + restart: unless-stopped + + configs: + - source: unbound_conf + target: /etc/unbound/unbound.conf + uid: "100" + gid: "101" + + networks: + default: + ipv4_address: 172.28.0.53 + + happydomain: + image: nemunaire/happydomain:${HAPPYDOMAIN_VERSION:-latest} + container_name: app-happydomain + pull_policy: always + ports: + - "127.0.0.1:8081:8081" + - "127.0.0.1:8082:8082" + environment: + # Override some checkers options: use local checkers instead of remote public ones + HAPPYDOMAIN_CHECKER_MATRIXIM_FEDERATIONTESTERSERVER: "http://matrixfederationtester:8080/api/report?server_name=%s" + HAPPYDOMAIN_CHECKER_ZONEMASTER_ZONEMASTERAPIURL: "http://zonemaster:5000" + HAPPYDOMAIN_CHECKER_DNSVIZ_ENDPOINT: "http://dnsviz:8080" + + env_file: + - beta.env + - secrets.env + + dns: + - 172.28.0.53 + networks: + - default + + restart: unless-stopped + + volumes: + - storage:/data:rw + + logging: + driver: syslog + options: + syslog-address: "unixgram:///dev/log" + syslog-facility: daemon + tag: app-happydomain + + dnsviz: + image: happydomain/checker-dnsviz + restart: unless-stopped + + matrixfederationtester: + image: matrixdotorg/federation-tester-backend + environment: + BIND_ADDRESS: "0.0.0.0:8080" + restart: unless-stopped + + zonemaster: + image: zonemaster/backend + command: full + restart: unless-stopped + + configs: + unbound_conf: + content: | + server: + verbosity: 1 + interface: 0.0.0.0 + port: 53 + do-ip4: yes + do-ip6: no + do-udp: yes + do-tcp: yes + + access-control: 127.0.0.0/8 allow + access-control: 172.28.0.0/24 allow + + # Short cache for a testing resolver + cache-max-ttl: 60 + + # Buffers: let the system decide + so-sndbuf: 0 + so-rcvbuf: 0 + + # Trust anchor (static, ships with the image) + trust-anchor-file: "/etc/unbound/root.key" + + volumes: + storage: + + networks: + default: + enable_ipv6: true + ipam: + config: + - subnet: 172.28.0.0/24 + path: /root/docker-compose.yml + - content: | + #!/bin/sh + + [ -z "${HAPPYDOMAIN_VERSION}" ] && export HAPPYDOMAIN_VERSION=$(cloud-init query ds.metadata.HAPPYDOMAIN_VERSION) + + # Secrets injected through cloud-init metadata; the static, non-secret + # configuration lives in /root/beta.env. + cat > /root/secrets.env <> /etc/fstab + - swapon -a + + # Allow traffic in IPv4 + - sed -i '/-A INPUT -j REJECT/i-A INPUT -p tcp -m state --state NEW -m tcp --dport 80 -j ACCEPT\n-A INPUT -p tcp -m state --state NEW -m tcp --dport 443 -j ACCEPT' /etc/iptables/rules.v4 + - iptables -I INPUT 5 -p tcp -m state --state NEW -m tcp --dport 443 -j ACCEPT + - iptables -I INPUT 5 -p tcp -m state --state NEW -m tcp --dport 80 -j ACCEPT + - ip6tables -I INPUT 5 -p tcp -m state --state NEW -m tcp --dport 443 -j ACCEPT + - ip6tables -I INPUT 5 -p tcp -m state --state NEW -m tcp --dport 80 -j ACCEPT + + # Retrieve last backup (the repo may not exist yet on first boot) + - export AWS_ACCESS_KEY_ID=$(cloud-init query ds.metadata.RESTIC_AWS_ACCESS_KEY_ID) + - export AWS_SECRET_ACCESS_KEY=$(cloud-init query ds.metadata.RESTIC_AWS_SECRET_ACCESS_KEY) + - export RESTIC_PASSWORD=$(cloud-init query ds.metadata.RESTIC_PASSWORD) + - export RESTIC_REPOSITORY=$(cloud-init query ds.metadata.RESTIC_REPOSITORY) + - mkdir -p /var/backups/happydomain-beta + - restic snapshots > /dev/null 2>&1 || restic init + - restic restore latest --target / --include /var/backups/happydomain-beta || true + + # Launch web server (uses /etc/caddy/Caddyfile) + - systemctl enable --now caddy + - systemctl restart caddy + + # Launch container + - /root/launch_container_app.sh + + # Restore happydomain backup + - | + [ -f /var/backups/happydomain-beta/db.json ] && docker exec -i app-happydomain hadmin /api/backup.json -X PUT -d @- < /var/backups/happydomain-beta/db.json diff --git a/host_beta.go b/host_beta.go new file mode 100644 index 0000000..0ce0952 --- /dev/null +++ b/host_beta.go @@ -0,0 +1,93 @@ +package main + +import ( + "encoding/base64" + "io/ioutil" + + "github.com/pulumi/pulumi-oci/sdk/go/oci/core" + "github.com/pulumi/pulumi-oci/sdk/go/oci/identity" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config" +) + +// setupHostBeta provisions the beta (near-pre-production) instance. Unlike the +// main host it is not placed behind the load balancer: beta.happydomain.org +// points directly at this instance's public IP. It runs a more recent, +// manually-pinned happyDomain version with real users, hence its own restic +// backup repository. +func setupHostBeta(ctx *pulumi.Context, ocicfg *config.Config, compartment *identity.Compartment, subnet *core.Subnet) error { + cfg := config.New(ctx, "") + + // Get boot image + imageId := compartment.CompartmentId.ApplyT(func(id string) string { + images, _ := core.GetImages(ctx, &core.GetImagesArgs{ + CompartmentId: id, + OperatingSystem: pulumi.StringRef("Canonical Ubuntu"), + OperatingSystemVersion: pulumi.StringRef("24.04 Minimal"), + SortBy: pulumi.StringRef("TIMECREATED"), + SortOrder: pulumi.StringRef("DESC"), + Shape: pulumi.StringRef(SHAPE_AMD64), + }) + return images.Images[0].Id + }).(pulumi.StringOutput) + + // Get availability domains + availabilityDomainName := compartment.CompartmentId.ApplyT(func(id string) string { + availabilityDomains, _ := identity.GetAvailabilityDomains(ctx, &identity.GetAvailabilityDomainsArgs{ + CompartmentId: id, + }) + return availabilityDomains.AvailabilityDomains[0].Name + }).(pulumi.StringOutput) + + // Load cloudinit + userData, err := ioutil.ReadFile("cloud-init-beta.yaml") + if err != nil { + return err + } + + // Create an OCI instance + instance, err := core.NewInstance(ctx, "happydomain-beta-1", &core.InstanceArgs{ + AvailabilityDomain: availabilityDomainName, + CompartmentId: compartment.ID(), + DisplayName: pulumi.Sprintf("%s-happydomain-beta", ctx.Stack()), + Shape: pulumi.String(SHAPE_AMD64), + SourceDetails: &core.InstanceSourceDetailsArgs{ + SourceId: imageId, + SourceType: pulumi.String("image"), + }, + CreateVnicDetails: &core.InstanceCreateVnicDetailsArgs{ + AssignIpv6ip: pulumi.Bool(true), + SubnetId: subnet.ID(), + DisplayName: pulumi.Sprintf("%s-happydomain-beta", ctx.Stack()), + }, + ExtendedMetadata: pulumi.Map{ + "EMAIL_SMTP_HOST": pulumi.String("smtp.email." + cfg.Require("region_origin") + ".oci.oraclecloud.com"), + "EMAIL_SMTP_PORT": pulumi.String("587"), + "EMAIL_SMTP_USERNAME": cfg.RequireSecret("happydomain_smtp_user"), + "EMAIL_SMTP_PASSWORD": cfg.RequireSecret("happydomain_smtp_password"), + "HAPPYDOMAIN_JWT_SECRET_KEY": cfg.RequireSecret("happydomain_beta_jwt_secret_key"), + "HAPPYDOMAIN_OVH_APPLICATION_KEY": cfg.RequireSecret("happydomain_ovh_application_key"), + "HAPPYDOMAIN_OVH_APPLICATION_SECRET": cfg.RequireSecret("happydomain_ovh_application_secret"), + "HAPPYDOMAIN_VERSION": pulumi.String("latest"), + "MY_DOMAIN": pulumi.String("beta.happydomain.org"), + "RESTIC_REPOSITORY": pulumi.String("s3:blob.nemunai.re/zbackup-happydomain-beta"), + "RESTIC_PASSWORD": cfg.RequireSecret("restic_password"), + "RESTIC_AWS_ACCESS_KEY_ID": cfg.RequireSecret("restic_aws_access_key_id"), + "RESTIC_AWS_SECRET_ACCESS_KEY": cfg.RequireSecret("restic_aws_secret_access_key"), + }, + Metadata: pulumi.Map{ + "user_data": pulumi.String(base64.StdEncoding.EncodeToString(userData)), + "ssh_authorized_keys": pulumi.String(SSH_AUTHORIZED_KEYS), + }, + }) + if err != nil { + return err + } + + // Export the public IP so DNS for beta.happydomain.org can point at it + // (the IPv6 address is read off the VNIC from the OCI console, as for the + // main host). + ctx.Export("beta-instance-ip", instance.PublicIp) + + return nil +} diff --git a/main.go b/main.go index 3e0b5a6..c4086a7 100644 --- a/main.go +++ b/main.go @@ -10,33 +10,53 @@ func main() { pulumi.Run(func(ctx *pulumi.Context) error { ocicfg := config.New(ctx, "oci") - // My Compartment - compartment, err := identity.NewCompartment(ctx, "compartment", &identity.CompartmentArgs{ - Name: pulumi.Sprintf("%s-happydomain-compartment", ctx.Stack()), - Description: pulumi.String("Compartment for happyDomain"), - }) - if err != nil { - return err - } + if ocicfg.Require("region") == "uk-london-1" { + // My Compartment + compartment, err := identity.NewCompartment(ctx, "compartment", &identity.CompartmentArgs{ + Name: pulumi.Sprintf("%s-happydomain-compartment", ctx.Stack()), + Description: pulumi.String("Compartment for happyDomain"), + }) + if err != nil { + return err + } - ns, listmonkAuthToken, err := setupListmonkStorage(ctx, ocicfg, compartment) - if err != nil { - return err - } + ns, listmonkAuthToken, err := setupListmonkStorage(ctx, ocicfg, compartment) + if err != nil { + return err + } - pemprvkey, smtpcreds, err := setupEmails(ctx, ocicfg, compartment) - if err != nil { - return err - } + pemprvkey, smtpcreds, err := setupEmails(ctx, ocicfg, compartment) + if err != nil { + return err + } - subnet, err := setupNetwork(ctx, compartment) - if err != nil { - return err - } + subnet, err := setupNetwork(ctx, compartment) + if err != nil { + return err + } - err = setupHostMain(ctx, ocicfg, compartment, ns, subnet, listmonkAuthToken, smtpcreds, pemprvkey) - if err != nil { - return err + err = setupHostMain(ctx, ocicfg, compartment, ns, subnet, listmonkAuthToken, smtpcreds, pemprvkey) + if err != nil { + return err + } + } else if ocicfg.Require("region") == "eu-zurich-1" { + compartment, err := identity.NewCompartment(ctx, "compartment", &identity.CompartmentArgs{ + Name: pulumi.Sprintf("%s-happydomain-compartment", ctx.Stack()), + Description: pulumi.String("Compartment for happyDomain"), + }) + if err != nil { + return err + } + + subnet, err := setupNetwork(ctx, compartment) + if err != nil { + return err + } + + err = setupHostBeta(ctx, ocicfg, compartment, subnet) + if err != nil { + return err + } } return nil