diff --git a/.gitignore b/.gitignore index d9390d9..303e9a1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ data *.secrets env.smtp +*.old +*.log +test diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..85a4244 --- /dev/null +++ b/Makefile @@ -0,0 +1,87 @@ +MODULES += nginx +MODULES += keycloak +MODULES += hedgedoc +MODULES += grafana +MODULES += prometheus +MODULES += mastodon +#MODULES += pixelfed + +include env.production +domain_name := $(DOMAIN_NAME) + + +help: + @echo "usage: make run" +UC = $(shell echo '$1' | tr '[:lower:]' '[:upper:]') + +DOCKER = \ + $(foreach m,$(MODULES),. data/$m/secrets && ) \ + docker-compose \ + --env-file env.production \ + $(foreach m,$(MODULES),--file ./$m.yaml) \ + +run: + $(DOCKER) up +down: + $(DOCKER) down +nginx-shell: + $(DOCKER) exec nginx sh +grafana-shell: + $(DOCKER) exec grafana bash +hedgedoc-shell: + $(DOCKER) exec hedgedoc sh +keycloak-shell: + $(DOCKER) exec keycloak sh +mastodon-shell: + $(DOCKER) exec mastodon bash +mastodon-streaming-shell: + $(DOCKER) exec mastodon-streaming bash +nginx-build: data/nginx/secrets + $(DOCKER) build nginx + +certdir = ./data/certbot/conf/live/${DOMAIN_NAME} + +run: secrets-setup + +secrets-setup: $(foreach m,$(MODULES),data/$m/secrets) + +# Create the per-subdomain secrets if they don't exist +# not every service requires all of these features, but create them anyway +GET_MODULE = $(call UC,$(word 2,$(subst /, ,$@))) +RAND = $$(openssl rand -hex $1) + +data/%/secrets: + mkdir -p $(dir $@) + echo >$@ "# DO NOT CHECK IN" + echo >>$@ "export $(GET_MODULE)_ADMIN_PASSWORD=$(call RAND,8)" + echo >>$@ "export $(GET_MODULE)_CLIENT_SECRET=$(call RAND,20)" + echo >>$@ "export $(GET_MODULE)_SESSION_SECRET=$(call RAND,20)" + +keycloak-setup: secrets-setup + $(DOCKER) run keycloak-setup + +certbot: + $(DOCKER) \ + run --entrypoint '/bin/sh -c "\ + rm -rf /etc/letsencrypt ; \ + certbot certonly \ + --webroot \ + --webroot-path /var/www/certbot \ + --email "admin@$(DOMAIN_NAME)" \ + --rsa-key-size "2048" \ + --agree-tos \ + --no-eff-email \ + --force-renewal \ + -d $(DOMAIN_NAME) \ + $(foreach m,$(MODULES),\ + -d $($(call UC,$m)_HOSTNAME).$(DOMAIN_NAME)) \ + "' certbot + +nginx-reload: + $(DOCKER) restart nginx + + +config: + $(DOCKER) config + +FORCE: diff --git a/README.md b/README.md index 10df55f..7243285 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Infrastructure for the self-hosted, single-sign-on, community-run services. ``` apt install jq docker-compose +apt install prometheus ``` * Setup each of the services. `keycloak` and `nginx` are required to start the others: diff --git a/env.production b/env.production index 0f083c1..1d807cb 100644 --- a/env.production +++ b/env.production @@ -1,12 +1,26 @@ -DOMAIN_NAME=hackerspace.zone +# Fill in with your top-level domain name and desired OAUTH realm name +DOMAIN_NAME=dev.v.st REALM=hackerspace -KEYCLOAK_HOSTNAME=login.hackerspace.zone -HEDGEDOC_HOSTNAME=docs.hackerspace.zone -MASTODON_HOSTNAME=social.hackerspace.zone -NEXTCLOUD_HOSTNAME=cloud.hackerspace.zone -GRAFANA_HOSTNAME=dashboard.hackerspace.zone -GITEA_HOSTNAME=git.hackerspace.zone -MATRIX_HOSTNAME=matrix.hackerspace.zone -MOBILIZON_HOSTNAME=events.hackerspace.zone -PIXELFED_HOSTNAME=pixelfed.hackerspace.zone +# Fill in with your SMTP server, if you have one +SMTP_SERVER= +SMTP_USER= +SMTP_PASSWORD= +SMTP_PORT= + +# You can leave these as is or change them if you like +NGINX_HOSTNAME=www +KEYCLOAK_HOSTNAME=login +HEDGEDOC_HOSTNAME=docs +MASTODON_HOSTNAME=social +NEXTCLOUD_HOSTNAME=cloud +GRAFANA_HOSTNAME=dashboard +GITEA_HOSTNAME=git +MATRIX_HOSTNAME=matrix +MOBILIZON_HOSTNAME=events +PIXELFED_HOSTNAME=pixelfed +PROMETHEUS_HOSTNAME=metrics + +AUTH_URL=https://${KEYCLOAK_HOSTNAME}.${DOMAIN_NAME}/realms/${REALM}/protocol/openid-connect/auth +TOKEN_URL=https://${KEYCLOAK_HOSTNAME}.${DOMAIN_NAME}/realms/${REALM}/protocol/openid-connect/token +USERINFO_URL=https://${KEYCLOAK_HOSTNAME}.${DOMAIN_NAME}/realms/${REALM}/protocol/openid-connect/userinfo diff --git a/grafana.yaml b/grafana.yaml new file mode 100644 index 0000000..24cea0c --- /dev/null +++ b/grafana.yaml @@ -0,0 +1,41 @@ +version: "3" +services: + grafana: + image: grafana/grafana-oss:8.5.1 + container_name: grafana + user: "0:0" + environment: + GF_AUTH_GENERIC_OAUTH_ENABLED: 'True' + GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP: 'True' # otherwise no login is possible + #GF_AUTH_GENERIC_OAUTH_TEAM_IDS: '' + #GF_AUTH_GENERIC_OAUTH_ALLOWED_ORGANIZATIONS: '' + #GF_AUTH_GENERIC_OAUTH_ALLOWED_DOMAINS: '' + #GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD} # ignored? + GF_AUTH_GENERIC_OAUTH_NAME: Keycloak + GF_AUTH_GENERIC_OAUTH_CLIENT_ID: grafana + GF_AUTH_GENERIC_OAUTH_SCOPES: openid profile email + GF_SERVER_ROOT_URL: https://${GRAFANA_HOSTNAME}.${DOMAIN_NAME}/ + GF_SERVER_DOMAIN: ${GRAFANA_HOSTNAME}.${DOMAIN_NAME} + GF_AUTH_GENERIC_OAUTH_AUTH_URL: ${AUTH_URL} + GF_AUTH_GENERIC_OAUTH_TOKEN_URL: ${TOKEN_URL} + GF_AUTH_GENERIC_OAUTH_API_URL: ${USERINFO_URL} + GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET: ${GRAFANA_CLIENT_SECRET} + # reset the admin password on every run, since otherwise it defaults to admin/admin + entrypoint: ["sh", "-c", "grafana-cli admin reset-admin-password ${GRAFANA_ADMIN_PASSWORD} && /run.sh"] + volumes: + - ./data/grafana:/var/lib/grafana + restart: always + # ports: + # - 3000:3000 + + # add the grafana nginx configuration into the nginx volume + nginx: + volumes: + - ./grafana/nginx.conf:/etc/nginx/templates/grafana.conf.template:ro + + # add the grafana client secrets to the keycloak-setup volume + keycloak-setup: + env_file: + - data/grafana/secrets + volumes: + - ./grafana/keycloak.sh:/keycloak-setup/grafana.sh:ro diff --git a/grafana/docker-compose.yaml b/grafana/docker-compose.yaml deleted file mode 100644 index e1e2b6a..0000000 --- a/grafana/docker-compose.yaml +++ /dev/null @@ -1,26 +0,0 @@ -version: "3" - -services: - grafana: - image: grafana/grafana-oss:8.5.1 - user: "0:0" - environment: - GF_AUTH_GENERIC_OAUTH_ENABLED: 'True' - GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP: 'True' # otherwise no login is possible - #GF_AUTH_GENERIC_OAUTH_TEAM_IDS: '' - #GF_AUTH_GENERIC_OAUTH_ALLOWED_ORGANIZATIONS: '' - #GF_AUTH_GENERIC_OAUTH_ALLOWED_DOMAINS: '' - GF_AUTH_GENERIC_OAUTH_NAME: Keycloak - GF_AUTH_GENERIC_OAUTH_CLIENT_ID: grafana - GF_AUTH_GENERIC_OAUTH_SCOPES: openid profile email - # GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET is in env.secrets - # auth URLs are in the env.secrets since they have hostname expansion - volumes: - - ../data/grafana:/var/lib/grafana - restart: always - ports: - - 8000:3000 - env_file: - - ../env.production - - env.production - - ../data/grafana/env.secrets diff --git a/grafana/env.production b/grafana/env.production deleted file mode 100644 index e69de29..0000000 diff --git a/grafana/keycloak.sh b/grafana/keycloak.sh new file mode 100755 index 0000000..377850d --- /dev/null +++ b/grafana/keycloak.sh @@ -0,0 +1,4 @@ +#!/bin/bash -x +# Setup the grafana client connection + +client-create grafana "$GRAFANA_HOSTNAME.$DOMAIN_NAME" "$GRAFANA_CLIENT_SECRET" &2 "$@" ; exit 1 ; } - -DIRNAME="$(dirname $0)" -cd "$DIRNAME" -source ../env.production || die "no top level env?" -source env.production || die "no local env?" - -BASE="https://$KEYCLOAK_HOSTNAME/realms/$REALM/protocol/openid-connect" -SECRETS="../data/grafana/env.secrets" - -if [ -r "$SECRETS" ]; then - docker-compose up -d || die "grafana: unable to start container" - exit 0 -fi - -docker-compose down 2>/dev/null - -GRAFANA_CLIENT_SECRET="$(openssl rand -hex 32)" -GRAFANA_ADMIN_PASSWORD="$(openssl rand -hex 4)" - -echo "Generating secrets: admin password $GRAFANA_ADMIN_PASSWORD" -mkdir -p "$(dirname "$SECRETS")" -cat < "$SECRETS" -# Do not check in! -GF_SECURITY_ADMIN_PASSWORD=$GRAFANA_ADMIN_PASSWORD -GF_SERVER_ROOT_URL=https://$GRAFANA_HOSTNAME/ -GF_SERVER_DOMAIN=$GRAFANA_HOSTNAME -GF_AUTH_GENERIC_OAUTH_AUTH_URL=$BASE/auth -GF_AUTH_GENERIC_OAUTH_TOKEN_URL=$BASE/token -GF_AUTH_GENERIC_OAUTH_API_URL=$BASE/userinfo -GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET=$GRAFANA_CLIENT_SECRET -EOF - - -../keycloak/client-delete 'grafana' 2>/dev/null - -../keycloak/client-create << EOF || die "unable to create client id" -{ - "clientId": "grafana", - "rootUrl": "https://$GRAFANA_HOSTNAME/", - "adminUrl": "https://$GRAFANA_HOSTNAME/", - "redirectUris": [ "https://$GRAFANA_HOSTNAME/*" ], - "webOrigins": [ "https://$GRAFANA_HOSTNAME" ], - "clientAuthenticatorType": "client-secret", - "secret": "$GRAFANA_CLIENT_SECRET" -} -EOF - -docker-compose up -d || die "grafana: unable to bring up container" diff --git a/hedgedoc.yaml b/hedgedoc.yaml new file mode 100644 index 0000000..6828dd4 --- /dev/null +++ b/hedgedoc.yaml @@ -0,0 +1,58 @@ +version: '3.9' +services: + hedgedoc-db: + image: postgres:13.4-alpine + container_name: hedgedoc-db + environment: + - POSTGRES_USER=hedgedoc + - POSTGRES_PASSWORD=password + - POSTGRES_DB=hedgedoc + volumes: + - ./data/hedgedoc/database:/var/lib/postgresql/data + restart: always + + hedgedoc: + # Make sure to use the latest release from https://hedgedoc.org/latest-release + image: quay.io/hedgedoc/hedgedoc:1.9.4 + container_name: hedgedoc + environment: + #- CMD_CSP_ENABLE=false + - CMD_DB_URL=postgres://hedgedoc:password@hedgedoc-db:5432/hedgedoc + - CMD_PROTOCOL_USESSL=true + - CMD_ALLOW_ANONYMOUS=false # anonymous user's can't create notes + - CMD_ALLOW_ANONYMOUS_EDITS=true # but they can be invited to edit notes + - CMD_ALLOW_FREEURL=true # users can create arbitrary names + - CMD_EMAIL=false # only oauth logins + - CMD_DOMAIN=${HEDGEDOC_HOSTNAME}.${DOMAIN_NAME} + - CMD_OAUTH2_AUTHORIZATION_URL=${AUTH_URL} + - CMD_OAUTH2_TOKEN_URL=${TOKEN_URL} + - CMD_OAUTH2_USER_PROFILE_URL=${USERINFO_URL} + - CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR=preferred_username + - CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR=name + - CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR=email + - CMD_OAUTH2_CLIENT_ID=hedgedoc + - CMD_OAUTH2_PROVIDERNAME=Keycloak + - CMD_OAUTH2_CLIENT_SECRET=${HEDGEDOC_CLIENT_SECRET} + - CMD_SESSION_SECRET=${HEDGEDOC_SESSION_SECRET} + env_file: + - env.production + volumes: + - ./data/hedgedoc/uploads:/hedgedoc/public/uploads + # ports: + #- "3000:3000" + restart: always + depends_on: + - hedgedoc-db + - keycloak + + # add the hedgedoc nginx configuration into the nginx volume + nginx: + volumes: + - ./hedgedoc/nginx.conf:/etc/nginx/templates/hedgedoc.conf.template:ro + + # add the hedgedoc client secrets to the keycloak-setup volume + keycloak-setup: + env_file: + - data/hedgedoc/secrets + volumes: + - ./hedgedoc/keycloak.sh:/keycloak-setup/hedgedoc.sh:ro diff --git a/hedgedoc/docker-compose.yaml b/hedgedoc/docker-compose.yaml deleted file mode 100644 index daf7342..0000000 --- a/hedgedoc/docker-compose.yaml +++ /dev/null @@ -1,34 +0,0 @@ -version: '3' -services: - database: - image: postgres:13.4-alpine - environment: - - POSTGRES_USER=hedgedoc - - POSTGRES_PASSWORD=password - - POSTGRES_DB=hedgedoc - volumes: - - ../data/hedgedoc/database:/var/lib/postgresql/data - restart: always - hedgedoc: - # Make sure to use the latest release from https://hedgedoc.org/latest-release - image: quay.io/hedgedoc/hedgedoc:1.9.3 - env_file: - - ../env.production - - env.production - - ../data/hedgedoc/env.secrets - environment: - #- CMD_CSP_ENABLE=false - - CMD_DB_URL=postgres://hedgedoc:password@database:5432/hedgedoc - - CMD_PROTOCOL_USESSL=true - - CMD_ALLOW_ANONYMOUS=false # anonymous user's can't create notes - - CMD_ALLOW_ANONYMOUS_EDITS=true # but they can be invited to edit notes - - CMD_ALLOW_FREEURL=true # users can create arbitrary names - - CMD_EMAIL=false # only oauth logins - # DOMAIN and OAUTH2 variables are now in env.secret - volumes: - - ../data/hedgedoc/uploads:/hedgedoc/public/uploads - ports: - - "3000:3000" - restart: always - depends_on: - - database diff --git a/hedgedoc/env.production b/hedgedoc/env.production deleted file mode 100644 index 118615d..0000000 --- a/hedgedoc/env.production +++ /dev/null @@ -1,2 +0,0 @@ -CMD_OAUTH2_CLIENT_SECRET=abcdef1234 -CMD_SESSION_SECRET=abcdef1234 diff --git a/hedgedoc/keycloak.sh b/hedgedoc/keycloak.sh new file mode 100755 index 0000000..b94337e --- /dev/null +++ b/hedgedoc/keycloak.sh @@ -0,0 +1,50 @@ +#!/bin/bash -x +# Setup the hedgedoc client connection + +# this might fail; we'll ignore it if we have already created it +# https://github.com/hedgedoc/hedgedoc/issues/56 +kcadm.sh \ + create client-scopes \ + -r "$REALM" \ + -f - <&2 "$@" ; exit 1 ; } - -DIRNAME="$(dirname $0)" -cd "$DIRNAME" -source ../env.production || die "no top levle env?" -source env.production || die "no local env?" - -DATA="../data/hedgedoc" -SECRETS="$DATA/env.secrets" - -if [ -r "$SECRETS" ]; then - docker-compose up -d || die "hedgedoc: unable to start" - exit 0 -fi - -docker-compose down 2>/dev/null - -# regenerate the client secrets -CLIENT_SECRET="$(openssl rand -hex 20)" -SESSION_SECRET="$(openssl rand -hex 20)" - -mkdir -p "$DATA/uploads" -chmod 666 "$DATA/uploads" - -cat < "$SECRETS" -# DO NOT CHECK IN -CMD_OAUTH2_CLIENT_SECRET=$CLIENT_SECRET -CMD_SESSION_SECRET=$SESSION_SECRET -CMD_DOMAIN=${HEDGEDOC_HOSTNAME} -CMD_OAUTH2_AUTHORIZATION_URL=https://${KEYCLOAK_HOSTNAME}/realms/${REALM}/protocol/openid-connect/auth -CMD_OAUTH2_TOKEN_URL=https://${KEYCLOAK_HOSTNAME}/realms/${REALM}/protocol/openid-connect/token -CMD_OAUTH2_USER_PROFILE_URL=https://${KEYCLOAK_HOSTNAME}/realms/${REALM}/protocol/openid-connect/userinfo -CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR=preferred_username -CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR=name -CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR=email -CMD_OAUTH2_CLIENT_ID=hedgedoc -CMD_OAUTH2_PROVIDERNAME=Keycloak -EOF - -../keycloak/client-delete hedgedoc - -../keycloak/client-create <&2 "$@" ; exit 1 ; } -DIRNAME="$(dirname $0)" -cd "$DIRNAME" +client_name="$1" +hostname="$2" +secret="$3" -source ../env.production || die "no top levle env?" -source env.production || die "no local env?" -source "../data/keycloak/env.secrets" || die "no local secrets?" +client_id="$(kcadm.sh get clients \ + -r "$REALM" \ + --fields id \ + -q clientId="$client_name" \ + --format csv \ + --noquotes \ +)" -docker-compose exec -T keycloak \ - /opt/keycloak/bin/kcadm.sh \ - create clients \ - --server http://localhost:8080/ \ - --user admin \ - --realm master \ - --password "$KEYCLOAK_ADMIN_PASSWORD" \ - -r "$REALM" \ - -f - \ -|| die "create client failed" +if [ -n "$client_id" ]; then + kcadm.sh delete "clients/$client_id" -r "$REALM" || die "$client_id: unable to delete" +fi + +# remember to add a leading , if adding extra data +extra="$(cat -)" + +kcadm.sh create clients -r "$REALM" -f - <&2 "$@" ; exit 1 ; } - -DIRNAME="$(dirname $0)" -cd "$DIRNAME" - -source ../env.production || die "no top levle env?" -source env.production || die "no local env?" -source "../data/keycloak/env.secrets" || die "no local secrets?" - -# try to get the clients by name -CLIENT_NAME="$1" -if [ -z "$CLIENT_NAME" ]; then - die "usage: $0 clientName" -fi - -CLIENT_ID="$(docker-compose exec -T keycloak \ - /opt/keycloak/bin/kcadm.sh \ - get clients \ - --server http://localhost:8080/ \ - --user admin \ - --password "$KEYCLOAK_ADMIN_PASSWORD" \ - --realm master \ - -r "$REALM" \ -| jq -r ".[] | select( .clientId == \"$CLIENT_NAME\" ).id")" - -if [ -z "$CLIENT_ID" ]; then - die "$CLIENT_NAME: no such client" -fi - -echo "$0: $CLIENT_NAME = $CLIENT_ID" -docker-compose exec -T keycloak \ - /opt/keycloak/bin/kcadm.sh \ - delete "clients/$CLIENT_ID" \ - --server http://localhost:8080/ \ - --user admin \ - --realm master \ - --password "$KEYCLOAK_ADMIN_PASSWORD" \ - -r "$REALM" \ - || die "$CLIENT_NAME($CLIENT_ID): unable to remove" diff --git a/keycloak/docker-compose.yaml b/keycloak/docker-compose.yaml deleted file mode 100644 index 62ec6e1..0000000 --- a/keycloak/docker-compose.yaml +++ /dev/null @@ -1,43 +0,0 @@ -version: '3' - -volumes: - mysql_data: - driver: local - -services: - mysql: - image: mysql:5.7 - restart: always - volumes: - - ../data/keycloak/database:/var/lib/mysql - environment: - MYSQL_ROOT_PASSWORD: root - MYSQL_DATABASE: keycloak - MYSQL_USER: keycloak - MYSQL_PASSWORD: password - - keycloak: - image: quay.io/keycloak/keycloak:18.0.0 - restart: always - entrypoint: /opt/keycloak/bin/kc.sh start --hostname="$${KEYCLOAK_HOSTNAME}" --proxy=edge - user: "0:0" # otherwise the persistent data directory is not writable - env_file: - - ../env.production - - env.production - - ../data/keycloak/env.secrets - environment: - DB_VENDOR: MYSQL - DB_ADDR: mysql - DB_DATABASE: keycloak - DB_USER: keycloak - DB_PASSWORD: password - KEYCLOAK_ADMIN: admin - # KEYCLOAK_ADMIN_PASSWORD should be set in env.secrets - PROXY_ADDRESS_FORWARDING: 'true' - volumes: - - ../data/keycloak/certs:/etc/x509/https - - ../data/keycloak/keycloak:/opt/keycloak/data - ports: - - 8080:8080 - depends_on: - - mysql diff --git a/keycloak/env.production b/keycloak/env.production deleted file mode 100644 index e69de29..0000000 diff --git a/nginx/nginx/templates/login.conf.template b/keycloak/nginx.conf similarity index 83% rename from nginx/nginx/templates/login.conf.template rename to keycloak/nginx.conf index 397b5c6..e7398b5 100644 --- a/nginx/nginx/templates/login.conf.template +++ b/keycloak/nginx.conf @@ -1,9 +1,9 @@ server { - server_name login.${DOMAIN_NAME}; + server_name ${KEYCLOAK_HOSTNAME} ${KEYCLOAK_HOSTNAME}.${DOMAIN_NAME}; client_max_body_size 128m; location / { - proxy_pass http://host.docker.internal:8080; + proxy_pass http://keycloak:8080; proxy_pass_header Set-Cookie; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $remote_addr; diff --git a/keycloak/setup b/keycloak/setup deleted file mode 100755 index 15792c4..0000000 --- a/keycloak/setup +++ /dev/null @@ -1,119 +0,0 @@ -#!/bin/bash -die() { echo >&2 "keycloak: ERROR: $@" ; exit 1 ; } -info() { echo >&2 "keycloak: $@" ; } - -DIRNAME="$(dirname $0)" -cd "$DIRNAME" -source ../env.production -source ./env.production -source "../env.smtp" 2>/dev/null - -SECRETS="../data/keycloak/env.secrets" - -if [ -r "$SECRETS" ]; then - docker-compose up -d || die "keycloak: unable to start container" - exit 0 -fi - -docker-compose down 2>/dev/null - -KEYCLOAK_ADMIN_PASSWORD="$(openssl rand -hex 8)" -echo "Keycloak admin password $KEYCLOAK_ADMIN_PASSWORD" - -mkdir -p "$(dirname "$SECRETS")" -cat < "$SECRETS" -# DO NOT CHECK IN -KEYCLOAK_ADMIN_PASSWORD=$KEYCLOAK_ADMIN_PASSWORD -EOF - -docker-compose up -d || die "unable to start keycloak" -echo "sleeping a minute while keycloak initializes..." -sleep 30 - - -info "logging into server" -docker-compose exec keycloak \ - /opt/keycloak/bin/kcadm.sh \ - config credentials \ - --server http://localhost:8080/ \ - --user admin \ - --password "$KEYCLOAK_ADMIN_PASSWORD" \ - --realm master \ -|| die "unable to login" - - -info "Create a new realm for '$REALM'" -docker-compose exec keycloak \ - /opt/keycloak/bin/kcadm.sh \ - create realms \ - -s "realm=$REALM" \ - -s enabled=true \ -|| die "unable to create realm" - - -# https://github.com/hedgedoc/hedgedoc/issues/56 -info "Fix up a id bug" -docker-compose exec -T keycloak \ - /opt/keycloak/bin/kcadm.sh \ - create client-scopes \ - -r "$REALM" \ - -f - < "$VAPID_KEY" \ + || exit 1 +fi + +. "$VAPID_KEY" + +if [ ! -r "$DB_SETUP" ]; then + rails db:setup \ + || exit 1 + + touch "$DB_SETUP" +fi + +exec bundle exec rails s -p 6001 +EOF + + diff --git a/mastodon/env.production b/mastodon/env.production index e99b24b..120834b 100644 --- a/mastodon/env.production +++ b/mastodon/env.production @@ -19,12 +19,12 @@ # Redis # ----- -REDIS_HOST=redis +REDIS_HOST=mastodon-redis REDIS_PORT=6379 # PostgreSQL # ---------- -DB_HOST=database +DB_HOST=mastodon-db DB_USER=mastodon DB_NAME=mastodon_production DB_PASS=mastodon @@ -33,7 +33,7 @@ DB_PORT=5432 # Elasticsearch (optional) # ------------------------ ES_ENABLED=true -ES_HOST=es +ES_HOST=mastodon-es ES_PORT=9200 # Authentication for ES (optional) ES_USER=elastic @@ -67,6 +67,9 @@ ES_PASS=password #AWS_SECRET_ACCESS_KEY= #S3_ALIAS_HOST=files.example.com +# Do not use sendfile since this is fronted by nginx +RAILS_SERVE_STATIC_FILES=false + # do not allow normal logins OMNIAUTH_ONLY=true diff --git a/mastodon/keycloak.sh b/mastodon/keycloak.sh new file mode 100755 index 0000000..389eb61 --- /dev/null +++ b/mastodon/keycloak.sh @@ -0,0 +1,3 @@ +#!/bin/bash -x + +client-create mastodon "$MASTODON_HOSTNAME.$DOMAIN_NAME" "$MASTODON_CLIENT_SECRET" &2 "ERROR: $@" ; exit 1 ; } -info() { echo >&2 "$@" ; } - -DIRNAME="$(dirname $0)" -cd "$DIRNAME" -source ../env.production -source ./env.production -source "../env.smtp" 2>/dev/null - -mkdir -p ../data/mastodon/system -chmod 777 ../data/mastodon/system - -SECRETS="../data/mastodon/env.secrets" - -if [ -r "$SECRETS" ]; then - docker-compose up -d || die "unable to restart mastodon" - exit 0 -fi - -# have to bring it all down before we touch the files -docker-compose down - -OIDC_CLIENT_SECRET="$(openssl rand -hex 32)" - -# create the secrets file, -# along with some parameters that should be in the environment -mkdir -p "$(dirname "$SECRETS")" -cat < "$SECRETS" -# DO NOT CHECK IN -WEB_DOMAIN=$MASTODON_HOSTNAME -LOCAL_DOMAIN=$DOMAIN_NAME -OIDC_DISPLAY_NAME=$REALM -OIDC_ISSUER=https://$KEYCLOAK_HOSTNAME/realms/$REALM -OIDC_REDIRECT_URI=https://$MASTODON_HOSTNAME/auth/auth/openid_connect/callback -OIDC_CLIENT_SECRET=$OIDC_CLIENT_SECRET -SECRET_KEY_BASE=$(openssl rand -hex 32) -OTP_SECRET=$(openssl rand -hex 32) -EOF - -if [ -n "$SMTP_SERVER" ]; then - cat <> "$SECRETS" -SMTP_SERVER=$SMTP_SERVER -SMTP_PORT=$SMTP_PORT -SMTP_LOGIN=$SMTP_USER -SMTP_PASSWORD=$SMTP_PASSWORD -SMTP_FROM_ADDRESS=mastodon@$DOMAIN_NAME -EOF -fi - -info "mastodon: creating push keys" -docker-compose run --rm mastodon \ - rails mastodon:webpush:generate_vapid_key \ - >> "$SECRETS" \ -|| die "unable to generate vapid key" - -info "mastodon: setting up database" -docker-compose run --rm mastodon \ - rails db:setup \ -|| die "unable to login" - -source "$SECRETS" - -info "mastodon: creating keycloak interface" -../keycloak/client-delete mastodon -../keycloak/client-create <&2 "$@" ; exit 1 ; } - -DIRNAME="$(dirname $0)" -cd "$DIRNAME" - -source ../env.production -source ./env.production - -domain_args="-d $DOMAIN_NAME,$KEYCLOAK_HOSTNAME,$HEDGEDOC_HOSTNAME,$MASTODON_HOSTNAME,$NEXTCLOUD_HOSTNAME,$GRAFANA_HOSTNAME,$MATRIX_HOSTNAME,$GITEA_HOSTNAME,$MOBILIZON_HOSTNAME,$PIXELFED_HOSTNAME" -rsa_key_size=2048 - -set -x - -# move the temp live directory away if -# this is the first time we've run anything here -if [ ! -d "../data/certbot/conf/accounts" ]; then - echo "deleting temp keys" - rm -rf ../data/certbot/conf/live -fi - -docker-compose run --rm certbot \ - certonly \ - --webroot \ - --webroot-path /var/www/certbot \ - --email "admin@$DOMAIN_NAME" \ - --rsa-key-size "$rsa_key_size" \ - --agree-tos \ - --no-eff-email \ - --force-renewal \ - $domain_args \ -|| die "unable to renew!" - -docker-compose exec nginx nginx -s reload diff --git a/nginx/nginx/templates/000-default.conf.template b/nginx/default.conf similarity index 70% rename from nginx/nginx/templates/000-default.conf.template rename to nginx/default.conf index 293d02c..da6c466 100644 --- a/nginx/nginx/templates/000-default.conf.template +++ b/nginx/default.conf @@ -1,6 +1,30 @@ -# Redirect *all* port 80 traffic to the same thing on port 443 +vhost_traffic_status_zone; + server { listen 80 default_server; + + # this works on the docker container with http_stub built in + # only allow from localhost + location /nginx_status { + stub_status on; + access_log off; + allow 127.0.0.1; + deny all; + } + + # this works with the vts module + location /status { + vhost_traffic_status_display; + vhost_traffic_status_display_format html; + access_log off; + #allow 127.0.0.1; + #deny all; + } + + # forward certbot challenges to the certbot directory + include /etc/nginx/includes/challenge.conf; + + # Redirect *all other* port 80 traffic to the same thing on port 443 location / { return 301 https://$host$request_uri; } @@ -27,13 +51,16 @@ server { chunked_transfer_encoding on; # delegated Matrix server - location /.well-known/matrix { - proxy_pass https://${MATRIX_HOSTNAME}; - } +# location /.well-known/matrix { +# proxy_pass https://${MATRIX_HOSTNAME}.${DOMAIN_NAME}; +# } # separate Mastodon WEB_DOMAIN and LOCAL_DOMAIN location = /.well-known/host-meta { - return 302 https://${MASTODON_HOSTNAME}$request_uri; + return 302 https://${MASTODON_HOSTNAME}.${DOMAIN_NAME}$request_uri; + } + location = /.well-known/webfinger { + return 302 https://${MASTODON_HOSTNAME}.${DOMAIN_NAME}$request_uri; } # tilde club home directories @@ -66,7 +93,7 @@ server { proxy_hide_header Content-Security-Policy; add_header Content-Security-Policy "script-src 'self' 'unsafe-inline' 'unsafe-eval' *.${DOMAIN_NAME}; frame-src 'self' *.${DOMAIN_NAME}; object-src 'self'; base-uri 'self' *.${DOMAIN_NAME}"; - proxy_pass http://host.docker.internal:3000/s$request_uri; + proxy_pass http://hedgedoc:3000/s$request_uri; proxy_cache_valid any 1m; } @@ -77,7 +104,7 @@ server { proxy_ignore_headers Cache-Control; proxy_cache_valid any 1m; - proxy_pass http://host.docker.internal:3000$request_uri; + proxy_pass http://hedgedoc:3000$request_uri; } listen 443 ssl default_server; diff --git a/nginx/docker-compose.yaml b/nginx/docker-compose.yaml deleted file mode 100644 index 11e78f8..0000000 --- a/nginx/docker-compose.yaml +++ /dev/null @@ -1,28 +0,0 @@ -version: '3' -services: - nginx: - image: nginx:1.21-alpine - restart: always - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - - ./nginx/templates:/etc/nginx/templates:ro - - ./nginx/includes:/etc/nginx/includes:ro - - ../html:/var/www/html:ro - - ../data/certbot/www:/var/www/certbot:ro - - ../data/certbot/conf:/etc/letsencrypt:ro - - ../data/nginx/cache:/data/nginx/cache:rw - - /home:/home:ro - env_file: - - ../env.production - - env.production - extra_hosts: - - "host.docker.internal:host-gateway" - - certbot: - image: certbot/certbot - volumes: - - ../data/certbot/conf:/etc/letsencrypt - - ../data/certbot/www:/var/www/certbot diff --git a/nginx/docker-entrypoint.d/01-collectd.sh b/nginx/docker-entrypoint.d/01-collectd.sh new file mode 100755 index 0000000..7750999 --- /dev/null +++ b/nginx/docker-entrypoint.d/01-collectd.sh @@ -0,0 +1,12 @@ +#!/bin/sh -x +touch /started + +#cat >> /etc/collectd/collectd.conf < /tmp/conf < + URL "http://localhost:80/nginx_status" + +EOF + +#collectd diff --git a/nginx/docker-entrypoint.d/10-createkey.sh b/nginx/docker-entrypoint.d/10-createkey.sh new file mode 100755 index 0000000..eb718ef --- /dev/null +++ b/nginx/docker-entrypoint.d/10-createkey.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +mkdir -p /data/nginx/cache + +if [ -z "$DOMAIN_NAME" ]; then + DOMAIN_NAME="example.com" +fi + +certdir="/etc/letsencrypt/live/${DOMAIN_NAME}" + +if [ -r "$certdir/fullchain.pem" ]; then + exit 0 +fi + +mkdir -p "$certdir" + +echo >&2 "$certdir: Creating temporary keys" +openssl req \ + -x509 \ + -newkey rsa:2048 \ + -keyout "$certdir/privkey.pem" \ + -out "$certdir/fullchain.pem" \ + -sha256 \ + -nodes \ + -days 365 \ + -subj "/CN=$DOMAIN_NAME'" \ +|| exit 1 + +echo >&2 "$certdir: Generated temporary keys -- certbot needs to request real ones" +exit 0 + diff --git a/nginx/docker-entrypoint.d/20-envsubst-on-templates.sh b/nginx/docker-entrypoint.d/20-envsubst-on-templates.sh new file mode 100755 index 0000000..d0398b1 --- /dev/null +++ b/nginx/docker-entrypoint.d/20-envsubst-on-templates.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +set -e + +ME=$(basename $0) + +entrypoint_log() { + if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then + echo "$@" + fi +} + +auto_envsubst() { + local template_dir="${NGINX_ENVSUBST_TEMPLATE_DIR:-/etc/nginx/templates}" + local suffix="${NGINX_ENVSUBST_TEMPLATE_SUFFIX:-.template}" + local output_dir="${NGINX_ENVSUBST_OUTPUT_DIR:-/etc/nginx/conf.d}" + local filter="${NGINX_ENVSUBST_FILTER:-}" + + local template defined_envs relative_path output_path subdir + defined_envs=$(printf '${%s} ' $(awk "END { for (name in ENVIRON) { print ( name ~ /${filter}/ ) ? name : \"\" } }" < /dev/null )) + [ -d "$template_dir" ] || return 0 + if [ ! -w "$output_dir" ]; then + entrypoint_log "$ME: ERROR: $template_dir exists, but $output_dir is not writable" + return 0 + fi + find "$template_dir" -follow -type f -name "*$suffix" -print | while read -r template; do + relative_path="${template#$template_dir/}" + output_path="$output_dir/${relative_path%$suffix}" + subdir=$(dirname "$relative_path") + # create a subdirectory where the template file exists + mkdir -p "$output_dir/$subdir" + entrypoint_log "$ME: Running envsubst on $template to $output_path" + envsubst "$defined_envs" < "$template" > "$output_path" + done +} + +auto_envsubst + +exit 0 diff --git a/nginx/docker-entrypoint.sh b/nginx/docker-entrypoint.sh new file mode 100755 index 0000000..e201fe6 --- /dev/null +++ b/nginx/docker-entrypoint.sh @@ -0,0 +1,47 @@ +#!/bin/sh +# vim:sw=4:ts=4:et + +set -e + +entrypoint_log() { + if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then + echo "$@" + fi +} + +if [ "$1" = "nginx" -o "$1" = "nginx-debug" ]; then + if /usr/bin/find "/docker-entrypoint.d/" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then + entrypoint_log "$0: /docker-entrypoint.d/ is not empty, will attempt to perform configuration" + + entrypoint_log "$0: Looking for shell scripts in /docker-entrypoint.d/" + find "/docker-entrypoint.d/" -follow -type f -print | sort -V | while read -r f; do + case "$f" in + *.envsh) + if [ -x "$f" ]; then + entrypoint_log "$0: Sourcing $f"; + . "$f" + else + # warn on shell scripts without exec bit + entrypoint_log "$0: Ignoring $f, not executable"; + fi + ;; + *.sh) + if [ -x "$f" ]; then + entrypoint_log "$0: Launching $f"; + "$f" + else + # warn on shell scripts without exec bit + entrypoint_log "$0: Ignoring $f, not executable"; + fi + ;; + *) entrypoint_log "$0: Ignoring $f";; + esac + done + + entrypoint_log "$0: Configuration complete; ready for start up" + else + entrypoint_log "$0: No files found in /docker-entrypoint.d/, skipping configuration" + fi +fi + +exec "$@" diff --git a/nginx/env.production b/nginx/env.production deleted file mode 100644 index e69de29..0000000 diff --git a/nginx/nginx/includes/challenge.conf b/nginx/etc/includes/challenge.conf similarity index 100% rename from nginx/nginx/includes/challenge.conf rename to nginx/etc/includes/challenge.conf diff --git a/nginx/nginx/includes/options-ssl-nginx.conf b/nginx/etc/includes/options-ssl-nginx.conf similarity index 100% rename from nginx/nginx/includes/options-ssl-nginx.conf rename to nginx/etc/includes/options-ssl-nginx.conf diff --git a/nginx/nginx/includes/ssl-dhparams.pem b/nginx/etc/includes/ssl-dhparams.pem similarity index 100% rename from nginx/nginx/includes/ssl-dhparams.pem rename to nginx/etc/includes/ssl-dhparams.pem diff --git a/nginx/nginx/nginx.conf b/nginx/etc/nginx.conf similarity index 75% rename from nginx/nginx/nginx.conf rename to nginx/etc/nginx.conf index 80bcd49..2a339a7 100644 --- a/nginx/nginx/nginx.conf +++ b/nginx/etc/nginx.conf @@ -70,26 +70,9 @@ http { include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*; include /tmp/sites-enabled/*; -} +log_format main 'XXXX $http_x_forwarded_for - $remote_user [$time_local] "$host" "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" $request_time'; -#mail { -# # See sample authentication script at: -# # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript -# -# # auth_http localhost/auth.php; -# # pop3_capabilities "TOP" "USER"; -# # imap_capabilities "IMAP4rev1" "UIDPLUS"; -# -# server { -# listen localhost:110; -# protocol pop3; -# proxy on; -# } -# -# server { -# listen localhost:143; -# protocol imap; -# proxy on; -# } -#} +} diff --git a/nginx/nginx/templates/pixelfed.conf.template b/nginx/nginx/templates/pixelfed.conf.template deleted file mode 100644 index d3dbb74..0000000 --- a/nginx/nginx/templates/pixelfed.conf.template +++ /dev/null @@ -1,30 +0,0 @@ -server { - server_name ${PIXELFED_HOSTNAME}; - client_max_body_size 128m; - - sendfile on; - tcp_nopush on; - tcp_nodelay on; - keepalive_timeout 65; - types_hash_max_size 2048; - - gzip on; - gzip_disable "msie6"; - - proxy_read_timeout 1800s; - - location / { - proxy_pass http://host.docker.internal:8090; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - listen 443 ssl; - ssl_certificate /etc/letsencrypt/live/${DOMAIN_NAME}/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/${DOMAIN_NAME}/privkey.pem; - include /etc/nginx/includes/options-ssl-nginx.conf; - include /etc/nginx/includes/challenge.conf; - ssl_dhparam /etc/nginx/includes/ssl-dhparams.pem; -} diff --git a/nginx/nginx/templates/social.conf.template b/nginx/nginx/templates/social.conf.template deleted file mode 100644 index 9170395..0000000 --- a/nginx/nginx/templates/social.conf.template +++ /dev/null @@ -1,41 +0,0 @@ -map $http_upgrade $connection_upgrade { - default upgrade; - '' close; -} - -server { - server_name social.${DOMAIN_NAME}; - client_max_body_size 128m; - - location / { - proxy_pass http://host.docker.internal:6001; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Forwarded-Proto https; - } - - location /api/v1/streaming { - proxy_pass http://host.docker.internal:4000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - - proxy_buffering off; - proxy_redirect off; - proxy_http_version 1.1; - tcp_nodelay on; - } - - - listen 443 ssl; - ssl_certificate /etc/letsencrypt/live/${DOMAIN_NAME}/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/${DOMAIN_NAME}/privkey.pem; - include /etc/nginx/includes/options-ssl-nginx.conf; - include /etc/nginx/includes/challenge.conf; - ssl_dhparam /etc/nginx/includes/ssl-dhparams.pem; -} - - diff --git a/nginx/setup b/nginx/setup deleted file mode 100755 index 6426f22..0000000 --- a/nginx/setup +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -die() { echo >&2 "$@" ; exit 1 ; } - -DIRNAME="$(dirname $0)" -cd "$DIRNAME" - -source ../env.production || die "no top level env" -source env.production || die "no local env" - -if [ -z "${DOMAIN_NAME}" ]; then - die "DOMAIN_NAME not set" -fi - -certdir="../data/certbot/conf/live/${DOMAIN_NAME}" - -if [ -r "$certdir/privkey.pem" ]; then - docker-compose up -d || die "nginx: unable to start" - exit 0 -fi - -mkdir -p "$certdir" || die "$certdir: unable to make" - -openssl req \ - -x509 \ - -newkey rsa:2048 \ - -keyout "$certdir/privkey.pem" \ - -out "$certdir/fullchain.pem" \ - -sha256 \ - -nodes \ - -days 365 \ - -subj "/CN=${DOMAIN_NAME}'" \ -|| die "$certdir/privkey.pem: unable to create temp key" - -docker-compose up -d || die "unable to bring up nginx" - -echo "SLEEPING..." -sleep 10 - -./certbot-renew || die "unable to create certs" diff --git a/prometheus.yaml b/prometheus.yaml new file mode 100644 index 0000000..c0c95ee --- /dev/null +++ b/prometheus.yaml @@ -0,0 +1,18 @@ +version: '3' +services: + prometheus: + image: prom/prometheus + container_name: prometheus + volumes: + - ./data/prometheus/storage:/prometheus + - ./prometheus/prometheus.yaml:/etc/prometheus/prometheus.yml:ro + + cadvisor: + image: gcr.io/cadvisor/cadvisor:latest + container_name: cadvisor + volumes: + - /:/rootfs:ro + - /var/run:/var/run:rw + - /sys:/sys:ro + - /var/lib/docker/:/var/lib/docker:ro + diff --git a/prometheus/prometheus.yaml b/prometheus/prometheus.yaml new file mode 100644 index 0000000..5bf3071 --- /dev/null +++ b/prometheus/prometheus.yaml @@ -0,0 +1,21 @@ +global: + scrape_interval: 15s + external_labels: + monitor: 'codelab-monitor' + +scrape_configs: + # nginx vts data + - job_name: 'nginx' + scrape_interval: 5s + metrics_path: "/status/format/prometheus" + static_configs: + - targets: ['nginx:80'] + - job_name: 'metrics' + scrape_interval: 5s + static_configs: + # grafana data from /metrics + - targets: ['dashboard:3000'] + # host running the docker-compose + - targets: ['172.17.0.1:9100'] + # cadvisor system + - targets: ['cadvisor:8080'] diff --git a/start-all b/start-all deleted file mode 100755 index cbebca4..0000000 --- a/start-all +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -die() { echo >&2 "$@" ; exit 1 ; } - -which jq > /dev/null || die "jq not installed?" -which docker-compose > /dev/null || die "docker-compose not installed?" - -source ./env.production || die "no production env?" - -if [ -z "$DOMAIN_NAME" ]; then - die "\$DOMAIN_NAME not set; things will break" -fi - -SERVICES=nginx # there is no host -SERVICES+=\ keycloak -SERVICES+=\ hedgedoc -SERVICES+=\ nextcloud -SERVICES+=\ mastodon -SERVICES+=\ grafana -SERVICES+=\ matrix -SERVICES+=\ gitea -SERVICES+=\ mobilizon - -HOSTS+=\ $KEYCLOAK_HOST -HOSTS+=\ $HEDGEDOC_HOST -HOSTS+=\ $NEXTCLOUD_HOST -HOSTS+=\ $MASTODON_HOST -HOSTS+=\ $GRAFANA_HOST -HOSTS+=\ $MATRIX_HOST -HOSTS+=\ $GITEA_HOST -HOSTS+=\ $MOBILIZON_HOST - -for host in $HOSTS ; do - host $host > /dev/null || die "$host: DNS entry not present?" -done - -for service in $SERVICES ; do - echo "$service: starting" - ./$service/setup || die "$server: failed to start" -done diff --git a/stop-all b/stop-all deleted file mode 100755 index 5b8e97b..0000000 --- a/stop-all +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -for file in */docker-compose.yaml ; do - dir="$(dirname "$file")" - echo "$dir" - ( cd "$dir" ; docker-compose down ) -done