Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions docker/default.conf
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,24 @@ server {

location / {
try_files $uri $uri.html $uri/ =404;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: ws: wss: https:;" always;
Comment thread
heisbrot marked this conversation as resolved.
add_header Last-Modified "";
expires off;
}

error_page 404 /404.html;
location = /404.html {
internal;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: ws: wss: https:;" always;
add_header Last-Modified "";
expires off;
}
}
89 changes: 88 additions & 1 deletion docker/init_react_envs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,93 @@ export NETBIRD_GOOGLE_TAG_MANAGER_ID=${NETBIRD_GOOGLE_TAG_MANAGER_ID}
export NETBIRD_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken}
export NETBIRD_DRAG_QUERY_PARAMS=${NETBIRD_DRAG_QUERY_PARAMS:-false}
export NETBIRD_WASM_PATH=${NETBIRD_WASM_PATH}
export NETBIRD_CSP=${NETBIRD_CSP}
export NETBIRD_CSP_CONNECT_SRC=${NETBIRD_CSP_CONNECT_SRC}
export NETBIRD_DISABLE_CSP=${NETBIRD_DISABLE_CSP}

echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"

# Build CSP
if [[ "${NETBIRD_DISABLE_CSP}" == "true" ]]; then
echo "CSP disabled via NETBIRD_DISABLE_CSP"
sed -i '/add_header Content-Security-Policy/d' /etc/nginx/http.d/default.conf
else
FIRST_PARTY_CSP="https://pkgs.netbird.io"
FIRST_PARTY_CSP_CONNECT_SRC="$NETBIRD_CSP_CONNECT_SRC"
THIRD_PARTY_CSP=""
THIRD_PARTY_CSP_CONNECT_SRC="https://api.github.com/repos/netbirdio/netbird/releases/latest https://raw.githubusercontent.com/netbirdio/dashboard/"
THIRD_PARTY_CSP_SCRIPT_SRC=""

CSP_DOMAINS=""
CSP_DOMAINS_CONNECT_SRC=""

if [[ -n "${NETBIRD_CSP}" ]]; then
CSP_DOMAINS="$CSP_DOMAINS $NETBIRD_CSP"
fi

# Add AUTH_AUTHORITY to CSP
if [[ -n "${AUTH_AUTHORITY}" ]]; then
CSP_DOMAINS="$CSP_DOMAINS $AUTH_AUTHORITY"
fi

# Add AUTH_AUDIENCE to CSP
if [[ -n "${AUTH_AUDIENCE}" && "${AUTH_AUDIENCE}" != "none" && "${AUTH_AUDIENCE}" == *.* ]]; then
if [[ "${AUTH_AUDIENCE}" == *"http://"* || "${AUTH_AUDIENCE}" == *"https://"* ]]; then
CSP_DOMAINS="$CSP_DOMAINS $AUTH_AUDIENCE"
else
CSP_DOMAINS="$CSP_DOMAINS https://$AUTH_AUDIENCE"
fi
fi

