apisix 카탈로그 CVE 게이트 완전 해소 (차단 165건 → 0건)
etcd(bitnamilegacy 동결 미러) → etcd.enabled=false + 카탈로그 자체 etcd 차트를 externalEtcd 기본값으로 연결. adc·apisix-ingress-controller·apisix(paasup/apisix) 세 이미지는 SUSE BCI 자체 빌드로 교체 — 전부 벤더 등급만으로는 안 보이던 벤더 하향 등급 CVE(NVD 재평가 시 드러남)가 원인이었다. - images/apisix-ingress-controller: 정적 링크 Go 모듈 취약 버전만 강제 업그레이드 - images/apisix: APISIX-Runtime(WASM·dubbo 등 커스텀 모듈 포함) 전체를 SUSE BCI 위에서 소스로 재현, keycloak-authz 플러그인 오버레이 - images/adc: 업스트림 빌더 스테이지는 그대로 두고 distroless 최종 베이스만 SUSE BCI+nodejs24 로 교체 scripts/build/patch-catalog-tag.py 의 TAG_BLOCK 이 점 구분 중첩 경로를 지원하도록 확장(apisix 서브차트 alias 때문에 필요). 세 이미지 모두 게이트 PASS(실효 CRITICAL/HIGH 0/0)와 배포 검증(테스트 클러스터)을 마쳤다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
# apisix — 자체 빌드
|
||||
|
||||
apisix(APISIX-Runtime + APISIX 애플리케이션 + keycloak-authz 커스텀 플러그인) 전체를
|
||||
업스트림 소스에서 SUSE BCI 위에 직접 컴파일한다.
|
||||
`manifests/helm/apisix/2.16.0/custom-values.yaml`의 `image.repository`/`tag`가 이
|
||||
산출물을 가리킨다.
|
||||
|
||||
> **자체 빌드는 대응 우선순위 3번이다.** 상위 태그 교체·베이스 OS 교체로 목표를
|
||||
> 만족할 수 있으면 그 쪽을 쓴다. 자체 빌드는 업스트림 서명·provenance·SBOM attestation 을
|
||||
> 잃고 재빌드 책임을 지는 선택이다.
|
||||
|
||||
## 왜 자체 빌드하나
|
||||
|
||||
apisix 카탈로그 CVE 조치(2026-08-11~12) 중 발견 — 기존엔 별도 프로젝트
|
||||
(`paasup/dataup` 레포 `experiment/apisix/apisix-plugin`)에서 `apache/apisix:3.17.0-debian`
|
||||
위에 keycloak-authz 커스텀 플러그인만 얹어 빌드했다. 이 Debian 베이스 자체가 게이트
|
||||
차단 26건(2026-08-11 실측) — `apache/apisix:3.17.0-debian`은 확인 시점 기준 **이미
|
||||
최신 업스트림 태그**라 상위 태그 교체로는 해소가 안 되고, 전부 Debian 베이스 OS
|
||||
패키지(libc, perl, pcre 등) CVE라 베이스 OS를 통째로 바꿔야 했다.
|
||||
|
||||
## 업스트림과 다르게 하는 부분 — SUSE 에 이 이미지가 없는 이유
|
||||
|
||||
업스트림(`apache/apisix:3.17.0-debian`)은 vanilla OpenResty 가 아니라 **"APISIX-Runtime"**
|
||||
이라는 커스텀 컴파일 nginx다 — OpenSSL 3.4.1·zlib·PCRE 를 직접 빌드해 넣고,
|
||||
`apisix-nginx-module`·`wasm-nginx-module`(WASM, wasmtime)·`lua-var-nginx-module`·
|
||||
`lua-resty-events`·`mod_dubbo`·`ngx_multi_upstream_module`을 `--add-module`로
|
||||
**컴파일 타임에 정적으로** 얹는다(2026-08-11 `openresty -V` 로 실측 — 아래 "소스·버전
|
||||
관리" 표에 정확한 버전 나열). 이 조합을 배포하는 apiseven 의 빌드 파이프라인
|
||||
(`api7/apisix-build-tools`)은 **Debian/RHEL(UBI9) 용 사전 빌드 패키지만** 만든다 — SUSE
|
||||
용은 없다.
|
||||
|
||||
- **vanilla OpenResty로 대체 불가**: openresty.org 는 SLES 15.x 를 공식 지원해 zypper
|
||||
로 설치할 수 있지만, 그건 위 커스텀 모듈(WASM·mod_dubbo 등)이 하나도 없는 순정
|
||||
빌드다. `--add-module`은 컴파일 타임에만 바이너리에 정적으로 박히는 옵션이라 이미
|
||||
컴파일된 vanilla 바이너리에 나중에 모듈을 추가할 수 없다 — nginx의 "동적 모듈"
|
||||
메커니즘도 이 모듈들이 그 형태로 배포되지 않아 적용 안 된다. 우리 apisix 배포가
|
||||
WASM 플러그인과 `http-dubbo` 관련 기능을 공식적으로 지원해야 한다는 요구사항이 있어
|
||||
(`custom-values.yaml`의 `apisix.plugins`에 `http-dubbo` 포함), 모듈을 뺀 축소판으로
|
||||
타협하지 않고 업스트림과 동일한 모듈 구성을 그대로 재현했다.
|
||||
- **Debian/RHEL 사전 빌드 RPM/DEB 를 SUSE 에 강제 설치도 불가**: 같은 이유로 시도하지
|
||||
않았다 — glibc/openssl 심볼 버전이 안 맞아 ABI 가 깨질 위험(cnpg-postgresql 때와
|
||||
달리 이번엔 SUSE 자체 저장소에 대응 패키지가 없어 "같은 배포판 계열 재설치"가 성립
|
||||
하지 않는다).
|
||||
- 그래서 **apiseven 의 공식 빌드 스크립트(`api7/apisix-build-tools`, 태그
|
||||
`apisix-runtime/1.3.6` — `openresty -V`의 `APISIX_RUNTIME_VER=1.3.6`과 실측 일치)를
|
||||
그대로 재현**해 SUSE BCI 위에서 소스 컴파일했다. 애플리케이션 코드·모듈 버전은
|
||||
업스트림과 100% 동일하고, 베이스 OS만 바뀐다(diff 최소화 원칙).
|
||||
|
||||
## 3단계 구성
|
||||
|
||||
| 단계 | 목표 |
|
||||
| --- | --- |
|
||||
| `runtime` | OpenSSL 3.4.1·zlib·PCRE 를 `$OR_PREFIX/{openssl3,zlib,pcre}`에 직접 빌드하고, OpenResty 소스에 위 커스텀 모듈 6종을 `--add-module`로 붙여 컴파일한다. openresty.org 의 SLES 전용 `-devel` 패키지가 없어 이 경로들을 소스 빌드로 채운다. |
|
||||
| `apisix-app` | `runtime`이 만든 OpenResty/LuaJIT 위에 APISIX 애플리케이션(Lua 코드 + 일부 C 확장 rock)을 `luarocks make`로 설치한다. |
|
||||
| `final` | 위 두 스테이지 산출물만 깨끗한 SUSE BCI 이미지로 옮기고, 런타임에 필요한 공유 라이브러리(libxml2·libxslt·libyaml·pcre·pcre2)만 추가, non-root(`apisix`, uid 636) 설정, keycloak-authz 플러그인 오버레이까지 마친다. |
|
||||
|
||||
## 실측 함정 (2026-08-11/12)
|
||||
|
||||
- **빌드 순서 — zlib 을 OpenSSL 보다 먼저 빌드한다.** OpenSSL 의 `zlib` config 옵션이
|
||||
컴파일 타임에 `zlib.h` 를 요구한다 — 반대 순서로 뒀다가 `zlib.h: No such file` 로
|
||||
실패했다.
|
||||
- **`lua-resty-saml`(luarocks 가 자동으로 받는 의존 rock)이 SUSE 표준 `libxml2-devel`
|
||||
(2.12.10)의 `xmlSetStructuredErrorFunc` 시그니처(콜백 인자에 `const` 추가됨)와 안
|
||||
맞아 `-Werror=incompatible-pointer-types` 로 빌드 실패한다.** rock 자체의 Makefile 이
|
||||
`-Werror` 를 하드코딩해 환경변수 `CFLAGS` 로는 못 끈다 — `apisix-app` 스테이지 안에서만
|
||||
쓰는 `gcc` 래퍼(`-Wno-error=incompatible-pointer-types` 추가)로 그 진단 하나만
|
||||
경고로 낮추고, `luarocks make` 끝나면 즉시 원복한다(다른 빌드에 영향 안 줌).
|
||||
source.Dockerfile 의 해당 RUN 스텝 주석 참고.
|
||||
- **`ui/`(Admin 대시보드 프론트엔드)는 `apache/apisix` 저장소 자체엔 없다.** apiseven
|
||||
의 패키징 파이프라인이 별도로(Node.js/yarn 빌드) 끼워 넣는 단계라 git clone 만으로는
|
||||
재현 안 된다 — 우리 배포는 이 UI 를 쓰지 않아(`custom-values.yaml` 에 관련 설정 없음)
|
||||
없으면 빈 디렉터리로 대체하고 건너뛴다.
|
||||
- **`/usr/bin/apisix` 래퍼 스크립트가 내부적으로 `awk` 를 쓴다.** OpenResty 버전 파싱에
|
||||
쓰는데, `final` 스테이지에 `gawk` 를 안 깔면 "awk: command not found" 로 `apisix
|
||||
version`부터 실패한다.
|
||||
- **SUSE 공유 라이브러리 패키지명은 soname 이 붙는다.** `libxml2`/`libxslt`/`pcre` 같은
|
||||
이름 그대로는 없고 `libxml2-2`·`libxslt1`·`libpcre1`·`libpcre2-8-0`·`libyaml-0-2` 다
|
||||
— `libxml2-2`/`libpcre2-8-0`/`libldap-2_4-2` 는 `bci-base` 에 기본 포함이라 명시 안
|
||||
해도 되지만, 명확성을 위해 실측한 이름 그대로 적었다.
|
||||
- **`docker build --pull` 은 베이스 이미지 digest 가 바뀌면 캐시를 전부 무효화한다.**
|
||||
`build-hardened-image.sh`가 항상 `--pull` 을 쓰므로(스케줄 재빌드가 최신 베이스를
|
||||
전제하기 때문), Dockerfile 을 안 고쳤어도 SUSE BCI 태그가 그 사이 갱신되면 전체
|
||||
재컴파일(약 35분)이 발생할 수 있다 — Dockerfile 반복 수정·테스트는 `--pull` 없는
|
||||
순수 `docker build` 로 하고, 최종 검증에서만 `build-hardened-image.sh` 를 쓴다.
|
||||
|
||||
## 소스·버전 관리
|
||||
|
||||
| 항목 | 값 |
|
||||
| --- | --- |
|
||||
| APISIX 소스 | `https://github.com/apache/apisix.git` (태그 `3.17.0`) |
|
||||
| OpenResty | `1.29.2.4` |
|
||||
| OpenSSL | `3.4.1` |
|
||||
| zlib / PCRE | `1.3.1` / `8.45` |
|
||||
| 커스텀 모듈 | `apisix-nginx-module 1.19.5`, `wasm-nginx-module 0.7.0`, `lua-var-nginx-module v0.5.3`, `lua-resty-events 0.2.0`, `ngx_multi_upstream_module 1.3.3`, `mod_dubbo 1.0.2` |
|
||||
| APISIX_RUNTIME_VER | `1.3.6` (apiseven 빌드 스크립트 버전 태그) |
|
||||
| 최종 베이스 | `registry.suse.com/bci/bci-base:15.7` |
|
||||
|
||||
전부 `source.build.env` 에서 관리하며 **자동 추적하지 않는다.** 다른 자체 빌드 이미지와
|
||||
동일하게, 사람이 업스트림 새 릴리스(또는 위 CVE 세트를 이미 해소한 버전)를 보고
|
||||
`source.build.env` 를 고쳐 PR 을 여는 것 자체가 갱신 트리거다.
|
||||
|
||||
**권장 점검 주기**: 카탈로그 게이트가 이 이미지의 차단 CVE 를 다시 보고할 때, 또는
|
||||
`apache/apisix`가 `3.17.1`(혹은 그 이상)을 내놓았을 때 — 릴리스가 나오면 그쪽으로
|
||||
갈아타는 것(대응 우선순위 a)이 이 자체 빌드를 유지하는 것보다 항상 우선이다.
|
||||
|
||||
## 빌드
|
||||
|
||||
```sh
|
||||
# 로컬 빌드 (push 없음)
|
||||
IMAGE=apisix BASE_OS=source bash scripts/build/build-hardened-image.sh /tmp/out
|
||||
|
||||
# 레지스트리에 push 까지
|
||||
IMAGE=apisix BASE_OS=source REGISTRY=docker.io/paasup \
|
||||
bash scripts/build/build-hardened-image.sh /tmp/out
|
||||
```
|
||||
|
||||
수행 순서: **빌드 → 기능 검증(`verify.sh`) → SBOM → 전 심각도 스캔 → 게이트 판정.**
|
||||
`verify.sh`는 `apisix version` 출력·nginx 설정 문법(`nginx -t`)뿐 아니라, standalone
|
||||
YAML 설정으로 **실제 nginx worker 를 기동**해 APISIX 라우터가 HTTP 요청에 응답하는지
|
||||
(라우트 없음 → 404)까지 확인한다 — 15개 커스텀 모듈 + ~90개 Lua 플러그인(lua-resty-saml
|
||||
같은 C 확장 포함) 로딩까지 실제로 거치는 유일한 방법이다. **keycloak-authz 커스텀
|
||||
플러그인은 실제 Keycloak 연동이 필요해 이 스모크 범위 밖**이다 — dev 클러스터 배포
|
||||
검증(`.claude/deploy-test-procedure.md`)이 담당한다.
|
||||
|
||||
**결과(2026-08-12 실측)**: 게이트 `PASS`, 커버리지 `ok`, 실효 CRITICAL/HIGH `0/0`
|
||||
(업스트림 26건에서 완전 해소). `docker.io/paasup/apisix:3.17.0-security-hardened-20260811`
|
||||
로 push 완료.
|
||||
|
||||
### 파일 구성
|
||||
|
||||
| 파일 | 역할 |
|
||||
| --- | --- |
|
||||
| `source.Dockerfile` | 빌드 정의 — 3단계(runtime/apisix-app/final) |
|
||||
| `source.build.env` | pinned 버전 전체(`BUILD_ARGS`에 나열한 이름만 `--build-arg` 로 전달됨) |
|
||||
| `verify.sh` | 기능 검증 — 실제 nginx 기동 + HTTP 요청까지 |
|
||||
| `keycloak-authz.lua` | `paasup/dataup` 레포(`experiment/apisix/apisix-plugin/keycloak-authz/`)와 동일한 커스텀 플러그인 — 원본 레포는 그대로 두고 이 카탈로그가 더 이상 참조만 안 한다 |
|
||||
| `docker-entrypoint.sh` | 업스트림 `apache/apisix-docker`(`utils/docker-entrypoint.sh`)와 동일 |
|
||||
|
||||
베이스 변종이 하나뿐이라 파일명이 `source.*` 로 고정돼 있다.
|
||||
|
||||
### 태그
|
||||
|
||||
```
|
||||
docker.io/paasup/apisix:3.17.0-security-hardened-20260811
|
||||
└ app ┘└ 슬러그 ┘└ 하드닝 ┘└ 빌드일 ┘
|
||||
```
|
||||
|
||||
## 아직 안 한 것 — 배포 검증
|
||||
|
||||
게이트 PASS·기능 스모크테스트(nginx 기동+HTTP 응답)까지만 확인했다. **실제 Keycloak
|
||||
연동(keycloak-authz 플러그인 동작), etcd 연동(externalEtcd), ingress-controller 와의
|
||||
연동을 포함한 실제 클러스터 배포 검증은 아직 하지 않았다** —
|
||||
`.claude/deploy-test-procedure.md` 절차로 카탈로그 반영 전 반드시 수행할 것.
|
||||
@@ -0,0 +1,13 @@
|
||||
# build-image.yml 이 읽는 카탈로그 반영 메타데이터. 빌드 정의(source.build.env)와는
|
||||
# 별개다 — 이건 "이 이미지가 어느 차트의 어느 필드를 가리키는가" 만 담는다.
|
||||
|
||||
CHART_DIRS="manifests/helm/apisix/2.16.0"
|
||||
|
||||
# 태그 표기 스타일 — imageName(단일 필드 문자열) | split(registry/repository/tag 분리)
|
||||
TAG_STYLE=split
|
||||
# apisix 차트 자체의 최상위 image 블록(서브차트 alias 아래가 아니다 — 그건
|
||||
# apisix-ingress-controller 자체 빌드가 쓰는 ingress-controller.deployment.image).
|
||||
TAG_BLOCK=image
|
||||
|
||||
# base_os 입력을 안 주면 쓸 기본 변종 (images/<image>/<DEFAULT_BASE_OS>.build.env)
|
||||
DEFAULT_BASE_OS=source
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
# contributor license agreements. See the NOTICE file distributed with
|
||||
# this work for additional information regarding copyright ownership.
|
||||
# The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
# (the "License"); you may not use this file except in compliance with
|
||||
# the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
PREFIX=${APISIX_PREFIX:=/usr/local/apisix}
|
||||
|
||||
if [[ "$1" == "docker-start" ]]; then
|
||||
if [ "$APISIX_STAND_ALONE" = "true" ]; then
|
||||
# If the file is not present then initialise the content otherwise update relevant keys for standalone mode
|
||||
if [ ! -f "${PREFIX}/conf/config.yaml" ]; then
|
||||
cat > ${PREFIX}/conf/config.yaml << _EOC_
|
||||
deployment:
|
||||
role: data_plane
|
||||
role_data_plane:
|
||||
config_provider: yaml
|
||||
_EOC_
|
||||
fi
|
||||
|
||||
if [ ! -f "${PREFIX}/conf/apisix.yaml" ]; then
|
||||
cat > ${PREFIX}/conf/apisix.yaml << _EOC_
|
||||
routes:
|
||||
-
|
||||
#END
|
||||
_EOC_
|
||||
fi
|
||||
/usr/bin/apisix init
|
||||
else
|
||||
/usr/bin/apisix init
|
||||
/usr/bin/apisix init_etcd
|
||||
fi
|
||||
|
||||
# For versions below 3.5.0 whose conf_server has not been removed.
|
||||
if [ -e "/usr/local/apisix/conf/config_listen.sock" ]; then
|
||||
rm -f "/usr/local/apisix/conf/config_listen.sock"
|
||||
fi
|
||||
|
||||
if [ -e "/usr/local/apisix/logs/worker_events.sock" ]; then
|
||||
rm -f "/usr/local/apisix/logs/worker_events.sock"
|
||||
fi
|
||||
|
||||
if [ -e "/usr/local/apisix/logs/stream_worker_events.sock" ]; then
|
||||
rm -f "/usr/local/apisix/logs/stream_worker_events.sock"
|
||||
fi
|
||||
|
||||
exec /usr/local/openresty/bin/openresty -p /usr/local/apisix -g 'daemon off;'
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
@@ -0,0 +1,168 @@
|
||||
-- keycloak-authz APISIX custom plugin
|
||||
-- Kong keycloak-authz 플러그인을 APISIX로 포팅
|
||||
--
|
||||
-- 동작:
|
||||
-- 1. X-Access-Token 헤더에서 JWT 추출 → preferred_username 파싱
|
||||
-- 2. 외부 API (/gwapi/v1/projectusers/{username}) POST 호출로 권한 확인
|
||||
-- 3. 결과를 TTL 기반으로 in-memory 캐싱 (lrucache)
|
||||
-- 4. 미인가 시 401, 내부 오류 시 500 반환
|
||||
|
||||
local core = require("apisix.core")
|
||||
local http = require("resty.http")
|
||||
local jwt = require("resty.jwt")
|
||||
local lrucache = require("resty.lrucache")
|
||||
local cjson = require("cjson.safe")
|
||||
|
||||
local plugin_name = "keycloak-authz"
|
||||
|
||||
-- 최대 1024개의 사용자 인증 결과를 캐싱
|
||||
local auth_cache, err = lrucache.new(1024)
|
||||
if not auth_cache then
|
||||
error("failed to create lrucache: " .. (err or "unknown"))
|
||||
end
|
||||
|
||||
local schema = {
|
||||
type = "object",
|
||||
properties = {
|
||||
api_url = {
|
||||
type = "string",
|
||||
description = "권한 확인 API의 base URL (예: https://api.example.com)",
|
||||
},
|
||||
basic_auth_token = {
|
||||
type = "string",
|
||||
description = "API 호출 시 사용할 Basic 인증 토큰 (Base64 인코딩된 값)",
|
||||
default = "",
|
||||
},
|
||||
timeout = {
|
||||
type = "integer",
|
||||
description = "API 호출 타임아웃 (밀리초)",
|
||||
default = 5000,
|
||||
minimum = 100,
|
||||
},
|
||||
skip_ssl_verify = {
|
||||
type = "boolean",
|
||||
description = "API 호출 시 SSL 인증서 검증 생략 여부",
|
||||
default = false,
|
||||
},
|
||||
ttl = {
|
||||
type = "integer",
|
||||
description = "인증 결과 캐싱 시간 (초)",
|
||||
default = 300,
|
||||
minimum = 1,
|
||||
},
|
||||
},
|
||||
required = {"api_url"},
|
||||
}
|
||||
|
||||
local _M = {
|
||||
version = 0.1,
|
||||
priority = 950, -- Kong의 PRIORITY = 950과 동일
|
||||
name = plugin_name,
|
||||
schema = schema,
|
||||
}
|
||||
|
||||
function _M.check_schema(conf)
|
||||
return core.schema.check(schema, conf)
|
||||
end
|
||||
|
||||
-- 외부 API 호출로 사용자 권한 확인
|
||||
local function fetch_auth_status(api_url, preferred_username, basic_auth_token, timeout, skip_ssl_verify)
|
||||
local httpc = http.new()
|
||||
|
||||
local scheme = ngx.var.scheme
|
||||
local host = ngx.var.host
|
||||
local url = scheme .. "://" .. host
|
||||
|
||||
local body, encode_err = cjson.encode({ url = url })
|
||||
if encode_err then
|
||||
return nil, "failed to encode request body: " .. encode_err
|
||||
end
|
||||
|
||||
local headers = { ["Content-Type"] = "application/json" }
|
||||
if basic_auth_token and basic_auth_token ~= "" then
|
||||
headers["Authorization"] = "Basic " .. basic_auth_token
|
||||
end
|
||||
|
||||
local full_url = api_url .. "/gwapi/v1/projectusers/" .. preferred_username
|
||||
|
||||
local res, req_err = httpc:request_uri(full_url, {
|
||||
method = "POST",
|
||||
body = body,
|
||||
headers = headers,
|
||||
timeout = timeout,
|
||||
ssl_verify = not skip_ssl_verify,
|
||||
keepalive_timeout = 60000,
|
||||
keepalive_pool = 10,
|
||||
})
|
||||
|
||||
httpc:close()
|
||||
|
||||
if not res then
|
||||
return nil, "API request failed: " .. (req_err or "unknown error")
|
||||
end
|
||||
|
||||
core.log.debug("auth API response status=", res.status, " user=", preferred_username)
|
||||
return res.status == 200, nil
|
||||
end
|
||||
|
||||
function _M.access(conf, ctx)
|
||||
-- X-Access-Token 헤더 추출
|
||||
local access_token = core.request.header(ctx, "X-Access-Token")
|
||||
if not access_token then
|
||||
return core.response.exit(401, { message = "Missing X-Access-Token header" })
|
||||
end
|
||||
|
||||
-- JWT 디코딩 (서명 검증 없이 페이로드만 파싱)
|
||||
local jwt_obj = jwt:load_jwt(access_token)
|
||||
if not jwt_obj or not jwt_obj.payload then
|
||||
core.log.warn("failed to decode JWT token")
|
||||
return core.response.exit(401, { message = "Failed to decode JWT token" })
|
||||
end
|
||||
|
||||
local preferred_username = jwt_obj.payload["preferred_username"]
|
||||
if not preferred_username then
|
||||
return core.response.exit(401, { message = "Missing preferred_username in JWT token" })
|
||||
end
|
||||
|
||||
local cache_key = "auth_status:" .. preferred_username
|
||||
|
||||
-- 캐시 조회
|
||||
local cached_result = auth_cache:get(cache_key)
|
||||
if cached_result ~= nil then
|
||||
core.log.debug("cache hit for user=", preferred_username, " result=", tostring(cached_result))
|
||||
if not cached_result then
|
||||
return core.response.exit(401, { message = "User is not authorized" })
|
||||
end
|
||||
ctx.authenticated_user = preferred_username
|
||||
return
|
||||
end
|
||||
|
||||
-- 캐시 미스: 외부 API 호출
|
||||
core.log.debug("cache miss, fetching auth status for user=", preferred_username)
|
||||
|
||||
local is_authorized, fetch_err = fetch_auth_status(
|
||||
conf.api_url,
|
||||
preferred_username,
|
||||
conf.basic_auth_token or "",
|
||||
conf.timeout or 5000,
|
||||
conf.skip_ssl_verify or false
|
||||
)
|
||||
|
||||
if fetch_err then
|
||||
core.log.error("auth check failed user=", preferred_username, " err=", fetch_err)
|
||||
return core.response.exit(500, { message = "Internal server error" })
|
||||
end
|
||||
|
||||
-- 결과 캐싱 (TTL 적용)
|
||||
auth_cache:set(cache_key, is_authorized, conf.ttl or 300)
|
||||
core.log.debug("cached auth result user=", preferred_username, " authorized=", tostring(is_authorized))
|
||||
|
||||
if not is_authorized then
|
||||
core.log.warn("authorization denied for user=", preferred_username)
|
||||
return core.response.exit(401, { message = "User is not authorized" })
|
||||
end
|
||||
|
||||
ctx.authenticated_user = preferred_username
|
||||
end
|
||||
|
||||
return _M
|
||||
@@ -0,0 +1,268 @@
|
||||
# apisix — 업스트림 apache/apisix:3.17.0-debian 를 대체하는 자체 빌드.
|
||||
#
|
||||
# 업스트림은 vanilla OpenResty 가 아니라 "APISIX-Runtime" 이라는 커스텀 컴파일 nginx다
|
||||
# (openssl3·zlib·pcre 를 직접 빌드해 넣고, apisix-nginx-module·wasm-nginx-module(WASM,
|
||||
# wasmtime)·lua-var-nginx-module·lua-resty-events·mod_dubbo·ngx_multi_upstream_module
|
||||
# 를 --add-module 로 정적으로 얹는다 — 2026-08-11 `openresty -V` 로 실측). 업스트림 빌드
|
||||
# 스크립트(https://github.com/api7/apisix-build-tools, 태그 apisix-runtime/1.3.6 —
|
||||
# `openresty -V` 의 APISIX_RUNTIME_VER=1.3.6 과 실측 일치)가 배포판 무관하게 소스에서
|
||||
# 컴파일하는 구조라 SUSE BCI 로 옮길 수 있었다 — Debian/RHEL 전용 사전빌드 RPM/DEB 를
|
||||
# SUSE 에 강제 설치하는 건 ABI 가 안 맞아 불가능하다(dockerfiles/Dockerfile.apisix.rpm
|
||||
# 등 참고, 둘 다 UBI9/Ubuntu 전용).
|
||||
#
|
||||
# 왜 자체 빌드인가 — apache/apisix:3.17.0-debian(2026-08-06 배포, 확인 시점 최신 태그)이
|
||||
# 게이트 차단 26건. 이미 최신 태그라 상위 태그 교체 불가, Debian 베이스 OS 패키지
|
||||
# (libc/perl/pcre 등) CVE 라 이번엔 정적 링크 바이너리가 아니라 진짜 베이스 OS 문제다.
|
||||
#
|
||||
# 구성(3단계):
|
||||
# 1) runtime — OpenSSL 3.4.1·zlib·pcre 를 $OR_PREFIX/{openssl3,zlib,pcre} 에 직접
|
||||
# 빌드하고, openresty-${OR_VER} 소스에 위 커스텀 모듈들을 --add-module 로 붙여
|
||||
# 컴파일한다(업스트림 빌드 스크립트 그대로 재현, 버전 전부 pinned).
|
||||
# 2) apisix — runtime 위에 APISIX 본체(Lua 애플리케이션 + 일부 C 확장 Lua rock)를
|
||||
# luarocks 로 설치한다. 이 단계에서 필요한 pcre2·openldap·libxml2·libxslt(lua-resty-
|
||||
# saml 이 xmlsec1 바인딩에 씀)·zlib devel 은 SUSE 표준 패키지로 설치한다(업스트림처럼
|
||||
# openresty 전용 -devel 패키지가 아니라 시스템 표준 -devel — 실제 배포판 표준
|
||||
# pcre/pcre2/libxml2/libxslt 헤더로 컴파일해도 무방한 부분이라 문제 없다).
|
||||
# 3) final — bci-base(런타임 공유 라이브러리 필요: libxml2·libxslt·openldap2 등을
|
||||
# 정적이 아니라 동적 링크로 쓰는 Lua rock 이 있어 bci-micro 로는 부족하다 — 실측
|
||||
# 후 bci-micro 로 축소 검토, 1차는 정확성 우선).
|
||||
#
|
||||
# 업스트림과 다르게 하는 부분
|
||||
# - 베이스: Debian → SUSE BCI(.claude/image-authoring.md 원칙 2).
|
||||
# - Rust 툴체인: rustup 으로 설치(wasm-nginx-module 의 wasmtime-c-api, APISIX 일부
|
||||
# rock 컴파일에 필요 — 업스트림도 동일하게 rustup 을 쓴다, 배포판 무관 설치라 차이 없음).
|
||||
# - 그 외 애플리케이션 코드·모듈 버전은 100% 동일(diff 최소화 원칙).
|
||||
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG BUILDER_BASE=registry.suse.com/bci/bci-base:15.7
|
||||
ARG RUNTIME_BASE=registry.suse.com/bci/bci-base:15.7
|
||||
|
||||
# =============================================================================
|
||||
# 1) runtime — OpenSSL/zlib/pcre + OpenResty(APISIX-Runtime 커스텀 모듈 전체)
|
||||
# =============================================================================
|
||||
FROM ${BUILDER_BASE} AS runtime
|
||||
ARG OPENRESTY_VERSION=1.29.2.4
|
||||
ARG OPENSSL_VERSION=3.4.1
|
||||
ARG APISIX_NGINX_MODULE_VER=1.19.5
|
||||
ARG WASM_NGINX_MODULE_VER=0.7.0
|
||||
ARG LUA_VAR_NGINX_MODULE_VER=v0.5.3
|
||||
ARG LUA_RESTY_EVENTS_VER=0.2.0
|
||||
ARG NGX_MULTI_UPSTREAM_MODULE_VER=1.3.3
|
||||
ARG MOD_DUBBO_VER=1.0.2
|
||||
ARG APISIX_RUNTIME_VER=1.3.6
|
||||
|
||||
ENV OR_PREFIX=/usr/local/openresty
|
||||
ENV PATH=/root/.cargo/bin:$PATH
|
||||
|
||||
RUN zypper -n refresh && zypper -n install -y \
|
||||
gcc gcc-c++ make patch git wget curl tar gzip xz which findutils perl unzip gawk
|
||||
|
||||
# 업스트림처럼 cpanm 으로 IPC::Cmd 를 보강한다(OpenSSL 3.x Configure 가 요구) — SUSE 는
|
||||
# perl-App-cpanminus 패키지가 없어 공식 부트스트랩으로 설치.
|
||||
RUN curl -L https://cpanmin.us | perl - App::cpanminus \
|
||||
&& cpanm --notest IPC::Cmd
|
||||
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
|
||||
WORKDIR /tmp/build
|
||||
|
||||
# --- zlib 먼저 (업스트림 openresty-zlib-devel 대신 표준 소스 빌드 — 같은 산출 경로).
|
||||
# OpenSSL 의 `zlib` config 옵션이 컴파일 타임에 zlib.h 를 요구하므로 OpenSSL 보다
|
||||
# 먼저 빌드해야 한다 — 실제로 순서를 반대로 뒀다가 `zlib.h: No such file` 로 실패한
|
||||
# 것을 실측했다(2026-08-11).
|
||||
ARG ZLIB_VERSION=1.3.1
|
||||
RUN wget "https://github.com/madler/zlib/releases/download/v${ZLIB_VERSION}/zlib-${ZLIB_VERSION}.tar.gz" \
|
||||
&& tar xzf "zlib-${ZLIB_VERSION}.tar.gz" \
|
||||
&& cd "zlib-${ZLIB_VERSION}" \
|
||||
&& ./configure --prefix="${OR_PREFIX}/zlib" \
|
||||
&& make -j"$(nproc)" \
|
||||
&& make install
|
||||
|
||||
# --- pcre (classic PCRE1, nginx/openresty 기본 요구사항 — 업스트림 openresty-pcre-devel
|
||||
# 과 같은 산출 경로 $OR_PREFIX/pcre 에 설치) ---
|
||||
ARG PCRE_VERSION=8.45
|
||||
RUN wget "https://sourceforge.net/projects/pcre/files/pcre/${PCRE_VERSION}/pcre-${PCRE_VERSION}.tar.gz/download" -O "pcre-${PCRE_VERSION}.tar.gz" \
|
||||
&& tar xzf "pcre-${PCRE_VERSION}.tar.gz" \
|
||||
&& cd "pcre-${PCRE_VERSION}" \
|
||||
&& ./configure --prefix="${OR_PREFIX}/pcre" --enable-jit --enable-utf --enable-unicode-properties \
|
||||
&& make -j"$(nproc)" \
|
||||
&& make install
|
||||
|
||||
# --- OpenSSL 3.4.1 (업스트림과 동일 버전) — zlib 헤더 경로를 명시로 넘긴다 ---
|
||||
RUN wget --no-check-certificate "https://github.com/openssl/openssl/releases/download/openssl-${OPENSSL_VERSION}/openssl-${OPENSSL_VERSION}.tar.gz" \
|
||||
&& tar xzf "openssl-${OPENSSL_VERSION}.tar.gz" \
|
||||
&& cd "openssl-${OPENSSL_VERSION}" \
|
||||
&& CFLAGS="-I${OR_PREFIX}/zlib/include" LDFLAGS="-L${OR_PREFIX}/zlib/lib -Wl,-rpath,${OR_PREFIX}/zlib/lib" \
|
||||
./config shared zlib enable-camellia enable-seed enable-rfc3779 \
|
||||
enable-cms enable-md2 enable-rc5 enable-weak-ssl-ciphers \
|
||||
--prefix="${OR_PREFIX}/openssl3" --libdir=lib \
|
||||
--with-zlib-lib="${OR_PREFIX}/zlib/lib" --with-zlib-include="${OR_PREFIX}/zlib/include" \
|
||||
&& make -j"$(nproc)" \
|
||||
&& make install_sw install_ssldirs
|
||||
|
||||
# --- OpenResty 소스 + 커스텀 모듈 (업스트림 build-apisix-runtime.sh 재현) ---
|
||||
RUN wget --no-check-certificate "https://openresty.org/download/openresty-${OPENRESTY_VERSION}.tar.gz" \
|
||||
&& tar zxf "openresty-${OPENRESTY_VERSION}.tar.gz"
|
||||
|
||||
RUN git clone --depth=1 -b "${LUA_RESTY_EVENTS_VER}" https://github.com/Kong/lua-resty-events.git "lua-resty-events-${LUA_RESTY_EVENTS_VER}" \
|
||||
&& git clone --depth=1 -b "${NGX_MULTI_UPSTREAM_MODULE_VER}" https://github.com/api7/ngx_multi_upstream_module.git "ngx_multi_upstream_module-${NGX_MULTI_UPSTREAM_MODULE_VER}" \
|
||||
&& git clone --depth=1 -b "${MOD_DUBBO_VER}" https://github.com/api7/mod_dubbo.git "mod_dubbo-${MOD_DUBBO_VER}" \
|
||||
&& git clone --depth=1 -b "${APISIX_NGINX_MODULE_VER}" -- https://github.com/api7/apisix-nginx-module.git "apisix-nginx-module-${APISIX_NGINX_MODULE_VER}" \
|
||||
&& git clone --depth=1 -b "${WASM_NGINX_MODULE_VER}" https://github.com/api7/wasm-nginx-module.git "wasm-nginx-module-${WASM_NGINX_MODULE_VER}" \
|
||||
&& git clone --depth=1 -b "${LUA_VAR_NGINX_MODULE_VER}" https://github.com/api7/lua-var-nginx-module "lua-var-nginx-module-${LUA_VAR_NGINX_MODULE_VER}"
|
||||
|
||||
RUN cd "ngx_multi_upstream_module-${NGX_MULTI_UPSTREAM_MODULE_VER}" && ./patch.sh "../openresty-${OPENRESTY_VERSION}" && cd .. \
|
||||
&& cd "apisix-nginx-module-${APISIX_NGINX_MODULE_VER}/patch" && ./patch.sh "../../openresty-${OPENRESTY_VERSION}" && cd ../.. \
|
||||
&& cd "wasm-nginx-module-${WASM_NGINX_MODULE_VER}" && ./install-wasmtime.sh && cd ..
|
||||
|
||||
RUN cd "openresty-${OPENRESTY_VERSION}" \
|
||||
&& or_limit_ver=0.09 \
|
||||
&& rm -rf "bundle/lua-resty-limit-traffic-${or_limit_ver}" \
|
||||
&& limit_ver=1.2.0 \
|
||||
&& wget "https://github.com/api7/lua-resty-limit-traffic/archive/refs/tags/v${limit_ver}.tar.gz" -O "lua-resty-limit-traffic-${limit_ver}.tar.gz" \
|
||||
&& tar xzf "lua-resty-limit-traffic-${limit_ver}.tar.gz" \
|
||||
&& mv "lua-resty-limit-traffic-${limit_ver}" "bundle/lua-resty-limit-traffic-${or_limit_ver}"
|
||||
|
||||
RUN cd "openresty-${OPENRESTY_VERSION}" \
|
||||
&& zlib_prefix="${OR_PREFIX}/zlib" pcre_prefix="${OR_PREFIX}/pcre" openssl_prefix="${OR_PREFIX}/openssl3" ; \
|
||||
./configure --prefix="${OR_PREFIX}" \
|
||||
--with-cc-opt="-DAPISIX_RUNTIME_VER=${APISIX_RUNTIME_VER} -DNGX_LUA_ABORT_AT_PANIC -I${zlib_prefix}/include -I${pcre_prefix}/include -I${openssl_prefix}/include" \
|
||||
--with-ld-opt="-Wl,-rpath,${OR_PREFIX}/wasmtime-c-api/lib -L${zlib_prefix}/lib -L${pcre_prefix}/lib -L${openssl_prefix}/lib -Wl,-rpath,${zlib_prefix}/lib:${pcre_prefix}/lib:${openssl_prefix}/lib" \
|
||||
--add-module="../mod_dubbo-${MOD_DUBBO_VER}" \
|
||||
--add-module="../ngx_multi_upstream_module-${NGX_MULTI_UPSTREAM_MODULE_VER}" \
|
||||
--add-module="../apisix-nginx-module-${APISIX_NGINX_MODULE_VER}" \
|
||||
--add-module="../apisix-nginx-module-${APISIX_NGINX_MODULE_VER}/src/stream" \
|
||||
--add-module="../apisix-nginx-module-${APISIX_NGINX_MODULE_VER}/src/meta" \
|
||||
--add-module="../wasm-nginx-module-${WASM_NGINX_MODULE_VER}" \
|
||||
--add-module="../lua-var-nginx-module-${LUA_VAR_NGINX_MODULE_VER}" \
|
||||
--add-module="../lua-resty-events-${LUA_RESTY_EVENTS_VER}" \
|
||||
--with-poll_module --with-pcre-jit \
|
||||
--without-http_rds_json_module --without-http_rds_csv_module --without-lua_rds_parser \
|
||||
--with-stream --with-stream_ssl_module --with-stream_ssl_preread_module \
|
||||
--with-http_v2_module --with-http_v3_module \
|
||||
--without-mail_pop3_module --without-mail_imap_module --without-mail_smtp_module \
|
||||
--with-http_stub_status_module --with-http_realip_module --with-http_addition_module \
|
||||
--with-http_auth_request_module --with-http_secure_link_module --with-http_random_index_module \
|
||||
--with-http_gzip_static_module --with-http_sub_module --with-http_dav_module \
|
||||
--with-http_flv_module --with-http_mp4_module --with-http_gunzip_module \
|
||||
--with-threads --with-compat \
|
||||
--with-luajit-xcflags="-DLUAJIT_NUMMODE=2 -DLUAJIT_ENABLE_LUA52COMPAT" \
|
||||
-j"$(nproc)" \
|
||||
&& make -j"$(nproc)" \
|
||||
&& make install
|
||||
|
||||
RUN cd "lua-resty-events-${LUA_RESTY_EVENTS_VER}" \
|
||||
&& install -d "${OR_PREFIX}/lualib/resty/events/" \
|
||||
&& install -m 664 lualib/resty/events/*.lua "${OR_PREFIX}/lualib/resty/events/" \
|
||||
&& install -d "${OR_PREFIX}/lualib/resty/events/compat/" \
|
||||
&& install -m 644 lualib/resty/events/compat/*.lua "${OR_PREFIX}/lualib/resty/events/compat/"
|
||||
|
||||
RUN cd "apisix-nginx-module-${APISIX_NGINX_MODULE_VER}" && OPENRESTY_PREFIX="${OR_PREFIX}" make install \
|
||||
&& cd "../wasm-nginx-module-${WASM_NGINX_MODULE_VER}" && OPENRESTY_PREFIX="${OR_PREFIX}" make install
|
||||
|
||||
# =============================================================================
|
||||
# 2) apisix — APISIX 본체(Lua 애플리케이션) 설치
|
||||
# =============================================================================
|
||||
FROM runtime AS apisix-app
|
||||
ARG APISIX_VERSION=3.17.0
|
||||
|
||||
ENV PATH=$PATH:${OR_PREFIX}/luajit/bin:${OR_PREFIX}/nginx/sbin:${OR_PREFIX}/bin
|
||||
|
||||
# lua-resty-saml(xmlsec1 바인딩)·lyaml·기타 rock 컴파일에 필요. 업스트림은 openresty
|
||||
# 전용 -devel 대신 시스템 표준 pcre/pcre2/openldap/libxml2/libxslt/zlib/yaml -devel 를
|
||||
# 쓴다(RHEL 기준 utils/install-dependencies.sh 참고) — SUSE 표준 패키지로 대응한다.
|
||||
# sudo 는 utils/linux-install-luarocks.sh 내부에서 그대로 호출한다(스크립트 수정 없이
|
||||
# 재사용하려고 패키지로 설치 — 이 스테이지는 어차피 root 로 실행된다).
|
||||
RUN zypper -n install -y \
|
||||
pcre-devel pcre2-devel openldap2-devel \
|
||||
libxml2-devel libxslt-devel zlib-devel libyaml-devel \
|
||||
diffutils cmake automake autoconf libtool gawk readline-devel sudo
|
||||
|
||||
RUN wget https://raw.githubusercontent.com/apache/apisix/${APISIX_VERSION}/utils/linux-install-luarocks.sh \
|
||||
&& chmod +x linux-install-luarocks.sh \
|
||||
&& ./linux-install-luarocks.sh
|
||||
|
||||
WORKDIR /apisix
|
||||
RUN git clone --depth=1 -b "${APISIX_VERSION}" https://github.com/apache/apisix.git .
|
||||
|
||||
# apisix-master-0.rockspec 는 저장소 루트에 있다(2026-08-11 태그 3.17.0 기준 실측).
|
||||
# `luarocks make` 는 rockspec 의 source.url(원격 tarball)을 쓰지 않고 현재 디렉토리를
|
||||
# 그대로 빌드 대상으로 삼는다 — git clone 만으로 충분하고 apiseven 빌드처럼 source.url
|
||||
# 을 로컬 경로로 sed 패치할 필요가 없다.
|
||||
#
|
||||
# lua-resty-saml(luarocks 가 자동으로 받는 의존 rock)의 xmlsec 바인딩(src/saml.c)이
|
||||
# libxml2 2.12(SUSE 표준 -devel 버전) 의 `xmlSetStructuredErrorFunc` 시그니처(콜백 인자에
|
||||
# const 추가됨)와 안 맞아 `-Werror=incompatible-pointer-types` 로 빌드 실패한다(2026-08-11
|
||||
# 실측) — rock 자체의 Makefile 이 `CFLAGS_ALL := ... -Werror ...` 를 하드코딩해 환경변수
|
||||
# CFLAGS 로는 못 끈다. rock 소스를 포크하지 않고, 이 스테이지 안에서만 쓰는 gcc 래퍼로
|
||||
# 그 진단 하나만 경고로 낮춘다(뒤에 오는 -Wno-error=가 앞의 -Werror 를 그 진단에 한해 이긴다).
|
||||
RUN mv /usr/bin/gcc /usr/bin/gcc.real \
|
||||
&& printf '#!/bin/sh\nexec /usr/bin/gcc.real -Wno-error=incompatible-pointer-types "$@"\n' > /usr/bin/gcc \
|
||||
&& chmod +x /usr/bin/gcc
|
||||
|
||||
RUN luarocks make ./apisix-master-0.rockspec --tree=/usr/local/apisix/deps --local
|
||||
|
||||
# 이후 단계에 영향 주지 않도록 원복한다 — 위 gcc 래퍼는 lua-resty-saml 빌드에만 쓴다.
|
||||
RUN mv /usr/bin/gcc.real /usr/bin/gcc
|
||||
|
||||
# apisix-build-tools 의 install_apisix() 를 재현한다(utils/install-common.sh) — luarocks
|
||||
# 가 rockspec 의 build.install.bin 으로 설치한 bin/apisix 래퍼와, deps 트리 안에 설치된
|
||||
# apisix Lua 패키지(share/lua/5.1/apisix)를 /usr/local/apisix 구조로 재배치한다.
|
||||
#
|
||||
# ui/ 는 apache/apisix 저장소 자체엔 없다(2026-08-11 3.17.0 태그 실측) — apiseven 의
|
||||
# 패키징 파이프라인이 별도로 admin 대시보드 프론트엔드를 빌드해 끼워 넣는 단계라
|
||||
# (Node.js/yarn 빌드, Dockerfile.package.apisix), 소스만 git clone 해서는 재현할 수
|
||||
# 없다. 우리 apisix 카탈로그 배포는 이 내장 UI 를 쓰지 않는다(custom-values.yaml 에
|
||||
# 관련 설정 없음) — 없으면 빈 디렉터리만 만들고 건너뛴다(있으면 그대로 복사).
|
||||
RUN mkdir -p /usr/local/apisix \
|
||||
&& cp -r conf /usr/local/apisix/conf \
|
||||
&& { cp -r ui /usr/local/apisix/ui 2>/dev/null || mkdir -p /usr/local/apisix/ui; } \
|
||||
&& install -m 755 bin/apisix /usr/local/apisix/apisix-cli-bin \
|
||||
&& mv /usr/local/apisix/deps/share/lua/5.1/apisix /usr/local/apisix/apisix \
|
||||
&& sed -i '1i package.path = "/usr/local/apisix/deps/share/lua/5.1/?/init.lua;" .. package.path' \
|
||||
/usr/local/apisix/apisix/cli/apisix.lua
|
||||
|
||||
# =============================================================================
|
||||
# 3) final
|
||||
# =============================================================================
|
||||
FROM ${RUNTIME_BASE} AS final
|
||||
|
||||
# 실제 SUSE 공유 라이브러리 패키지명은 soname 이 붙는다(libxml2/libldap 등은 이미
|
||||
# bci-base 기본 설치에 있어 명시 안 해도 되지만, 명확성을 위해 실측한 이름 그대로 적는다
|
||||
# — 2026-08-11 zypper search 로 확인: libxml2-2, libpcre2-8-0, libldap-2_4-2 는 기본
|
||||
# 포함, libpcre1·libxslt1·libyaml-0-2 만 추가 설치가 필요했다).
|
||||
# gawk 는 라이브러리가 아니라 /usr/bin/apisix 래퍼 스크립트 자체가 openresty 버전
|
||||
# 파싱에 awk 를 쓰기 때문에 필요하다(2026-08-11 실측: "awk: command not found").
|
||||
RUN zypper -n install -y libpcre1 libxslt1 libyaml-0-2 libxml2-2 libpcre2-8-0 gawk \
|
||||
&& zypper -n clean --all
|
||||
|
||||
COPY --from=apisix-app /usr/local/openresty /usr/local/openresty
|
||||
COPY --from=apisix-app /usr/local/apisix /usr/local/apisix
|
||||
COPY --from=apisix-app /usr/local/apisix/apisix-cli-bin /usr/bin/apisix
|
||||
|
||||
ENV PATH=$PATH:/usr/local/openresty/luajit/bin:/usr/local/openresty/nginx/sbin:/usr/local/openresty/bin
|
||||
|
||||
RUN chmod 755 /usr/bin/apisix \
|
||||
&& rm -f /usr/local/apisix/apisix-cli-bin \
|
||||
&& mkdir -p /usr/local/apisix/logs /usr/local/apisix/conf/cert \
|
||||
&& groupadd --system --gid 636 apisix \
|
||||
&& useradd --system --gid apisix --no-create-home --shell /usr/sbin/nologin --uid 636 apisix \
|
||||
&& chown -R apisix:0 /usr/local/apisix \
|
||||
&& chmod -R g=u /usr/local/apisix \
|
||||
&& ln -sf /dev/stdout /usr/local/apisix/logs/access.log \
|
||||
&& ln -sf /dev/stderr /usr/local/apisix/logs/error.log
|
||||
|
||||
# keycloak-authz 커스텀 플러그인 (paasup/dataup 레포의 dockerfile 과 동일한 오버레이)
|
||||
COPY keycloak-authz.lua /usr/local/apisix/apisix/plugins/keycloak-authz.lua
|
||||
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
RUN chmod 755 /docker-entrypoint.sh
|
||||
|
||||
WORKDIR /usr/local/apisix
|
||||
USER apisix
|
||||
|
||||
EXPOSE 9080 9443
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
CMD ["docker-start"]
|
||||
@@ -0,0 +1,34 @@
|
||||
# apisix — 소스 컴파일형 자체 빌드 (유일한 변종)
|
||||
#
|
||||
# build-hardened-image.sh 가 source 한다. BUILD_ARGS 에 나열한 이름만 --build-arg 로 넘어간다.
|
||||
# 이 이미지는 소스를 직접 컴파일하므로 "베이스 OS" 선택지가 없다 — BASE_OS=source 로 호출한다:
|
||||
# IMAGE=apisix BASE_OS=source bash scripts/build/build-hardened-image.sh <OUT_DIR>
|
||||
#
|
||||
# 왜 자체 빌드하는가·구성 상세 → source.Dockerfile 상단 주석. apisix 카탈로그 CVE 조치
|
||||
# (2026-08-11)로 도입 — 근거·경과는 MEMORY.md.
|
||||
|
||||
DOCKERFILE=source.Dockerfile
|
||||
TARGET=final
|
||||
TAG_SLUG=security
|
||||
|
||||
# build-hardened-image.sh 가 태그·verify.sh 전달용으로 요구하는 범용 필수값.
|
||||
# 스톡 3.17.0 과는 태그 슬러그(TAG_SLUG=security)로 구분되므로 버전 문자열 자체는 그대로 둔다.
|
||||
APP_VERSION=3.17.0
|
||||
|
||||
# 아래 값은 전부 2026-08-11 실측(apache/apisix:3.17.0-debian 의 `openresty -V`, apiseven
|
||||
# 빌드 스크립트 api7/apisix-build-tools 태그 apisix-runtime/1.3.6) — 상위 태그 나오면
|
||||
# 이 값들을 갱신하는 게 자체 빌드 유지보수보다 항상 우선이다(대응 우선순위 a).
|
||||
APISIX_VERSION=3.17.0
|
||||
OPENRESTY_VERSION=1.29.2.4
|
||||
OPENSSL_VERSION=3.4.1
|
||||
ZLIB_VERSION=1.3.1
|
||||
PCRE_VERSION=8.45
|
||||
APISIX_NGINX_MODULE_VER=1.19.5
|
||||
WASM_NGINX_MODULE_VER=0.7.0
|
||||
LUA_VAR_NGINX_MODULE_VER=v0.5.3
|
||||
LUA_RESTY_EVENTS_VER=0.2.0
|
||||
NGX_MULTI_UPSTREAM_MODULE_VER=1.3.3
|
||||
MOD_DUBBO_VER=1.0.2
|
||||
APISIX_RUNTIME_VER=1.3.6
|
||||
|
||||
BUILD_ARGS="APISIX_VERSION OPENRESTY_VERSION OPENSSL_VERSION ZLIB_VERSION PCRE_VERSION APISIX_NGINX_MODULE_VER WASM_NGINX_MODULE_VER LUA_VAR_NGINX_MODULE_VER LUA_RESTY_EVENTS_VER NGX_MULTI_UPSTREAM_MODULE_VER MOD_DUBBO_VER APISIX_RUNTIME_VER"
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
# apisix 이미지 기능 검증 — 호스트에서 bash 로 실행된다(build-hardened-image.sh 가
|
||||
# `env TAG=... PLATFORM=... bash verify.sh` 형태로 호출한다).
|
||||
#
|
||||
# 최종 베이스가 bci-base 라 bash/coreutils/awk 가 있다 — 게스트 셸 스크립트를 그대로
|
||||
# 주입한다. 단순 --help 스모크가 아니라 실제로 nginx worker 를 기동해 APISIX 라우터가
|
||||
# HTTP 요청에 응답하는지까지 확인한다(standalone/yaml config, etcd 불필요) — 15개
|
||||
# 커스텀 nginx 모듈 + ~90개 Lua 플러그인(lua-resty-saml 같은 C 확장 포함) 로딩까지
|
||||
# 실제로 거치는 유일한 방법이다. keycloak-authz 커스텀 플러그인은 실제 Keycloak 연동이
|
||||
# 필요해 이 스모크 범위 밖 — 이건 dev 클러스터 배포 검증(.claude/deploy-test-procedure.md)
|
||||
# 이 담당한다.
|
||||
#
|
||||
# 마지막 줄에 VERIFY-OK 를 출력하면 통과다. build-hardened-image.sh 가 그것으로 판정한다.
|
||||
set -e
|
||||
|
||||
TAG="${TAG:?TAG 환경변수가 필요하다}"
|
||||
PLATFORM="${PLATFORM:-linux/amd64}"
|
||||
|
||||
echo "== apisix version =="
|
||||
OUT="$(docker run --rm --platform "$PLATFORM" --entrypoint sh "$TAG" -c '
|
||||
export PATH=$PATH:/usr/local/openresty/luajit/bin:/usr/local/openresty/nginx/sbin:/usr/local/openresty/bin
|
||||
/usr/bin/apisix version
|
||||
')"
|
||||
echo " $OUT"
|
||||
case "$OUT" in
|
||||
*3.17.0*) ;;
|
||||
*) echo "FAIL: apisix version 출력에 3.17.0 이 없다"; exit 1 ;;
|
||||
esac
|
||||
|
||||
echo "== nginx 설정 문법 검사 (init + nginx -t) =="
|
||||
CONTAINER="verify-apisix-$$"
|
||||
docker run -d --rm --platform "$PLATFORM" --name "$CONTAINER" --entrypoint sh "$TAG" -c '
|
||||
export PATH=$PATH:/usr/local/openresty/luajit/bin:/usr/local/openresty/nginx/sbin:/usr/local/openresty/bin
|
||||
cd /usr/local/apisix
|
||||
export APISIX_STAND_ALONE=true
|
||||
cat > conf/config.yaml <<EOF
|
||||
deployment:
|
||||
role: data_plane
|
||||
role_data_plane:
|
||||
config_provider: yaml
|
||||
EOF
|
||||
cat > conf/apisix.yaml <<EOF
|
||||
routes:
|
||||
#END
|
||||
EOF
|
||||
/usr/bin/apisix init >/tmp/init.log 2>&1
|
||||
/usr/local/openresty/nginx/sbin/nginx -p /usr/local/apisix -t >>/tmp/init.log 2>&1
|
||||
exec /usr/local/openresty/bin/openresty -p /usr/local/apisix -g "daemon off;"
|
||||
' >/dev/null
|
||||
trap 'docker rm -f "$CONTAINER" >/dev/null 2>&1 || true' EXIT
|
||||
|
||||
echo "== HTTP 응답 대기 (최대 15초) =="
|
||||
# curl 은 연결 자체가 실패해도 %{http_code} 로 "000" 을 찍는다 — 빈 문자열이 아니라서
|
||||
# `[ -n "$CODE" ]` 만으로는 연결 실패를 통과로 오판한다(2026-08-12 실측: 파이프라인
|
||||
# 첫 실행에서 "HTTP 000" 인데도 VERIFY-OK 가 나온 버그). "000" 은 명시적으로 실패로 친다.
|
||||
ok=0
|
||||
for i in $(seq 1 15); do
|
||||
CODE="$(docker run --rm --platform "$PLATFORM" --network "container:$CONTAINER" curlimages/curl:latest \
|
||||
-s -o /dev/null -w '%{http_code}' -m 2 http://127.0.0.1:9080/ 2>/dev/null || true)"
|
||||
if [ -n "$CODE" ] && [ "$CODE" != "000" ]; then ok=1; break; fi
|
||||
sleep 1
|
||||
done
|
||||
if [ "$ok" != "1" ]; then
|
||||
echo "FAIL: 15초 내에 9080 포트가 유효한 HTTP 응답을 하지 않았다 (마지막 시도: '${CODE:-없음}')"
|
||||
docker logs "$CONTAINER" 2>&1 | tail -40
|
||||
exit 1
|
||||
fi
|
||||
echo " HTTP $CODE (라우트가 없어 404 가 정상 — nginx/APISIX 라우터가 실제로 요청을 처리했다는 뜻)"
|
||||
case "$CODE" in
|
||||
404) ;;
|
||||
*) echo "FAIL: 예상한 404(라우트 없음)가 아니라 $CODE 가 나왔다 — 로그 확인 필요"; docker logs "$CONTAINER" 2>&1 | tail -40; exit 1 ;;
|
||||
esac
|
||||
|
||||
echo "VERIFY-OK"
|
||||
Reference in New Issue
Block a user