Changemaker/add-cname-records.sh

255 lines
8.4 KiB
Bash
Executable File

#!/bin/bash
echo "#############################################################"
echo "# "
echo "# This script will ADD CNAME records for your services "
echo "# to point to your Cloudflare tunnel. "
echo "# "
echo "#############################################################"
echo ""
echo "-------------------------------------------------------------"
echo "Cloudflare Credentials Required"
echo "Please ensure your .env file contains the following variables:"
echo " CF_API_TOKEN=your_cloudflare_api_token"
echo " CF_ZONE_ID=your_cloudflare_zone_id"
echo " CF_TUNNEL_ID=your_cloudflared_tunnel_id"
echo " CF_DOMAIN=yourdomain.com"
echo ""
echo "You can find these values in your Cloudflare dashboard:"
echo " - API Token: https://dash.cloudflare.com/profile/api-tokens (Create a token with Zone:DNS:Edit and Access:Apps:Edit permissions for your domain)"
echo " - Zone ID: On your domain's overview page"
echo " - Tunnel ID: In the Zero Trust dashboard under Access > Tunnels"
echo " - Domain: The domain you want to use for your services"
echo ""
echo "-------------------------------------------------------------"
echo ""
read -p "Type 'y' to continue or any other key to abort: " consent
if [[ "$consent" != "y" && "$consent" != "Y" ]]; then
echo "Aborted by user."
exit 1
fi
# Source environment variables from the .env file in the same directory
ENV_FILE="$(dirname "$0")/.env"
if [ -f "$ENV_FILE" ]; then
export $(grep -v '^#' "$ENV_FILE" | xargs)
else
echo "Error: .env file not found at $ENV_FILE"
exit 1
fi
# Check if required Cloudflare variables are set
if [ -z "$CF_API_TOKEN" ] || [ -z "$CF_ZONE_ID" ] || [ -z "$CF_TUNNEL_ID" ] || [ -z "$CF_DOMAIN" ]; then
echo "Error: One or more required Cloudflare environment variables (CF_API_TOKEN, CF_ZONE_ID, CF_TUNNEL_ID, CF_DOMAIN) are not set in $ENV_FILE."
exit 1
fi
# Check if jq is installed
if ! command -v jq &> /dev/null; then
echo "Error: jq is required but not installed. Please install jq to continue."
echo "On Debian/Ubuntu: sudo apt-get install jq"
echo "On RHEL/CentOS: sudo yum install jq"
exit 1
fi
# Array of subdomains based on docker-compose.yml services
SUBDOMAINS=(
"homepage"
"excalidraw"
"listmonk"
"monica"
"flatnotes"
"code-server"
"ollama"
"chat"
"gitea"
"mini-qr"
"ferdium"
"answer"
"nocodb"
"n8n"
"convertx"
"rocket"
"live"
"vw"
"docs"
)
# Add CNAME records for each subdomain
echo "Adding CNAME records for services..."
for subdomain in "${SUBDOMAINS[@]}"; do
echo "Adding CNAME record for $subdomain.$CF_DOMAIN..."
curl -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data "{
\"type\": \"CNAME\",
\"name\": \"$subdomain\",
\"content\": \"$CF_TUNNEL_ID.cfargotunnel.com\",
\"ttl\": 1,
\"proxied\": true
}"
echo -e "\n"
done
echo "All CNAME records have been added."
# Add root domain record
echo "Adding root domain (@ record)..."
curl -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data "{
\"type\": \"CNAME\",
\"name\": \"@\",
\"content\": \"$CF_TUNNEL_ID.cfargotunnel.com\",
\"ttl\": 1,
\"proxied\": true
}"
echo -e "\n"
echo "Root domain CNAME record has been added."
# Prompt for admin email
echo "Please enter the admin email address that should have access to protected services:"
read ADMIN_EMAIL
# Validate email format
if [[ ! "$ADMIN_EMAIL" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo "Error: Invalid email format. Please provide a valid email address."
exit 1
fi
# Get account ID (required for reusable policies)
echo "Getting Cloudflare account ID..."
ACCOUNT_RESPONSE=$(curl -s -X GET "https://api.cloudflare.com/client/v4/accounts" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json")
ACCOUNT_ID=$(echo $ACCOUNT_RESPONSE | jq -r '.result[0].id')
if [ -z "$ACCOUNT_ID" ] || [ "$ACCOUNT_ID" == "null" ]; then
echo "Error: Could not retrieve account ID. Response: $ACCOUNT_RESPONSE"
exit 1
fi
echo "Using account ID: $ACCOUNT_ID"
# Create reusable Access policy
echo "Creating reusable Access policy for admin access..."
REUSABLE_POLICY_RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/access/policies" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data "{
\"name\": \"Admin Access Policy for $CF_DOMAIN\",
\"decision\": \"allow\",
\"include\": [
{
\"email\": {
\"email\": \"$ADMIN_EMAIL\"
}
},
{
\"email_domain\": {
\"domain\": \"$CF_DOMAIN\"
}
}
],
\"require\": [],
\"exclude\": [],
\"session_duration\": \"24h\"
}")
# Extract the reusable policy ID
REUSABLE_POLICY_ID=$(echo $REUSABLE_POLICY_RESPONSE | jq -r '.result.id')
if [ -z "$REUSABLE_POLICY_ID" ] || [ "$REUSABLE_POLICY_ID" == "null" ]; then
echo "Error creating reusable Access policy. Response: $REUSABLE_POLICY_RESPONSE"
exit 1
else
echo "Successfully created reusable Access policy with ID: $REUSABLE_POLICY_ID"
fi
# Create single Access application for the entire domain
echo "Creating Access application for *.$CF_DOMAIN..."
DOMAIN_APP_RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/access/apps" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data "{
\"name\": \"Changemaker Services - $CF_DOMAIN\",
\"domain\": \"*.$CF_DOMAIN\",
\"type\": \"self_hosted\",
\"session_duration\": \"24h\",
\"app_launcher_visible\": true,
\"skip_interstitial\": true,
\"policies\": [\"$REUSABLE_POLICY_ID\"]
}")
# Extract the application ID
DOMAIN_APP_ID=$(echo $DOMAIN_APP_RESPONSE | jq -r '.result.id')
if [ -z "$DOMAIN_APP_ID" ] || [ "$DOMAIN_APP_ID" == "null" ]; then
echo "Error creating domain Access application. Response: $DOMAIN_APP_RESPONSE"
exit 1
else
echo "Successfully created domain Access application with ID: $DOMAIN_APP_ID"
fi
# Create bypass applications for public services
echo "Creating bypass applications for public services..."
PUBLIC_SERVICES=("excalidraw" "rocket" "listmonk" "vw" "docs")
for service in "${PUBLIC_SERVICES[@]}"; do
echo "Creating bypass access application for $service.$CF_DOMAIN..."
SERVICE_APP_RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/access/apps" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data "{
\"name\": \"$service $CF_DOMAIN (Public)\",
\"domain\": \"$service.$CF_DOMAIN\",
\"type\": \"self_hosted\",
\"session_duration\": \"24h\",
\"app_launcher_visible\": false,
\"skip_interstitial\": true
}")
SERVICE_APP_ID=$(echo $SERVICE_APP_RESPONSE | jq -r '.result.id')
if [ -z "$SERVICE_APP_ID" ] || [ "$SERVICE_APP_ID" == "null" ]; then
echo "Error creating $service access application. Response: $SERVICE_APP_RESPONSE"
else
echo "Successfully created $service access application with ID: $SERVICE_APP_ID"
# Create bypass policy
POLICY_RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/access/apps/$SERVICE_APP_ID/policies" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data "{
\"name\": \"Bypass for Everyone\",
\"decision\": \"bypass\",
\"include\": [{
\"everyone\": {}
}],
\"require\": [],
\"exclude\": []
}")
POLICY_SUCCESS=$(echo $POLICY_RESPONSE | jq -r '.success')
if [ "$POLICY_SUCCESS" == "true" ]; then
echo "Bypass policy for $service created successfully"
else
echo "Error creating bypass policy for $service: $(echo $POLICY_RESPONSE | jq -r '.errors[0].message')"
fi
fi
done
echo "Cloudflare Access setup complete."
echo "Admin access configured for: $ADMIN_EMAIL and users with @$CF_DOMAIN email addresses"
echo "Public services (excalidraw, rocket, listmonk, vw) are accessible without authentication"
echo "All other services require authentication through the unified Access policy"