# Add NETBIRD_MGMT_API_ENDPOINT to CSP
if [[ -n "${NETBIRD_MGMT_API_ENDPOINT}" ]]; then
MGMT_HOST=$(echo "$NETBIRD_MGMT_API_ENDPOINT" | sed -E 's|https?://||' | cut -d'/' -f1)
if [[ -n "$MGMT_HOST" ]]; then
if [[ "$NETBIRD_MGMT_API_ENDPOINT" == https://* ]]; then
CSP_DOMAINS="$CSP_DOMAINS $NETBIRD_MGMT_API_ENDPOINT"
CSP_DOMAINS_CONNECT_SRC="$CSP_DOMAINS_CONNECT_SRC wss://$MGMT_HOST"
elif [[ "$NETBIRD_MGMT_API_ENDPOINT" == http://* ]]; then
CSP_DOMAINS="$CSP_DOMAINS $NETBIRD_MGMT_API_ENDPOINT"
CSP_DOMAINS_CONNECT_SRC="$CSP_DOMAINS_CONNECT_SRC ws://$MGMT_HOST"
fi
fi
fi

# Add LETSENCRYPT_DOMAIN to CSP
if [[ -n "${LETSENCRYPT_DOMAIN}" && "${LETSENCRYPT_DOMAIN}" != "none" ]]; then
if [[ "$LETSENCRYPT_DOMAIN" == *"localhost"* ]]; then
CSP_DOMAINS="$CSP_DOMAINS http://$LETSENCRYPT_DOMAIN"
CSP_DOMAINS_CONNECT_SRC="$CSP_DOMAINS_CONNECT_SRC ws://$LETSENCRYPT_DOMAIN ws://*.$LETSENCRYPT_DOMAIN"
else
CSP_DOMAINS="$CSP_DOMAINS https://$LETSENCRYPT_DOMAIN"
CSP_DOMAINS_CONNECT_SRC="$CSP_DOMAINS_CONNECT_SRC wss://$LETSENCRYPT_DOMAIN wss://*.$LETSENCRYPT_DOMAIN"
fi
fi

CSP_CONNECT_SRC="$CSP_DOMAINS $CSP_DOMAINS_CONNECT_SRC $FIRST_PARTY_CSP $FIRST_PARTY_CSP_CONNECT_SRC $THIRD_PARTY_CSP $THIRD_PARTY_CSP_CONNECT_SRC"
CSP_FRAME_SRC="$CSP_DOMAINS $FIRST_PARTY_CSP $THIRD_PARTY_CSP"
CSP_SCRIPT_SRC="$CSP_DOMAINS $FIRST_PARTY_CSP $THIRD_PARTY_CSP $THIRD_PARTY_CSP_SCRIPT_SRC"

# Remove duplicates
CSP_CONNECT_SRC=$(echo $CSP_CONNECT_SRC | tr ' ' '\n' | grep -v '^$' | sort -u | tr '\n' ' ' | sed 's/ $//')
CSP_FRAME_SRC=$(echo $CSP_FRAME_SRC | tr ' ' '\n' | grep -v '^$' | sort -u | tr '\n' ' ' | sed 's/ $//')
CSP_SCRIPT_SRC=$(echo $CSP_SCRIPT_SRC | tr ' ' '\n' | grep -v '^$' | sort -u | tr '\n' ' ' | sed 's/ $//')

# Update CSP in nginx config
CSP_POLICY="default-src 'none'; connect-src 'self' $CSP_CONNECT_SRC; frame-src 'self' $CSP_FRAME_SRC; script-src 'self' 'wasm-unsafe-eval' $CSP_SCRIPT_SRC; font-src 'self'; img-src * data:; manifest-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests;"
CSP_HEADER="add_header Content-Security-Policy \"$CSP_POLICY\" always;"

# Escape sed special characters in replacement string (& | \)
CSP_HEADER=$(printf '%s' "$CSP_HEADER" | sed -e 's/[\\&|]/\\&/g')

echo "CSP header: $CSP_HEADER"

# Replace CSP header in nginx config
sed -i "s|add_header Content-Security-Policy \"[^\"]*\" always;|$CSP_HEADER|g" /etc/nginx/http.d/default.conf || {
echo "Failed to replace CSP header"
}
fi
Comment thread
heisbrot marked this conversation as resolved.

# replace ENVs in the config
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_CLIENT_SECRET \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$NETBIRD_GOOGLE_ANALYTICS_ID \$\$NETBIRD_GOOGLE_TAG_MANAGER_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE \$\$NETBIRD_DRAG_QUERY_PARAMS \$\$NETBIRD_WASM_PATH"

Expand All @@ -74,4 +158,7 @@ for f in $(grep -R -l AUTH_SUPPORTED_SCOPES /usr/share/nginx/html); do
cp "$f" "$f".copy
envsubst "$ENV_STR" < "$f".copy > "$f"
rm "$f".copy
done
done

# Reload nginx to pick up the updated CSP and config
nginx -s reload
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"dev": "next dev -p 3000",
"turbo": "next dev -p 3000 --turbo",
"build": "next build",
"postbuild": "node postbuild.js",
"start": "next start",
"lint": "next lint",
"cypress:open": "cypress open"
Expand Down
96 changes: 96 additions & 0 deletions postbuild.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
const { resolve, join } = require("path");
const { createHash } = require("crypto");
const {
readFileSync,
writeFileSync,
mkdirSync,
readdirSync,
statSync,
} = require("fs");

process.env.NODE_ENV = "production";
const PLACEHOLDER = "NB_INLINE_SCRIPT_PLACEHOLDER";
console.log("Starting post-build script to extract inline scripts...");

// Function to find HTML files recursively
function findHtmlFiles(dir) {
const files = [];
const entries = readdirSync(dir);

for (const entry of entries) {
const fullPath = join(dir, entry);
const stat = statSync(fullPath);

if (stat.isDirectory()) {
files.push(...findHtmlFiles(fullPath));
} else if (entry.endsWith(".html")) {
files.push(fullPath);
}
}

return files;
}

// For Next.js export output, the files are in the 'out' directory
const baseDir = resolve("out");
const htmlFiles = findHtmlFiles(baseDir);

console.log(`Found ${htmlFiles.length} .html files to process`);

// Ensure assets directory exists
const assetsDir = `${baseDir}/assets`;
mkdirSync(assetsDir, { recursive: true });

htmlFiles.forEach((file) => {
// Read file contents
const contents = readFileSync(file, "utf8");
const scripts = [];

// Extract inline scripts
const newFile = contents.replace(
/<script(?![^>]*src)([^>]*)>(.+?)<\/script>/gs,
(match, attributes, scriptContent) => {
// Skip if script has src attribute (external script)
if (attributes.includes("src=")) {
return match;
}

const addPlaceholderString = scripts.length === 0;
const cleanedScript = scriptContent.trim();

if (cleanedScript) {
scripts.push(
`${cleanedScript}${cleanedScript.endsWith(";") ? "" : ";"}`,
);
}

return addPlaceholderString ? PLACEHOLDER : "";
},
);

// Early exit if no inline scripts found
if (!scripts.length) {
console.log(`No inline scripts found`);
return;
}

// Combine scripts and create hash
const chunk = scripts.join("\n");
const hash = createHash("md5").update(chunk).digest("hex").slice(0, 8);
const chunkFileName = `chunk.${hash}.js`;
const chunkPath = `${assetsDir}/${chunkFileName}`;

// Write the chunk file
writeFileSync(chunkPath, chunk, "utf8");

// Replace placeholder string with script tag
const updatedFile = newFile.replace(
PLACEHOLDER,
`<script src="/assets/${chunkFileName}" crossorigin=""></script>`,
);

// Write updated HTML file
writeFileSync(file, updatedFile, "utf8");
});

console.log("Post-build script completed successfully!");