diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ead0e71 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +# default +.git* +maintain/ +LICENSE +*.md +img/ +node_modules/ \ No newline at end of file diff --git a/.gitattributes b/.gitattributes index dfe0770..471fe64 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ -# Auto detect text files and perform LF normalization -* text=auto +# default +* text=auto +*.sh eol=lf \ No newline at end of file diff --git a/.github/workflows/cron.update.yml b/.github/workflows/cron.update.yml new file mode 100644 index 0000000..8d870d1 --- /dev/null +++ b/.github/workflows/cron.update.yml @@ -0,0 +1,115 @@ +name: cron-update + +on: + workflow_dispatch: + schedule: + - cron: "0 5 * * *" + +jobs: + cron-update: + runs-on: ubuntu-latest + + permissions: + actions: read + contents: write + + steps: + - name: init / checkout + uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 + with: + ref: 'master' + fetch-depth: 0 + + - name: cron-update / get latest version + run: | + echo "LATEST_VERSION=$(curl -s https://api.github.com/repos/netbirdio/netbird/releases/latest | jq -r '.tag_name' | sed 's/v//')" >> "${GITHUB_ENV}" + echo "LATEST_TAG=$(git describe --abbrev=0 --tags `git rev-list --tags --max-count=1` | sed 's/v//')" >> "${GITHUB_ENV}" + + - name: cron-update / setup node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: '20' + - run: npm i semver + + - name: cron-update / compare latest with current version + uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298 + with: + script: | + const { existsSync, readFileSync, writeFileSync } = require('node:fs'); + const { resolve } = require('node:path'); + const { inspect } = require('node:util'); + const semver = require('semver') + const repository = {dot:{}}; + + try{ + const path = resolve('.json'); + if(existsSync(path)){ + try{ + repository.dot = JSON.parse(readFileSync(path).toString()); + }catch(e){ + throw new Error('could not parse .json'); + } + }else{ + throw new Error('.json does not exist'); + } + }catch(e){ + core.setFailed(e); + } + + const latest = semver.valid(semver.coerce('${{ env.LATEST_VERSION }}')); + const current = semver.valid(semver.coerce(repository.dot.semver.version)); + const tag = semver.valid(semver.coerce('${{ env.LATEST_TAG }}')); + + if(latest && latest !== current){ + core.info(`new ${semver.diff(current, latest)} release found (${latest})!`) + repository.dot.semver.version = latest; + if(tag){ + core.exportVariable('WORKFLOW_NEW_TAG', semver.inc(tag, semver.diff(current, latest))); + } + + if(repository.dot.semver?.latest){ + repository.dot.semver.latest = repository.dot.semver.version; + } + + if(repository.dot?.readme?.comparison?.image){ + repository.dot.readme.comparison.image = repository.dot.readme.comparison.image.replace(current, repository.dot.semver.version); + } + + try{ + writeFileSync(resolve('.json'), JSON.stringify(repository.dot, null, 2)); + core.exportVariable('WORKFLOW_AUTO_UPDATE', true); + }catch(e){ + core.setFailed(e); + } + }else{ + core.info('no new release found'); + } + + core.info(inspect(repository.dot, {showHidden:false, depth:null, colors:true})); + + - name: cron-update / checkout + id: checkout + if: env.WORKFLOW_AUTO_UPDATE == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add .json + git commit -m "[upgrade] ${{ env.LATEST_VERSION }}" + git push origin HEAD:master + + - name: cron-update / tag + if: env.WORKFLOW_AUTO_UPDATE == 'true' && steps.checkout.outcome == 'success' + run: | + SHA256=$(git rev-list --branches --max-count=1) + git tag -a v${{ env.WORKFLOW_NEW_TAG }} -m "v${{ env.WORKFLOW_NEW_TAG }}" ${SHA256} + git push --follow-tags + + - name: cron-update / build docker image + if: env.WORKFLOW_AUTO_UPDATE == 'true' && steps.checkout.outcome == 'success' + uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 + with: + workflow: docker.yml + wait-for-completion: false + token: "${{ secrets.REPOSITORY_TOKEN }}" + inputs: '{ "release":"true", "readme":"true" }' + ref: "v${{ env.WORKFLOW_NEW_TAG }}" \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..9724675 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,439 @@ +name: docker +run-name: ${{ inputs.run-name }} + +on: + workflow_dispatch: + inputs: + run-name: + description: 'set run-name for workflow (multiple calls)' + type: string + required: false + default: 'docker' + + runs-on: + description: 'set runs-on for workflow (github or selfhosted)' + type: string + required: false + default: 'ubuntu-22.04' + + build: + description: 'set WORKFLOW_BUILD' + required: false + default: 'true' + + release: + description: 'set WORKFLOW_GITHUB_RELEASE' + required: false + default: 'false' + + readme: + description: 'set WORKFLOW_GITHUB_README' + required: false + default: 'false' + + etc: + description: 'base64 encoded json string' + required: false + +jobs: + docker: + runs-on: ${{ inputs.runs-on }} + timeout-minutes: 1440 + + services: + registry: + image: registry:2 + ports: + - 5000:5000 + + permissions: + actions: read + contents: write + packages: write + + steps: + - name: init / checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + ref: ${{ github.ref_name }} + fetch-depth: 0 + + - name: init / setup environment + uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298 + with: + script: | + const { existsSync, readFileSync } = require('node:fs'); + const { resolve } = require('node:path'); + const { inspect } = require('node:util'); + const { Buffer } = require('node:buffer'); + const inputs = `${{ toJSON(github.event.inputs) }}`; + const opt = {input:{}, dot:{}}; + + try{ + if(inputs.length > 0){ + opt.input = JSON.parse(inputs); + if(opt.input?.etc){ + opt.input.etc = JSON.parse(Buffer.from(opt.input.etc, 'base64').toString('ascii')); + } + } + }catch(e){ + core.warning('could not parse github.event.inputs'); + } + + try{ + const path = resolve('.json'); + if(existsSync(path)){ + try{ + opt.dot = JSON.parse(readFileSync(path).toString()); + }catch(e){ + throw new Error('could not parse .json'); + } + }else{ + throw new Error('.json does not exist'); + } + }catch(e){ + core.setFailed(e); + } + + core.info(inspect(opt, {showHidden:false, depth:null, colors:true})); + + const docker = { + image:{ + name:opt.dot.image, + arch:(opt.dot.arch || 'linux/amd64,linux/arm64'), + prefix:((opt.input?.etc?.semverprefix) ? `${opt.input?.etc?.semverprefix}-` : ''), + suffix:((opt.input?.etc?.semversuffix) ? `-${opt.input?.etc?.semversuffix}` : ''), + description:(opt.dot?.readme?.description || ''), + tags:[], + }, + app:{ + image:opt.dot.image, + name:opt.dot.name, + version:(opt.input?.etc?.version || opt.dot?.semver?.version), + root:opt.dot.root, + UID:(opt.input?.etc?.uid || 1000), + GID:(opt.input?.etc?.gid || 1000), + no_cache:new Date().getTime(), + }, + cache:{ + registry:'localhost:5000/', + }, + tags:[], + }; + + docker.cache.name = `${docker.image.name}:${docker.image.prefix}buildcache${docker.image.suffix}`; + docker.cache.grype = `${docker.cache.registry}${docker.image.name}:${docker.image.prefix}grype${docker.image.suffix}`; + docker.app.prefix = docker.image.prefix; + docker.app.suffix = docker.image.suffix; + + // setup tags + if(!opt.dot?.semver?.disable?.rolling){ + docker.image.tags.push('rolling'); + } + if(opt.input?.etc?.dockerfile !== 'arch.dockerfile' && opt.input?.etc?.tag){ + docker.image.tags.push(`${context.sha.substring(0,7)}`); + docker.image.tags.push(opt.input.etc.tag); + docker.image.tags.push(`${opt.input.etc.tag}-${docker.app.version}`); + docker.cache.name = `${docker.image.name}:buildcache-${opt.input.etc.tag}`; + }else if(docker.app.version !== 'latest'){ + const semver = docker.app.version.split('.'); + docker.image.tags.push(`${context.sha.substring(0,7)}`); + if(Array.isArray(semver)){ + if(semver.length >= 1) docker.image.tags.push(`${semver[0]}`); + if(semver.length >= 2) docker.image.tags.push(`${semver[0]}.${semver[1]}`); + if(semver.length >= 3) docker.image.tags.push(`${semver[0]}.${semver[1]}.${semver[2]}`); + } + if(opt.dot?.semver?.stable && new RegExp(opt.dot?.semver.stable, 'ig').test(docker.image.tags.join(','))) docker.image.tags.push('stable'); + if(opt.dot?.semver?.latest && new RegExp(opt.dot?.semver.latest, 'ig').test(docker.image.tags.join(','))) docker.image.tags.push('latest'); + }else{ + docker.image.tags.push('latest'); + } + + for(const tag of docker.image.tags){ + docker.tags.push(`${docker.image.name}:${docker.image.prefix}${tag}${docker.image.suffix}`); + docker.tags.push(`ghcr.io/${docker.image.name}:${docker.image.prefix}${tag}${docker.image.suffix}`); + docker.tags.push(`quay.io/${docker.image.name}:${docker.image.prefix}${tag}${docker.image.suffix}`); + } + + // setup build arguments + if(opt.input?.etc?.build?.args){ + for(const arg in opt.input.etc.build.args){ + docker.app[arg] = opt.input.etc.build.args[arg]; + } + } + if(opt.dot?.build?.args){ + for(const arg in opt.dot.build.args){ + docker.app[arg] = opt.dot.build.args[arg]; + } + } + const arguments = []; + for(const argument in docker.app){ + arguments.push(`APP_${argument.toUpperCase()}=${docker.app[argument]}`); + } + + // export to environment + core.exportVariable('DOCKER_CACHE_REGISTRY', docker.cache.registry); + core.exportVariable('DOCKER_CACHE_NAME', docker.cache.name); + core.exportVariable('DOCKER_CACHE_GRYPE', docker.cache.grype); + + core.exportVariable('DOCKER_IMAGE_NAME', docker.image.name); + core.exportVariable('DOCKER_IMAGE_ARCH', docker.image.arch); + core.exportVariable('DOCKER_IMAGE_TAGS', docker.tags.join(',')); + core.exportVariable('DOCKER_IMAGE_DESCRIPTION', docker.image.description); + core.exportVariable('DOCKER_IMAGE_ARGUMENTS', arguments.join("\r\n")); + core.exportVariable('DOCKER_IMAGE_DOCKERFILE', opt.input?.etc?.dockerfile || 'arch.dockerfile'); + + core.exportVariable('WORKFLOW_BUILD', (opt.input?.build === undefined) ? false : opt.input.build); + core.exportVariable('WORKFLOW_CREATE_RELEASE', (opt.input?.release === undefined) ? false : opt.input.release); + core.exportVariable('WORKFLOW_CREATE_README', (opt.input?.readme === undefined) ? false : opt.input.readme); + core.exportVariable('WORKFLOW_GRYPE_FAIL_ON_SEVERITY', (opt.dot?.grype?.fail === undefined) ? true : opt.dot.grype.fail); + core.exportVariable('WORKFLOW_GRYPE_SEVERITY_CUTOFF', (opt.dot?.grype?.severity || 'high')); + if(opt.dot?.readme?.comparison){ + core.exportVariable('WORKFLOW_CREATE_COMPARISON', true); + core.exportVariable('WORKFLOW_CREATE_COMPARISON_FOREIGN_IMAGE', opt.dot.readme.comparison.image); + core.exportVariable('WORKFLOW_CREATE_COMPARISON_IMAGE', `${docker.image.name}:${docker.app.version}`); + } + + + + # DOCKER + - name: docker / login to hub + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 + with: + username: 11notes + password: ${{ secrets.DOCKER_TOKEN }} + + - name: github / login to ghcr + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 + with: + registry: ghcr.io + username: 11notes + password: ${{ secrets.GITHUB_TOKEN }} + + - name: quay / login to quay + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 + with: + registry: quay.io + username: 11notes+github + password: ${{ secrets.QUAY_TOKEN }} + + - name: docker / setup qemu + if: env.WORKFLOW_BUILD == 'true' + uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a + + - name: docker / setup buildx + if: env.WORKFLOW_BUILD == 'true' + uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 + with: + driver-opts: network=host + + - name: docker / build & push & tag grype + if: env.WORKFLOW_BUILD == 'true' + id: docker-build + uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d + with: + context: . + file: ${{ env.DOCKER_IMAGE_DOCKERFILE }} + push: true + platforms: ${{ env.DOCKER_IMAGE_ARCH }} + cache-from: type=registry,ref=${{ env.DOCKER_CACHE_NAME }} + cache-to: type=registry,ref=${{ env.DOCKER_CACHE_REGISTRY }}${{ env.DOCKER_CACHE_NAME }},mode=max,compression=zstd,force-compression=true + build-args: | + ${{ env.DOCKER_IMAGE_ARGUMENTS }} + tags: | + ${{ env.DOCKER_CACHE_GRYPE }} + + - name: grype / scan + if: env.WORKFLOW_BUILD == 'true' + id: grype + uses: anchore/scan-action@dc6246fcaf83ae86fcc6010b9824c30d7320729e + with: + image: ${{ env.DOCKER_CACHE_GRYPE }} + fail-build: ${{ env.WORKFLOW_GRYPE_FAIL_ON_SEVERITY }} + severity-cutoff: ${{ env.WORKFLOW_GRYPE_SEVERITY_CUTOFF }} + output-format: 'sarif' + by-cve: true + cache-db: true + + - name: grype / fail + if: env.WORKFLOW_BUILD == 'true' && (failure() || steps.grype.outcome == 'failure') + uses: anchore/scan-action@dc6246fcaf83ae86fcc6010b9824c30d7320729e + with: + image: ${{ env.DOCKER_CACHE_GRYPE }} + fail-build: false + severity-cutoff: ${{ env.WORKFLOW_GRYPE_SEVERITY_CUTOFF }} + output-format: 'table' + by-cve: true + cache-db: true + + - name: docker / build & push + if: env.WORKFLOW_BUILD == 'true' + uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d + with: + context: . + file: ${{ env.DOCKER_IMAGE_DOCKERFILE }} + push: true + sbom: true + provenance: mode=max + platforms: ${{ env.DOCKER_IMAGE_ARCH }} + cache-from: type=registry,ref=${{ env.DOCKER_CACHE_REGISTRY }}${{ env.DOCKER_CACHE_NAME }} + cache-to: type=registry,ref=${{ env.DOCKER_CACHE_NAME }},mode=max,compression=zstd,force-compression=true + build-args: | + ${{ env.DOCKER_IMAGE_ARGUMENTS }} + tags: | + ${{ env.DOCKER_IMAGE_TAGS }} + + + + # RELEASE + - name: github / release / log + continue-on-error: true + id: git-log + run: | + LOCAL_LAST_TAG=$(git describe --abbrev=0 --tags `git rev-list --tags --skip=1 --max-count=1`) + echo "using last tag: ${LOCAL_LAST_TAG}" + LOCAL_COMMITS=$(git log ${LOCAL_LAST_TAG}..HEAD --oneline) + + EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) + echo "commits<<${EOF}" >> ${GITHUB_OUTPUT} + echo "${LOCAL_COMMITS}" >> ${GITHUB_OUTPUT} + echo "${EOF}" >> ${GITHUB_OUTPUT} + + - name: github / release / markdown + if: env.WORKFLOW_CREATE_RELEASE == 'true' && steps.git-log.outcome == 'success' + id: git-release + uses: 11notes/action-docker-release@v1 + # WHY IS THIS ACTION NOT SHA256 PINNED? SECURITY MUCH?!?!?! + # --------------------------------------------------------------------------------- + # the next step "github / release / create" creates a new release based on the code + # in the repo. This code is not modified and can't be modified by this action. + # It does create the markdown for the release, which could be abused, but to what + # extend? Adding a link to a malicious repo? + with: + git_log: ${{ steps.git-log.outputs.commits }} + + - name: github / release / create + if: env.WORKFLOW_CREATE_RELEASE == 'true' && steps.git-release.outcome == 'success' + uses: actions/create-release@4c11c9fe1dcd9636620a16455165783b20fc7ea0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} + body: ${{ steps.git-release.outputs.release }} + draft: false + prerelease: false + + + + + # LICENSE + - name: license / update year + continue-on-error: true + uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298 + with: + script: | + const { existsSync, readFileSync, writeFileSync } = require('node:fs'); + const { resolve } = require('node:path'); + const file = 'LICENSE'; + const year = new Date().getFullYear(); + try{ + const path = resolve(file); + if(existsSync(path)){ + let license = readFileSync(file).toString(); + if(!new RegExp(`Copyright \\(c\\) ${year} 11notes`, 'i').test(license)){ + license = license.replace(/Copyright \(c\) \d{4} /i, `Copyright (c) ${new Date().getFullYear()} `); + writeFileSync(path, license); + } + }else{ + throw new Error(`file ${file} does not exist`); + } + }catch(e){ + core.setFailed(e); + } + + + + + # README + - name: github / checkout HEAD + continue-on-error: true + run: | + git checkout HEAD + + - name: docker / setup comparison images + if: env.WORKFLOW_CREATE_COMPARISON == 'true' + continue-on-error: true + run: | + docker image pull ${{ env.WORKFLOW_CREATE_COMPARISON_IMAGE }} + docker image ls --filter "reference=${{ env.WORKFLOW_CREATE_COMPARISON_IMAGE }}" --format json | jq --raw-output '.Size' &> ./comparison.size0.log + + docker image pull ${{ env.WORKFLOW_CREATE_COMPARISON_FOREIGN_IMAGE }} + docker image ls --filter "reference=${{ env.WORKFLOW_CREATE_COMPARISON_FOREIGN_IMAGE }}" --format json | jq --raw-output '.Size' &> ./comparison.size1.log + + docker run --entrypoint "/bin/sh" --rm ${{ env.WORKFLOW_CREATE_COMPARISON_FOREIGN_IMAGE }} -c id &> ./comparison.id.log + + - name: github / create README.md + id: github-readme + continue-on-error: true + if: env.WORKFLOW_CREATE_README == 'true' + uses: 11notes/action-docker-readme@v1 + # WHY IS THIS ACTION NOT SHA256 PINNED? SECURITY MUCH?!?!?! + # --------------------------------------------------------------------------------- + # the next step "github / commit & push" only adds the README and LICENSE as well as + # compose.yaml to the repository. This does not pose a security risk if this action + # would be compromised. The code of the app can't be changed by this action. Since + # only the files mentioned are commited to the repo. Sure, someone could make a bad + # compose.yaml, but since this serves only as an example I see no harm in that. + with: + sarif_file: ${{ steps.grype.outputs.sarif }} + build_output_metadata: ${{ steps.docker-build.outputs.metadata }} + + - name: docker / push README.md to docker hub + continue-on-error: true + if: steps.github-readme.outcome == 'success' && hashFiles('README_NONGITHUB.md') != '' + uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 + env: + DOCKER_USER: 11notes + DOCKER_PASS: ${{ secrets.DOCKER_TOKEN }} + with: + destination_container_repo: ${{ env.DOCKER_IMAGE_NAME }} + provider: dockerhub + short_description: ${{ env.DOCKER_IMAGE_DESCRIPTION }} + readme_file: 'README_NONGITHUB.md' + + - name: github / commit & push + continue-on-error: true + if: steps.github-readme.outcome == 'success' && hashFiles('README.md') != '' + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add README.md + if [ -f compose.yaml ]; then + git add compose.yaml + fi + if [ -f LICENSE ]; then + git add LICENSE + fi + git commit -m "github-actions[bot]: update README.md" + git push origin HEAD:master + + + + + # REPOSITORY SETTINGS + - name: github / update description and set repo defaults + run: | + curl --request PATCH \ + --url https://api.github.com/repos/${{ github.repository }} \ + --header 'authorization: Bearer ${{ secrets.REPOSITORY_TOKEN }}' \ + --header 'content-type: application/json' \ + --data '{ + "description":"${{ env.DOCKER_IMAGE_DESCRIPTION }}", + "homepage":"", + "has_issues":true, + "has_discussions":true, + "has_projects":false, + "has_wiki":false + }' \ + --fail \ No newline at end of file diff --git a/.github/workflows/readme.yml b/.github/workflows/readme.yml new file mode 100644 index 0000000..068edce --- /dev/null +++ b/.github/workflows/readme.yml @@ -0,0 +1,16 @@ +name: readme + +on: + workflow_dispatch: + +jobs: + readme: + runs-on: ubuntu-latest + steps: + - name: update README.md + uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 + with: + wait-for-completion: false + workflow: docker.yml + token: "${{ secrets.REPOSITORY_TOKEN }}" + inputs: '{ "build":"false", "release":"false", "readme":"true" }' \ No newline at end of file diff --git a/.github/workflows/tags.yml b/.github/workflows/tags.yml new file mode 100644 index 0000000..7085726 --- /dev/null +++ b/.github/workflows/tags.yml @@ -0,0 +1,102 @@ +name: tags +on: + push: + tags: + - 'v*' +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: build docker image + uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 + with: + workflow: docker.yml + token: "${{ secrets.REPOSITORY_TOKEN }}" + inputs: '{ "release":"true", "readme":"true" }' + + docker-unraid: + runs-on: ubuntu-latest + steps: + - name: init / base64 nested json + uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298 + with: + script: | + const { Buffer } = require('node:buffer'); + const etc = { + semversuffix:"unraid", + uid:99, + gid:100, + }; + core.exportVariable('WORKFLOW_BASE64JSON', Buffer.from(JSON.stringify(etc)).toString('base64')); + + - name: build docker image for unraid community + uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 + with: + workflow: docker.yml + token: "${{ secrets.REPOSITORY_TOKEN }}" + inputs: '{ "release":"false", "readme":"false", "run-name":"unraid", "etc":"${{ env.WORKFLOW_BASE64JSON }}" }' + + kms-gui: + runs-on: ubuntu-latest + needs: docker + steps: + - name: init / base64 nested json + uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298 + with: + script: | + const { Buffer } = require('node:buffer'); + (async()=>{ + try{ + const master = await fetch('https://raw.githubusercontent.com/11notes/docker-kms/refs/heads/master/.json'); + const dot = await master.json(); + const etc = { + version:dot.semver.version, + }; + core.exportVariable('WORKFLOW_BASE64JSON', Buffer.from(JSON.stringify(etc)).toString('base64')); + }catch(e){ + core.setFailed(`workflow failed: ${e}`); + } + })(); + + - name: build downstream kms gui + uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 + with: + workflow: docker.yml + token: "${{ secrets.REPOSITORY_TOKEN }}" + repo: 11notes/docker-kms-gui + ref: master + inputs: '{ "release":"false", "readme":"true", "etc":"${{ env.WORKFLOW_BASE64JSON }}" }' + + kms-gui-unraid: + runs-on: ubuntu-latest + needs: docker-unraid + steps: + - name: init / base64 nested json + uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298 + with: + script: | + const { Buffer } = require('node:buffer'); + (async()=>{ + try{ + const master = await fetch('https://raw.githubusercontent.com/11notes/docker-kms/refs/heads/master/.json'); + const dot = await master.json(); + const etc = { + version:dot.semver.version, + semversuffix:"unraid", + uid:99, + gid:100, + }; + core.exportVariable('WORKFLOW_BASE64JSON', Buffer.from(JSON.stringify(etc)).toString('base64')); + }catch(e){ + core.setFailed(`workflow failed: ${e}`); + } + })(); + + - name: build downstream kms gui for unraid community + uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 + with: + workflow: docker.yml + token: "${{ secrets.REPOSITORY_TOKEN }}" + repo: 11notes/docker-kms-gui + ref: master + inputs: '{ "release":"false", "readme":"false", "run-name":"unraid", "etc":"${{ env.WORKFLOW_BASE64JSON }}" }' \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7bfecea --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# default +maintain/ +node_modules/ + +# custom +.env \ No newline at end of file diff --git a/.json b/.json new file mode 100644 index 0000000..ae8bd3f --- /dev/null +++ b/.json @@ -0,0 +1,17 @@ +{ + "image":"11notes/netbird", + "name":"netbird", + "root":"/netbird", + "arch":"linux/amd64,linux/arm64,linux/arm/v7", + + "semver":{ + "version":"1.0.2" + }, + + "readme":{ + "description":"Activate any version of Windows and Office, forever", + "built":{ + "netbirdio/netbird":"https://github.com/netbirdio/netbird" + } + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dbae36a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 11notes + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/arch.dockerfile b/arch.dockerfile new file mode 100644 index 0000000..a6dd059 --- /dev/null +++ b/arch.dockerfile @@ -0,0 +1,151 @@ +# ╔═════════════════════════════════════════════════════╗ +# ║ SETUP ║ +# ╚═════════════════════════════════════════════════════╝ + # GLOBAL + ARG APP_UID=1000 \ + APP_GID=1000 \ + BUILD_ROOT="/go/netbird/management /go/netbird/relay /go/netbird/signal" + + # :: FOREIGN IMAGES + FROM 11notes/nginx:stable AS distroless-nginx + FROM 11notes/distroless:curl AS distroless-curl + FROM 11notes/util AS util + +# ╔═════════════════════════════════════════════════════╗ +# ║ BUILD ║ +# ╚═════════════════════════════════════════════════════╝ + # :: netbird + FROM golang:1.24-alpine AS build + COPY --from=util /usr/local/bin /usr/local/bin + ARG APP_VERSION \ + BUILD_ROOT \ + BUILD_BIN + + ENV CGO_ENABLED=0 + + RUN set -ex; \ + apk --update --no-cache add \ + build-base \ + upx \ + git; + + RUN set -ex; \ + git clone https://github.com/netbirdio/netbird -b v${APP_VERSION}; + + RUN set -ex; \ + mkdir -p /distroless/usr/local/bin; \ + for BUILD in ${BUILD_ROOT}; do \ + cd ${BUILD}; \ + BIN="${BUILD}/$(echo ${BUILD} | awk -F '/' '{print $4}')"; \ + go build -ldflags="-extldflags=-static" -o ${BIN} main.go; \ + eleven checkStatic ${BIN}; \ + eleven strip ${BIN}; \ + cp ${BIN} /distroless/usr/local/bin; \ + done; \ + mv /distroless/usr/local/bin/management /distroless/usr/local/bin/netbird; + + # :: management + FROM golang:1.24-alpine AS management + COPY --from=util /usr/local/bin /usr/local/bin + COPY ./build/go/management /go/management + ENV CGO_ENABLED=0 + ARG BIN=/go/management/management + + RUN set -ex; \ + apk --update --no-cache add \ + build-base \ + upx; + + RUN set -ex; \ + cd /go/management; \ + go build -ldflags="-extldflags=-static" -o ${BIN} main.go; \ + mkdir -p /distroless/usr/local/bin; \ + eleven checkStatic ${BIN}; \ + eleven strip ${BIN}; \ + cp ${BIN} /distroless/usr/local/bin; + + # :: dashboard + FROM golang:1.24-alpine AS dashboard + COPY --from=util /usr/local/bin /usr/local/bin + COPY ./build/go/dashboard /go/dashboard + ENV CGO_ENABLED=0 + ARG BIN=/go/dashboard/dashboard + + RUN set -ex; \ + apk --update --no-cache add \ + build-base \ + upx \ + git \ + nodejs \ + npm; + + RUN set -ex; \ + cd /go/dashboard; \ + go build -ldflags="-extldflags=-static" -o ${BIN} main.go; \ + mkdir -p /distroless/usr/local/bin; \ + eleven checkStatic ${BIN}; \ + eleven strip ${BIN}; \ + cp ${BIN} /distroless/usr/local/bin; + + RUN set -ex; \ + git clone https://github.com/netbirdio/dashboard /dashboard; + + RUN set -ex; \ + cd /dashboard; \ + npm i --save; \ + echo '{}' > .local-config.json; \ + npm run build; \ + mkdir -p /distroless/nginx/var; \ + cp -R ./out/* /distroless/nginx/var; + + # :: file system + FROM alpine AS file-system + COPY --from=util /usr/local/bin /usr/local/bin + ARG APP_ROOT + USER root + + RUN set -ex; \ + eleven mkdir /distroless${APP_ROOT}/{etc,var}; \ + mkdir -p /distroless/var/lib; \ + ln -sf ${APP_ROOT}/var /distroless/var/lib/netbird; + + +# ╔═════════════════════════════════════════════════════╗ +# ║ IMAGE ║ +# ╚═════════════════════════════════════════════════════╝ + # :: HEADER + FROM scratch + + # :: default arguments + ARG TARGETPLATFORM \ + TARGETOS \ + TARGETARCH \ + TARGETVARIANT \ + APP_IMAGE \ + APP_NAME \ + APP_VERSION \ + APP_ROOT \ + APP_UID \ + APP_GID \ + APP_NO_CACHE + + # :: default environment + ENV APP_IMAGE=${APP_IMAGE} \ + APP_NAME=${APP_NAME} \ + APP_VERSION=${APP_VERSION} \ + APP_ROOT=${APP_ROOT} + + # :: multi-stage + COPY --from=build /distroless/ / + COPY --from=dashboard --chown=${APP_UID}:${APP_GID} /distroless/ / + COPY --from=management --chown=${APP_UID}:${APP_GID} /distroless/ / + COPY --from=distroless-nginx --chown=${APP_UID}:${APP_GID} / / + COPY --from=file-system --chown=${APP_UID}:${APP_GID} /distroless/ / + COPY --from=distroless-curl /usr/local/bin /usr/local/bin + COPY --chown=${APP_UID}:${APP_GID} ./rootfs/ / + +# :: PERSISTENT DATA + VOLUME ["${APP_ROOT}/etc", "${APP_ROOT}/var"] + +# :: EXECUTE + USER ${APP_UID}:${APP_GID} \ No newline at end of file diff --git a/build/go/dashboard/go.mod b/build/go/dashboard/go.mod new file mode 100644 index 0000000..af2f5a8 --- /dev/null +++ b/build/go/dashboard/go.mod @@ -0,0 +1,2 @@ +module github.com/11notes/docker-netbird +go 1.24 \ No newline at end of file diff --git a/build/go/dashboard/go.sum b/build/go/dashboard/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/build/go/dashboard/main.go b/build/go/dashboard/main.go new file mode 100644 index 0000000..77d4d17 --- /dev/null +++ b/build/go/dashboard/main.go @@ -0,0 +1,91 @@ +package main + +import ( + "fmt" + "io/ioutil" + "strings" + "path/filepath" + "io" + "os" + "regexp" + "syscall" + "time" +) + +const TrustedDomainsTemplate = "/nginx/var/OidcTrustedDomains.js.tmpl" +const TrustedDomains = "/nginx/var/OidcTrustedDomains.js" + +func logInfo(s string){ + log(os.Stdout, fmt.Sprintf("INFO %s", s)) +} + +func logError(s string){ + log(os.Stderr, fmt.Sprintf("ERROR %s", s)) +} + +func log(r io.Writer, s string) { + fmt.Fprintf(r, "%s %s\n", time.Now().Format(time.RFC3339), s) +} + +func main() { + // find all the files that contain AUTH_SUPPORTED_SCOPES + err := filepath.Walk("/nginx/var", + func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + text, _ := ioutil.ReadFile(path) + if strings.Contains(string(text), "AUTH_SUPPORTED_SCOPES") { + replaceEnv(path, string(text)) + } + return nil + }) + if err != nil { + logError(fmt.Sprintf("filepath.Walk(\"/nginx/var\"): %s", err)) + os.Exit(1) + } + + // replace custom files + custom := []string{TrustedDomainsTemplate} + for _, path := range custom { + text, _ := ioutil.ReadFile(path) + replaceEnv(path, string(text)) + } + + // rename tmpl + os.Rename(TrustedDomainsTemplate, TrustedDomains) + + logInfo("starting dashboard") + if err = syscall.Exec("/usr/local/bin/nginx", []string{}, os.Environ()); err != nil { + os.Exit(1) + } +} + +func replaceEnv(path string, text string){ + nextjs := []string{"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"} + + // replace all environment variables in file + for _, e := range os.Environ() { + key := strings.Split(e, "=")[0] + value := os.Getenv(key) + text = string(regexp.MustCompile(fmt.Sprintf(`\$%s`, key)).ReplaceAllString(text, value)) + } + + // replace all not set environment variables in file + for _, e := range nextjs { + text = string(regexp.MustCompile(fmt.Sprintf(`\$%s`, e)).ReplaceAllString(text, "")) + } + + err := ioutil.WriteFile(path, []byte(text), os.ModePerm) + if err != nil { + logError(fmt.Sprintf("ioutil.WriteFile(%s): %s", path, err)) + os.Exit(3) + } +} + +func getEnv(key, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} \ No newline at end of file diff --git a/build/go/management/go.mod b/build/go/management/go.mod new file mode 100644 index 0000000..af2f5a8 --- /dev/null +++ b/build/go/management/go.mod @@ -0,0 +1,2 @@ +module github.com/11notes/docker-netbird +go 1.24 \ No newline at end of file diff --git a/build/go/management/go.sum b/build/go/management/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/build/go/management/main.go b/build/go/management/main.go new file mode 100644 index 0000000..972a917 --- /dev/null +++ b/build/go/management/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "fmt" + "io/ioutil" + "strings" + "os" + "regexp" + "syscall" +) + +func main() { + custom := []string{"/netbird/etc/management.json"} + for _, path := range custom { + text, _ := ioutil.ReadFile(path) + replaceEnv(path, string(text)) + } + if err := syscall.Exec("/usr/local/bin/netbird", []string{"management", "management", "--config", "/netbird/etc/management.json", "--log-file", "console", "--log-level", "info", "--disable-anonymous-metrics", "--disable-geolite-update"}, os.Environ()); err != nil { + os.Exit(1) + } +} + +func replaceEnv(path string, text string){ + // replace all environment variables in file + for _, e := range os.Environ() { + key := strings.Split(e, "=")[0] + value := os.Getenv(key) + text = string(regexp.MustCompile(fmt.Sprintf(`\${%s}`, key)).ReplaceAllString(text, value)) + } + + // replace all not set environment variables in file + uenv := regexp.MustCompile(`\$\{[A-Z_a-z]+\}`).FindAllString(text, -1) + for _, e := range uenv { + fmt.Printf("variable %s not set, will be set to empty string!\n", e) + text = string(regexp.MustCompile(fmt.Sprintf(`%s`, e)).ReplaceAllString(text, "")) + } + + err := ioutil.WriteFile(path, []byte(text), os.ModePerm) + if err != nil { + os.Exit(3) + } +} + +func getEnv(key, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} \ No newline at end of file diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..91d9450 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,142 @@ +name: "netbird" +services: + db: + image: "11notes/postgres:16" + read_only: true + environment: + TZ: "Europe/Zurich" + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - "db.etc:/postgres/etc" + - "db.var:/postgres/var" + - "db.backup:/postgres/backup" + - "db.cmd:/run/cmd" + tmpfs: + - "/run/postgresql:uid=1000,gid=1000" + - "/postgres/log:uid=1000,gid=1000" + networks: + backend: + restart: "always" + + cron: + depends_on: + db: + condition: "service_healthy" + restart: true + image: "11notes/cron:4.6" + environment: + TZ: "Europe/Zurich" + CRONTAB: |- + 0 3 * * * cmd-socket '{"bin":"backup"}' > /proc/1/fd/1 + volumes: + - "db.cmd:/run/cmd" + restart: "always" + + dashboard: + image: "11notes/netbird:0.46.0" + read_only: true + environment: + NETBIRD_MGMT_API_ENDPOINT: "https://${NETBIRD_FQDN}" + NETBIRD_MGMT_GRPC_API_ENDPOINT: "https://${NETBIRD_FQDN}" + AUTH_AUDIENCE: "netbird-client" + AUTH_CLIENT_ID: "netbird-client" + AUTH_CLIENT_SECRET: + AUTH_AUTHORITY: "https://${KEYCLOAK_FQDN}/realms/${KEYCLOAK_REALM}" + USE_AUTH0: false + AUTH_SUPPORTED_SCOPES: "openid" + NETBIRD_TOKEN_SOURCE: "accessToken" + entrypoint: ["/usr/local/bin/dashboard"] + volumes: + - "dashboard.var:/nginx/var" + tmpfs: + - "/nginx/cache:uid=1000,gid=1000" + - "/nginx/run:uid=1000,gid=1000" + networks: + frontend: + ports: + - "3000:3000/tcp" + healthcheck: + test: ["CMD", "/usr/local/bin/curl", "-kILs", "--fail", "http://localhost:3000/ping"] + interval: 5s + timeout: 2s + start_period: 5s + restart: "always" + + management: + depends_on: + db: + condition: "service_healthy" + restart: true + image: "11notes/netbird:0.46.0" + read_only: true + env_file: '.env' + environment: + TZ: "Europe/Zurich" + NETBIRD_STORE_ENGINE_POSTGRES_DSN: "host=db user=postgres password=${POSTGRES_PASSWORD} dbname=postgres port=5432" + NB_ACTIVITY_EVENT_STORE_ENGINE: "postgres" + NB_ACTIVITY_EVENT_POSTGRES_DSN: "host=db user=postgres password=${POSTGRES_PASSWORD} dbname=postgres port=5432" + entrypoint: ["/usr/local/bin/management"] + volumes: + - "management.etc:/netbird/etc" + - "management.var:/netbird/var" + networks: + frontend: + backend: + ports: + - "3080:80/tcp" + - "33073:33073/tcp" + sysctls: + net.ipv4.ip_unprivileged_port_start: 80 + healthcheck: + test: ["CMD", "/usr/local/bin/curl", "-kILs", "--fail", "http://localhost:9090/metrics"] + interval: 5s + timeout: 2s + start_period: 5s + restart: "always" + + signal: + image: "11notes/netbird:0.46.0" + environment: + TZ: "Europe/Zurich" + entrypoint: ["/usr/local/bin/signal"] + command: [ + "run", + "--log-file", "console", + "--log-level", "info" + ] + volumes: + - "signal.var:/netbird/var" + networks: + frontend: + ports: + - "10000:10000/tcp" + restart: "always" + + relay: + image: "11notes/netbird:0.46.0" + environment: + TZ: "Europe/Zurich" + NB_LISTEN_ADDRESS: ":33080" + NB_EXPOSED_ADDRESS: "rels://${NETBIRD_FQDN}:443" + NB_AUTH_SECRET: ${NETBIRD_RELAY_SECRET} + entrypoint: ["/usr/local/bin/relay"] + networks: + frontend: + ports: + - "33080:33080/tcp" + restart: "always" + +volumes: + management.etc: + management.var: + dashboard.var: + signal.var: + db.etc: + db.var: + db.backup: + db.cmd: + +networks: + frontend: + backend: + internal: true \ No newline at end of file diff --git a/project.md b/project.md new file mode 100644 index 0000000..8d66b8d --- /dev/null +++ b/project.md @@ -0,0 +1,64 @@ +${{ content_synopsis }} This image will run netbird from a single image (not multiple) rootless and distroless for more security. Due to the nature of a single image and not multiple, you see in the [compose.yaml](https://github.com/11notes/docker-netbird/blob/master/compose.yaml) example that an ```entrypoint:``` has been defined. This image also needs some environment variables present in your **.env** file. This image's defaults (management.json) as well as the example **.env** are to be used with Keycloak as your IdP. You can however provide your own **management.json** file and use any IdP you like. + +${{ github:> [!IMPORTANT] }} +${{ github:> }}* This image runs as 1000:1000 by default, most other images run everything as root +${{ github:> }}* This image has no shell since it is distroless, most other images run on a distro like Debian or Alpine with full shell access (security) +${{ github:> }}* This image does not ship with any critical or high rated CVE and is automatically maintained via CI/CD, most other images mostly have no CVE scanning or code quality tools in place +${{ github:> }}* This image is created via a secure, pinned CI/CD process and immune to upstream attacks, most other images have upstream dependencies that can be exploited +${{ github:> }}* This image works as read-only, most other images need to write files to the image filesystem +${{ github:> }}* This image is a lot smaller than most other images + +If you value security, simplicity and the ability to interact with the maintainer and developer of an image. Using my images is a great start in that direction. + +# COMPARISON 🏁 +Below you find a comparison between this image and the most used or original one. + +| **image** | 11notes/netbird | netbirdio/* | +| ---: | :---: | :---: | +| **image size on disk** | 44.6MB | 377.9MB | +| **process UID/GID** | 1000/1000 | 0/0 | +| **distroless?** | ✅ | ❌ | +| **rootless?** | ✅ | ❌ | + +${{ title_volumes }} +* **${{ json_root }}/etc** - Directory of your management.json config +* **${{ json_root }}/var** - Directory of dynamic data from differnet init systems (relay, signal, management) + +# EXAMPLE ENV FILE 📑 +```ini +# postgres settings +POSTGRES_PASSWORD= + +# netbird settings +NETBIRD_RELAY_SECRET= +NETBIRD_DATASTORE_ENCRYPTION_KEY= +NETBIRD_FQDN=netbird.domain.com + +# Keycloak settings +KEYCLOAK_FQDN=keycloak.domain.com +KEYCLOAK_REALM=netbird +KEYCLOAK_CLIENT_SECRET= + +# STUN/TURN configuration +STUN_FQDN_AND_PORT=turn.domain.com:5349 +TURN_FQDN_AND_PORT=turn.domain.com:5349 +TURN_SECRET= +``` + +${{ content_compose }} + +${{ content_defaults }} + +${{ content_environment }} + +${{ content_source }} + +${{ content_parent }} + +${{ content_built }} + +${{ content_tips }} + +${{ title_caution }} +${{ github:> [!CAUTION] }} +${{ github:> }}* Because this image is distroless, it only works with PostgreSQL, not SQLite. The GeoLocation middleware is also disabled because of this! \ No newline at end of file diff --git a/rootfs/netbird/etc/management.json b/rootfs/netbird/etc/management.json new file mode 100644 index 0000000..476ae24 --- /dev/null +++ b/rootfs/netbird/etc/management.json @@ -0,0 +1,98 @@ +{ + "Stuns": [ + { + "Proto": "udp", + "URI": "stun:${STUN_FQDN_AND_PORT}", + "Secret": "$STUN_SECRET" + } + ], + "TURNConfig": { + "Turns": [ + { + "Proto": "udp", + "URI": "turn:${TURN_FQDN_AND_PORT}" + } + ], + "CredentialsTTL": "12h", + "Secret": "${TURN_SECRET}", + "TimeBasedCredentials": false + }, + "Relay": { + "Addresses": ["rels://${NETBIRD_FQDN}:443/relay"], + "CredentialsTTL": "24h", + "Secret": "${NETBIRD_RELAY_SECRET}" + }, + "Signal": { + "Proto": "https", + "URI": "${NETBIRD_FQDN}:443", + "Username": "", + "Password": null + }, + "ReverseProxy": { + "TrustedHTTPProxies": [], + "TrustedHTTPProxiesCount": 0, + "TrustedPeers": [ + "0.0.0.0/0" + ] + }, + "Datadir": "/netbird/etc", + "DataStoreEncryptionKey": "${NETBIRD_DATASTORE_ENCRYPTION_KEY}", + "StoreConfig": { + "Engine": "postgres" + }, + "HttpConfig": { + "Address": "0.0.0.0:33073", + "AuthIssuer": "https://${KEYCLOAK_FQDN}/realms/${KEYCLOAK_REALM}", + "AuthAudience": "netbird-client", + "AuthKeysLocation": "https://${KEYCLOAK_FQDN}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/certs", + "AuthUserIDClaim": "", + "IdpSignKeyRefreshEnabled": false, + "OIDCConfigEndpoint":"https://${KEYCLOAK_FQDN}/realms/${KEYCLOAK_REALM}/.well-known/openid-configuration" + }, + "IdpManagerConfig": { + "ManagerType": "keycloak", + "ClientConfig": { + "ClientID": "netbird-backend", + "ClientSecret": "${KEYCLOAK_CLIENT_SECRET}", + "TokenEndpoint": "https://${KEYCLOAK_FQDN}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token", + "GrantType": "client_credentials" + }, + "ExtraConfig": { + "AdminEndpoint": "https://${KEYCLOAK_FQDN}/admin/realms/${KEYCLOAK_REALM}", + "Auth0ClientCredentials": null, + "AzureClientCredentials": null, + "KeycloakClientCredentials": null, + "ZitadelClientCredentials": null + }, + "DeviceAuthorizationFlow": { + "Provider": "hosted", + "ProviderConfig": { + "Audience": "netbird-client", + "AuthorizationEndpoint": "", + "Domain": "", + "ClientID": "netbird-client", + "ClientSecret": "", + "TokenEndpoint": "https://${KEYCLOAK_FQDN}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token", + "DeviceAuthEndpoint": "https://${KEYCLOAK_FQDN}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/auth/device", + "Scope": "openid", + "UseIDToken": false, + "RedirectURLs": null + } + } + }, + "PKCEAuthorizationFlow": { + "ProviderConfig": { + "Audience": "netbird-client", + "ClientID": "netbird-client", + "ClientSecret": "", + "Domain": "", + "AuthorizationEndpoint": "https://${KEYCLOAK_FQDN}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/auth", + "TokenEndpoint": "https://${KEYCLOAK_FQDN}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token", + "Scope": "openid", + "RedirectURLs": ["http://localhost:53000"], + "UseIDToken": false, + "DisablePromptLogin": false, + "LoginFlag": 1 + } + } +} \ No newline at end of file diff --git a/rootfs/nginx/etc/default.conf b/rootfs/nginx/etc/default.conf new file mode 100644 index 0000000..522c390 --- /dev/null +++ b/rootfs/nginx/etc/default.conf @@ -0,0 +1,21 @@ +server { + listen 3000 default_server; + root /nginx/var; + + location / { + try_files $uri $uri.html $uri/ =404; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + 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"; + expires off; + } + + location /ping { + return 200; + } +} \ No newline at end of file