diff --git a/compose/openbao.yml b/compose/openbao.yml new file mode 100644 index 0000000..c440175 --- /dev/null +++ b/compose/openbao.yml @@ -0,0 +1,60 @@ +services: + core: + depends_on: + openbao-init: + condition: service_completed_successfully + + celery: + depends_on: + openbao-init: + condition: service_completed_successfully + + openbao: + image: ${OPENBAO_IMAGE:-openbao/openbao:2.6.0} + container_name: jms_openbao + hostname: openbao + # Start the image entrypoint as root so it can fix bind-mount ownership; + # the official entrypoint then launches bao via su-exec as openbao. + user: "0" + restart: always + command: server -config=/openbao/config/server.hcl + environment: + BAO_ADDR: http://127.0.0.1:8200 + VAULT_API_ADDR: ${OPENBAO_RAFT_API_ADDR:-http://openbao:8200} + VAULT_CLUSTER_ADDR: ${OPENBAO_RAFT_CLUSTER_ADDR:-http://openbao:8201} + VAULT_RAFT_NODE_ID: ${OPENBAO_RAFT_NODE_ID:-openbao} + TZ: ${TZ:-Asia/Shanghai} + volumes: + - ${CONFIG_DIR}/openbao/server.hcl:/openbao/config/server.hcl:ro + - ${VOLUME_DIR}/openbao/data:/openbao/file + ports: + - "${OPENBAO_UI_BIND:-127.0.0.1}:${OPENBAO_UI_PORT:-8200}:8200" + - "${OPENBAO_CLUSTER_BIND:-127.0.0.1}:${OPENBAO_CLUSTER_PORT:-8201}:8201" + healthcheck: + test: "bao status -address=http://127.0.0.1:8200" + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + networks: + - net + + openbao-init: + image: ${OPENBAO_IMAGE:-openbao/openbao:2.6.0} + container_name: jms_openbao_init + hostname: openbao-init + # The one-shot initializer writes root-owned 0600 unseal material. + user: "0" + restart: "no" + command: sh /openbao/bootstrap/bootstrap.sh + env_file: + - ${CONFIG_FILE} + environment: + BAO_ADDR: http://openbao:8200 + TZ: ${TZ:-Asia/Shanghai} + volumes: + - ${CONFIG_DIR}/openbao:/openbao/bootstrap + depends_on: + - openbao + networks: + - net diff --git a/config-example.txt b/config-example.txt index 3b8a2b1..a413d9c 100644 --- a/config-example.txt +++ b/config-example.txt @@ -162,3 +162,36 @@ USE_LB=1 # TZ=Asia/Shanghai CURRENT_VERSION= + +################################# Vault Configuration ################################# +# OpenBao runs as an independent Docker service in this installer. JumpServer connects to OpenBao Server directly. +# OpenBao 作为独立 Docker 服务部署,JumpServer 直接访问 OpenBao Server。 +# +# Disabled by default. Set to true to enable Vault storage. When the backend is +# openbao and OPENBAO_EXTERNAL=false, the installer starts the built-in service. +# 默认关闭;设为 true 后启用 Vault。当后端为 openbao 且 OPENBAO_EXTERNAL=false 时,安装器启动内置服务。 +VAULT_ENABLED=false +VAULT_BACKEND=openbao +VAULT_OPENBAO_ADDR=http://openbao:8200 +VAULT_OPENBAO_MOUNT_POINT=pam +VAULT_OPENBAO_TOKEN= +VAULT_OPENBAO_TIMEOUT=10 + +# Set to true when VAULT_OPENBAO_ADDR points to an external OpenBao cluster or HA endpoint. +# 设置为 true 时,installer 不会启动内置 OpenBao,Core 仍使用上面的 Vault 配置连接外部服务。 +OPENBAO_EXTERNAL=false + +OPENBAO_RAFT_NODE_ID=openbao +OPENBAO_RAFT_API_ADDR=http://openbao:8200 +OPENBAO_RAFT_CLUSTER_ADDR=http://openbao:8201 +OPENBAO_RAFT_BOOTSTRAP=true +# Additional Raft nodes must set OPENBAO_RAFT_BOOTSTRAP=false and receive a +# protected copy of openbao/init.json from the bootstrap node before startup. +# OPENBAO_RAFT_RETRY_JOIN=http://openbao-1:8200,http://openbao-2:8200 + +OPENBAO_UNSEAL_KEY_SHARES=5 +OPENBAO_UNSEAL_KEY_THRESHOLD=3 +OPENBAO_UI_BIND=127.0.0.1 +OPENBAO_UI_PORT=8200 +OPENBAO_CLUSTER_BIND=127.0.0.1 +OPENBAO_CLUSTER_PORT=8201 diff --git a/config_init/openbao/bootstrap.sh b/config_init/openbao/bootstrap.sh new file mode 100644 index 0000000..e3a6f36 --- /dev/null +++ b/config_init/openbao/bootstrap.sh @@ -0,0 +1,162 @@ +#!/bin/sh +set -eu + +export BAO_ADDR="${BAO_ADDR:-http://openbao:8200}" + +MOUNT_POINT="${VAULT_OPENBAO_MOUNT_POINT:-pam}" +SERVICE_TOKEN="${VAULT_OPENBAO_TOKEN:-}" +RAFT_BOOTSTRAP="${OPENBAO_RAFT_BOOTSTRAP:-true}" +UNSEAL_KEY_SHARES="${OPENBAO_UNSEAL_KEY_SHARES:-5}" +UNSEAL_KEY_THRESHOLD="${OPENBAO_UNSEAL_KEY_THRESHOLD:-3}" +INIT_FILE="/openbao/bootstrap/init.json" +POLICY_FILE="/tmp/jumpserver-policy.hcl" +SERVICE_TOKEN_FILE="/openbao/bootstrap/jumpserver-token.json" + +wait_openbao() { + i=0 + while [ "$i" -lt 60 ]; do + if bao status >/tmp/openbao-status 2>&1; then + return 0 + fi + if grep -q "Initialized" /tmp/openbao-status 2>/dev/null; then + return 0 + fi + i=$((i + 1)) + sleep 1 + done + cat /tmp/openbao-status 2>/dev/null || true + echo "OpenBao is not reachable" + exit 1 +} + +json_value() { + key="$1" + tr -d '\n ' <"${INIT_FILE}" | sed -n "s/.*\"${key}\":\"\\([^\"]*\\)\".*/\\1/p" +} + +json_array_first() { + key="$1" + tr -d '\n ' <"${INIT_FILE}" | sed -n "s/.*\"${key}\":\\[\"\\([^\"]*\\)\".*/\\1/p" +} + +json_array_values() { + key="$1" + tr -d '\n ' <"${INIT_FILE}" | sed -n "s/.*\"${key}\":\\[\\([^]]*\\)\\].*/\\1/p" | tr ',' '\n' | sed 's/^"//;s/"$//' +} + +is_true() { + case "$1" in + 1|true|True|TRUE|yes|Yes|YES) return 0 ;; + *) return 1 ;; + esac +} + +is_initialized() { + bao status 2>/dev/null | grep -q "Initialized[[:space:]]*true" +} + +is_uninitialized() { + bao status 2>/dev/null | grep -q "Initialized[[:space:]]*false" +} + +is_sealed() { + bao status 2>/dev/null | grep -q "Sealed[[:space:]]*true" +} + +wait_unsealed() { + i=0 + while [ "$i" -lt 30 ]; do + if ! is_sealed; then + return 0 + fi + i=$((i + 1)) + sleep 1 + done + return 1 +} + +unseal_openbao() { + if ! is_sealed; then + return 0 + fi + + json_array_values unseal_keys_b64 | while IFS= read -r key; do + [ -z "${key}" ] && continue + if ! is_sealed; then + break + fi + bao operator unseal "${key}" >/dev/null + done + + if ! wait_unsealed; then + echo "OpenBao is still sealed after applying unseal keys from ${INIT_FILE}." + exit 1 + fi +} + +wait_openbao + +if is_uninitialized; then + if is_true "${RAFT_BOOTSTRAP}"; then + bao operator init -key-shares="${UNSEAL_KEY_SHARES}" -key-threshold="${UNSEAL_KEY_THRESHOLD}" -format=json >"${INIT_FILE}" + chmod 600 "${INIT_FILE}" 2>/dev/null || true + else + i=0 + while [ "$i" -lt 60 ]; do + is_initialized && break + i=$((i + 1)) + sleep 1 + done + if is_uninitialized; then + echo "OpenBao is not initialized. Set OPENBAO_RAFT_BOOTSTRAP=true on the first Raft node, or wait for retry_join to finish." + exit 1 + fi + fi +fi + +if [ ! -f "${INIT_FILE}" ]; then + echo "OpenBao is initialized, but ${INIT_FILE} is missing; cannot unseal automatically." + echo "On an additional Raft node, copy init.json from the bootstrap node to this node before startup." + exit 1 +fi + +ROOT_TOKEN="$(json_value root_token)" + +if [ -z "${ROOT_TOKEN}" ] || [ -z "$(json_array_first unseal_keys_b64)" ]; then + echo "Invalid OpenBao initialization file: ${INIT_FILE}" + exit 1 +fi + +unseal_openbao + +export BAO_TOKEN="${ROOT_TOKEN}" + +if ! bao secrets list -format=json | grep -q "\"${MOUNT_POINT}/\""; then + bao secrets enable -path="${MOUNT_POINT}" -version=2 kv +fi + +bao write "${MOUNT_POINT}/config" max_versions=20 >/dev/null + +cat >"${POLICY_FILE}" </dev/null + +if [ -n "${SERVICE_TOKEN}" ]; then + if ! bao token lookup "${SERVICE_TOKEN}" >/dev/null 2>&1; then + bao token create \ + -id="${SERVICE_TOKEN}" \ + -policy=jumpserver \ + -orphan \ + -no-default-policy \ + -format=json >"${SERVICE_TOKEN_FILE}" + chmod 600 "${SERVICE_TOKEN_FILE}" 2>/dev/null || true + fi +fi diff --git a/config_init/openbao/server.hcl b/config_init/openbao/server.hcl new file mode 100644 index 0000000..39957de --- /dev/null +++ b/config_init/openbao/server.hcl @@ -0,0 +1,16 @@ +ui = true +disable_mlock = true + +storage "raft" { + path = "/openbao/file" + node_id = "openbao" +} + +listener "tcp" { + address = "0.0.0.0:8200" + cluster_address = "0.0.0.0:8201" + tls_disable = true +} + +api_addr = "http://openbao:8200" +cluster_addr = "http://openbao:8201" diff --git a/jmsctl.sh b/jmsctl.sh index d113f87..668f6cf 100755 --- a/jmsctl.sh +++ b/jmsctl.sh @@ -80,6 +80,9 @@ function service_to_docker_name() { EXE="" function start() { + set_openbao || return 1 + gen_safe_config >/dev/null + EXE=$(get_docker_compose_cmd_line) ${EXE} up -d base_dir="${PROJECT_DIR}" diff --git a/scripts/0_prepare.sh b/scripts/0_prepare.sh index 5d84dd3..9fd1b7e 100644 --- a/scripts/0_prepare.sh +++ b/scripts/0_prepare.sh @@ -49,6 +49,10 @@ function prepare_image_files() { fi rm -f "${IMAGE_DIR}"/* + # The offline bundle must carry optional OpenBao even when it is disabled by + # default, so it can be enabled later without registry access. + local INCLUDE_OPENBAO_IMAGE=1 + export INCLUDE_OPENBAO_IMAGE pull_images images=$(get_images) diff --git a/scripts/1_config_jumpserver.sh b/scripts/1_config_jumpserver.sh index 2a2d7d8..106897a 100644 --- a/scripts/1_config_jumpserver.sh +++ b/scripts/1_config_jumpserver.sh @@ -258,6 +258,7 @@ function main() { if set_redis; then echo_done fi + set_openbao || return 1 if set_service; then echo_done fi diff --git a/scripts/7_upgrade.sh b/scripts/7_upgrade.sh index fe1a14d..c1a864e 100644 --- a/scripts/7_upgrade.sh +++ b/scripts/7_upgrade.sh @@ -165,6 +165,7 @@ function update_config_if_need() { migrate_coco_to_koko migrate_config upgrade_config + set_openbao || exit 1 clean_file } diff --git a/scripts/gists/conf.sh b/scripts/gists/conf.sh index 992368d..dffb12b 100644 --- a/scripts/gists/conf.sh +++ b/scripts/gists/conf.sh @@ -38,7 +38,19 @@ function get_config_or_env() { echo "${value}" } -CONFIG_SAFE_EXCLUDES="DB_HOST DB_PORT DB_PASSWORD" +CONFIG_SAFE_EXCLUDES="DB_HOST DB_PORT DB_PASSWORD REDIS_PASSWORD VAULT_OPENBAO_TOKEN" + +function is_config_excluded() { + local key=$1 + local excluded + + for excluded in ${CONFIG_SAFE_EXCLUDES}; do + if [[ "${key}" == "${excluded}" ]]; then + return 0 + fi + done + return 1 +} function gen_safe_config() { local base_config_file=${CONFIG_FILE} @@ -177,6 +189,9 @@ function prepare_config() { find "${CONFIG_DIR}" -type d -exec chmod 700 {} \; find "${CONFIG_DIR}" -type f -exec chmod 600 {} \; chmod 644 "${CONFIG_DIR}/redis/redis.conf" + if [[ -f "${CONFIG_DIR}/openbao/server.hcl" ]]; then + chmod 644 "${CONFIG_DIR}/openbao/server.hcl" + fi if [[ "$(uname -m)" == "aarch64" ]]; then sed_in_place "s/# ignore-warnings ARM64-COW-BUG/ignore-warnings ARM64-COW-BUG/g" "${CONFIG_DIR}/redis/redis.conf" diff --git a/scripts/gists/image.sh b/scripts/gists/image.sh index 66f90e7..9048024 100644 --- a/scripts/gists/image.sh +++ b/scripts/gists/image.sh @@ -25,6 +25,9 @@ function get_pull_images() { if [[ "${use_xpack}" == "1" ]]; then images+=("jumpserver/ansible-executor:latest") fi + if should_include_openbao_image; then + images+=("$(get_openbao_image)") + fi echo "${images[@]}" } @@ -47,6 +50,9 @@ function get_images() { if [[ "${use_xpack}" == "1" ]]; then images+=("${namespace}/ansible-executor:latest") fi + if should_include_openbao_image; then + images+=("$(get_openbao_image)") + fi echo "${images[@]}" } @@ -86,7 +92,9 @@ function get_image_full_path() { full_image_path="${image}" if [[ -n "${DOCKER_IMAGE_PREFIX}" ]]; then - if echo "${DOCKER_IMAGE_PREFIX}" | grep -q "/";then + if [[ "${image}" == */* && $(image_has_prefix "${image}") != "1" ]]; then + full_image_path="${image}" + elif echo "${DOCKER_IMAGE_PREFIX}" | grep -q "/";then app=$(echo "$image" | awk -F'/' '{ print $NF }') full_image_path="${DOCKER_IMAGE_PREFIX}/${app}" elif [[ $(image_has_prefix "${image}") != "1" ]]; then @@ -151,4 +159,3 @@ function pull_images() { trap - SIGINT SIGTERM } - diff --git a/scripts/gists/openbao.sh b/scripts/gists/openbao.sh new file mode 100644 index 0000000..66caa9e --- /dev/null +++ b/scripts/gists/openbao.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash + +function is_internal_openbao_enabled() { + local vault_enabled vault_backend openbao_external + + vault_enabled=$(get_config_or_env VAULT_ENABLED) + vault_backend=$(get_config_or_env VAULT_BACKEND) + openbao_external=$(get_config_or_env OPENBAO_EXTERNAL "false") + + case "${vault_enabled}" in + 1|true|True|TRUE) ;; + *) return 1 ;; + esac + [[ "${vault_backend}" == "openbao" ]] || return 1 + case "${openbao_external}" in + 1|true|True|TRUE) return 1 ;; + esac + return 0 +} + +function get_openbao_image() { + get_config_or_env OPENBAO_IMAGE "openbao/openbao:2.6.0" +} + +function should_include_openbao_image() { + case "${INCLUDE_OPENBAO_IMAGE:-}" in + 1|true|True|TRUE) return 0 ;; + esac + is_internal_openbao_enabled +} + +function set_openbao() { + local vault_enabled vault_backend openbao_external vault_addr vault_token + + vault_enabled=$(get_config VAULT_ENABLED "false") + vault_backend=$(get_config VAULT_BACKEND "openbao") + openbao_external=$(get_config OPENBAO_EXTERNAL "false") + + set_config VAULT_ENABLED "${vault_enabled}" + if [[ "${vault_enabled}" != "1" && "${vault_enabled}" != "true" && "${vault_enabled}" != "True" && "${vault_enabled}" != "TRUE" ]]; then + return + fi + if [[ "${vault_backend}" != "openbao" ]]; then + return + fi + + vault_addr=$(get_config VAULT_OPENBAO_ADDR) + vault_token=$(get_config VAULT_OPENBAO_TOKEN) + + if [[ "${openbao_external}" == "1" || "${openbao_external}" == "true" || "${openbao_external}" == "True" || "${openbao_external}" == "TRUE" ]]; then + if [[ -z "${vault_token}" ]]; then + log_error "$(gettext 'VAULT_OPENBAO_TOKEN is required when using external OpenBao')" + return 1 + fi + if [[ -z "${vault_addr}" || "${vault_addr}" == "http://openbao:8200" || "${vault_addr}" == "https://openbao:8200" ]]; then + log_error "$(gettext 'Set VAULT_OPENBAO_ADDR to the external OpenBao address')" + return 1 + fi + else + if [[ -z "${vault_addr}" ]]; then + vault_addr="http://openbao:8200" + fi + if [[ -z "${vault_token}" ]]; then + vault_token=$(random_str 48) + set_config VAULT_OPENBAO_TOKEN "${vault_token}" + fi + fi + + set_config VAULT_BACKEND openbao + set_config VAULT_OPENBAO_ADDR "${vault_addr}" + set_config VAULT_OPENBAO_MOUNT_POINT "$(get_config VAULT_OPENBAO_MOUNT_POINT pam)" + set_config VAULT_OPENBAO_TIMEOUT "$(get_config VAULT_OPENBAO_TIMEOUT 10)" + set_config OPENBAO_EXTERNAL "${openbao_external}" + + if [[ "${openbao_external}" == "1" || "${openbao_external}" == "true" || "${openbao_external}" == "True" || "${openbao_external}" == "TRUE" ]]; then + return 0 + fi + + set_config OPENBAO_RAFT_NODE_ID "$(get_config OPENBAO_RAFT_NODE_ID openbao)" + set_config OPENBAO_RAFT_API_ADDR "$(get_config OPENBAO_RAFT_API_ADDR http://openbao:8200)" + set_config OPENBAO_RAFT_CLUSTER_ADDR "$(get_config OPENBAO_RAFT_CLUSTER_ADDR http://openbao:8201)" + set_config OPENBAO_RAFT_BOOTSTRAP "$(get_config OPENBAO_RAFT_BOOTSTRAP true)" + set_config OPENBAO_UNSEAL_KEY_SHARES "$(get_config OPENBAO_UNSEAL_KEY_SHARES 5)" + set_config OPENBAO_UNSEAL_KEY_THRESHOLD "$(get_config OPENBAO_UNSEAL_KEY_THRESHOLD 3)" + set_openbao_bootstrap_script + set_openbao_server_config +} + +function set_openbao_bootstrap_script() { + local source_file target_file + + source_file="${PROJECT_DIR}/config_init/openbao/bootstrap.sh" + target_file="${CONFIG_DIR}/openbao/bootstrap.sh" + mkdir -p "${CONFIG_DIR}/openbao" + cp -f "${source_file}" "${target_file}" + chmod 600 "${target_file}" 2>/dev/null || true +} + +function set_openbao_server_config() { + local config_file data_path node_id api_addr cluster_addr retry_join addr + + config_file="${CONFIG_DIR}/openbao/server.hcl" + data_path="/openbao/file" + node_id=$(get_config OPENBAO_RAFT_NODE_ID openbao) + api_addr=$(get_config OPENBAO_RAFT_API_ADDR http://openbao:8200) + cluster_addr=$(get_config OPENBAO_RAFT_CLUSTER_ADDR http://openbao:8201) + retry_join=$(get_config OPENBAO_RAFT_RETRY_JOIN) + + mkdir -p "${CONFIG_DIR}/openbao" + + cat >"${config_file}" <>"${config_file}" <>"${config_file}" </dev/null || true +} diff --git a/scripts/gists/service.sh b/scripts/gists/service.sh index 5aef6c9..e54fe02 100644 --- a/scripts/gists/service.sh +++ b/scripts/gists/service.sh @@ -140,6 +140,9 @@ function get_docker_compose_services() { [[ "${use_es}" == "1" ]] && services+=" es" [[ "${use_minio}" == "1" ]] && services+=" minio" [[ "${use_loki}" == "1" ]] && services+=" loki" + if is_internal_openbao_enabled; then + services+=" openbao" + fi echo "${services}" } @@ -348,4 +351,4 @@ function installation_log() { version=$(get_current_version) url="https://community.fit2cloud.com/installation-analytics?product=${product}&type=${install_type}&version=${version}" curl --connect-timeout 5 -m 10 -k $url &>/dev/null -} \ No newline at end of file +} diff --git a/scripts/utils.sh b/scripts/utils.sh index 357492d..19ddb54 100644 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -6,8 +6,8 @@ BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" . "${BASE_DIR}/const.sh" . "${BASE_DIR}/gists/common.sh" . "${BASE_DIR}/gists/conf.sh" +. "${BASE_DIR}/gists/openbao.sh" . "${BASE_DIR}/gists/image.sh" . "${BASE_DIR}/gists/service.sh" namespace=${NAMESPACE:-jumpserver} -