The token we thought we deleted came right back out of a layer and the commit history
Goal
You reproduce by hand the four places where secrets remain (build logs, image layers, the Env and history of the image configuration, and commit history), change the design so that secrets are taken out of the image and injected at run time, and then set up a gate that sweeps three places at once.
Why it matters
Accidents in which secrets leak usually come not from malice but from convenience. Someone turned on tracing to debug, writing to a file in the middle of the build was the simplest, and someone committed the configuration file as it was. Each was something meant to save a few minutes, but the result is that the range of people who can read that value widens wholesale. What people lean on most often here is masking, but masking hides only the same string as one it knows — if you change it to base64 or split it into two pieces, it passes straight through. So making it not get printed comes first, and masking is the last net. The image side is quieter. An image contains not only the file system but also how it was made, so even if you write to a file in an intermediate layer and delete it in a later layer, it remains as it is in the earlier layer blob and the value is also written in the configuration's Env and history. Commit history is the same — even if you delete it in the next commit, the blob remains. And the place people go wrong most often is at the end. The response to a secret that has leaked is not to delete it but to revoke it and issue a new one. This is because rewriting history has no effect on copies, forks, caches and backups that have already been cloned, and there is no way to know who read it in the meantime.
Steps
- The working directory is
/root/secrets-lab. First create/root/secrets-lab/secret/api.tokenand write this lab's fake tokensk-labhub-fake-9f3a2b1c8d7e6f5a4b3c2d1e0f9a8b7con the first line. The file permission must be600. Then create/root/secrets-lab/publish.sh <토큰파일> <산출물경로>(token file, artifact path). This script (1) reads the first line of the token file, (2) creates the parent directory of the artifact path if it does not exist and then writes one linepublished <산출물 파일 이름>(artifact file name) into that file, and (3) printsAuthorization: Bearer <토큰>(token) with shell tracing turned on. Give it execute permission. Next runbash /root/secrets-lab/publish.sh /root/secrets-lab/secret/api.token /root/secrets-lab/out/app-1.0.0.txtand save standard output and standard error together to/root/secrets-lab/logs/publish.log, and in/root/secrets-lab/leak-count.txtwrite as a single number the number of lines in that log where the token is visible. It must be two or more lines. - Create
/root/secrets-lab/mask.sh <토큰파일>(token file). It is a filter that reads standard input and replaces every string identical to the token with***MASKED***and sends it to standard output. If the token is empty, it prints a line starting withERRORand ends with 2. Then create/root/secrets-lab/leaky-shapes.sh <토큰파일>. It prints the same value in four shapes, exactly four lines (the order is also as given):raw <토큰>(token),base64 <토큰을 줄바꿈 없이 base64 로 인코딩한 값>(the token encoded in base64 without a newline),split <앞 절반> <뒤 절반>(first half, second half), andhex <토큰 바이트를 16진수 소문자로 이어 붙인 값>(the token bytes concatenated as lowercase hexadecimal). Give both execute permission and save the result ofbash /root/secrets-lab/leaky-shapes.sh /root/secrets-lab/secret/api.token | bash /root/secrets-lab/mask.sh /root/secrets-lab/secret/api.tokento/root/secrets-lab/logs/mask-probe.log. Finally, in/root/secrets-lab/mask-report.txt, write four lines in the form<이름> <masked|leaked>(name, masked or leaked) —maskedif a hidden spot is visible on that line, andleakedif the value is visible as it is. - You build an OCI image by hand without starting a container. Create
/root/secrets-lab/build-image.sh <레이아웃디렉터리> <태그> <토큰파일>(layout directory, tag, token file). There are two layers. The root of the first layer containsapp/main.py(the content is one lineprint("labhub demo")) andetc/build.env(the content is one lineAPI_TOKEN=<토큰>), and the root of the second layer contains only the whiteout entryetc/.wh.build.env(an empty file) that deletes that file. In the configuration JSON, writearchitecturetaken fromuname -m(x86_64becomesamd64,aarch64becomesarm64),osaslinux,config.Envas the two entriesPATH=/usr/local/bin:/usr/bin:/binandAPI_TOKEN=<토큰>,rootfs.diff_idsas the sha256 of the uncompressed tar in layer order, andhistoryas two entries, with the command containing the token written in the first'screated_byand the command that deletes that file written in the second'screated_by. For the manifest'sconfigandlayers, thedigestandsizeare post-compression values, and every blob goes atblobs/sha256/<접두어 없는 다이제스트>(digest without the prefix). Put one manifest inindex.jsonwith the second argument's tag written in the annotationorg.opencontainers.image.ref.name, andoci-layoutis{"imageLayoutVersion":"1.0.0"}. After giving it execute permission, build withbash /root/secrets-lab/build-image.sh /root/secrets-lab/oci/app v1 /root/secrets-lab/secret/api.tokenand save the output ofskopeo inspect oci:/root/secrets-lab/oci/app:v1to/root/secrets-lab/inspect.json. Finally, unpack the first layer blob and write the line ofetc/build.envtaken from it as it is to/root/secrets-lab/layer-leak.txt— it comes out even though it was deleted in the second layer. - You write the rules as data. In
/root/secrets-lab/rules.txt, write three rules, one per line, as<이름> <확장정규식>(name, extended regular expression) (from the space after the name to the end of the line is the regular expression). The names are exactlylabhub-fake-token,aws-access-key-idandprivate-key-header, and they must catch, respectively, this lab's fake token shape (sk-labhub-fake-followed by 32 hexadecimal digits), the AWS access key ID shape (AKIAfollowed by 16 uppercase letters and digits), and a PEM private key header (uppercase letters and spaces between-----BEGINandPRIVATE KEY-----). Then create/root/secrets-lab/image-scan.sh <이미지참조> <규칙파일>(image reference, rules file). It sweeps the four places of the image and prints exactly four lines in this order —env,history,layer,manifest. Each line is<자리> hit <걸린 규칙 이름들>(place, hit, names of the matched rules) if caught, and<자리> cleanif not. It ends with 1 if even one is caught and with 0 if none is caught. If the rules file does not exist or is empty or the image cannot be read, it prints a line whose first line starts withERRORand ends with 2. After giving it execute permission, save the output ofbash /root/secrets-lab/image-scan.sh oci:/root/secrets-lab/oci/app:v1 /root/secrets-lab/rules.txtto/root/secrets-lab/scan-v1.txt. - Create
/root/secrets-lab/build-image-clean.sh <레이아웃디렉터리> <태그>(layout directory, tag). Not taking the token file as an argument is the heart of the design. There is one layer, and the root containsapp/main.py(the same one line as in the earlier step) andetc/app.conf(the content is one lineapi_token_file=/run/secrets/api.token). The configuration'sconfig.Envhas the two entriesPATH=/usr/local/bin:/usr/bin:/binandAPI_TOKEN_FILE=/run/secrets/api.token, andhistoryis one entry with no secret written in it. The rest of the structure (digests, sizes, diff_ids, index.json, oci-layout) is the same as in the earlier step. And create/root/secrets-lab/run.sh. It reads the file that the environment variableAPI_TOKEN_FILE(or/run/secrets/api.tokenif absent) points to and prints one lineready token_sha256=<그 값의 sha256 앞 12자리>(the first 12 digits of that value's sha256). It never prints the value itself. If the file does not exist or is empty, it prints a line starting withERRORand ends with 2. After giving both execute permission, build the image withbash /root/secrets-lab/build-image-clean.sh /root/secrets-lab/oci/app-clean v1and save the output ofbash /root/secrets-lab/image-scan.sh oci:/root/secrets-lab/oci/app-clean:v1 /root/secrets-lab/rules.txtto/root/secrets-lab/scan-clean.txt. All four lines must beclean. - Create
/root/secrets-lab/with-secret.sh <토큰파일> <명령> [인자...](token file, command, optional arguments). This wrapper (1) creates its own temporary directory and narrows its permissions to700, (2) copies the first line of the token file into it as a file with permission600, (3) printssecret_file <만든 파일 경로>(path of the created file) on the first line, (4) runs the command it was given with that path put into the environment variableAPI_TOKEN_FILE, (5) takes the command's exit code as it is as its own exit code, and (6) deletes that directory when it ends, whether it succeeds or fails. It does not print the secret value itself on the screen. If there is no command to run or the token file does not exist or is empty, it prints a line starting withERRORand ends with 2. After giving it execute permission, save the output ofbash /root/secrets-lab/with-secret.sh /root/secrets-lab/secret/api.token bash /root/secrets-lab/run.shto/root/secrets-lab/logs/with-secret.log. And after setting the permission of the/root/secrets-lab/secretdirectory to700, write two lines in/root/secrets-lab/perm-audit.txtin the form<모드> <경로>(mode, path) — the first line is/root/secrets-lab/secretand the second line is/root/secrets-lab/secret/api.token. - Create a practice repository
/root/secrets-lab/repoand stack three commits. The first commit isconfig/app.conf(no secret), the second commit isconfig/deploy.env(the content is one lineDEPLOY_TOKEN=sk-labhub-fake-9f3a2b1c8d7e6f5a4b3c2d1e0f9a8b7c), and the third commit deletes thatconfig/deploy.env. Then pull the deleted value back out of the history — in/root/secrets-lab/git-leak.txtwrite the content of that blob, and in/root/secrets-lab/git-leak-commit.txtthe 40-character hash of the commit that first added that file. Finally, create/root/secrets-lab/scan-repo.sh <저장소> <규칙파일>(repository, rules file). It sweeps every blob attached to every commit, not the working tree, with the rules file from the earlier step, prints one line<규칙 이름> <블롭 해시> <경로>(rule name, blob hash, path) for each one caught, and ends with 1 if even one is caught and with 0 printing nothing if none. If it is not a repository or the rules file does not exist or is empty, it prints a line starting withERRORand ends with 2. After giving it execute permission, save the output ofbash /root/secrets-lab/scan-repo.sh /root/secrets-lab/repo /root/secrets-lab/rules.txtto/root/secrets-lab/repo-scan.txt. - Create
/root/secrets-lab/secret-gate.sh <로그디렉터리> <이미지참조> <저장소> <규칙파일> <보고서파일>(log directory, image reference, repository, rules file, report file). It sweeps three places at once: every file under the log directory, the image, and the repository history (for the image and repository it calls the two checkers built in earlier steps as they are). If even one thing is caught, it printsSECRETS-FOUND <전체 건수>(total count) on the first line of the screen and of the report and ends with 3, and if none, it printsSECRETS-CLEANand ends with 0. If the log directory does not exist or the rules file does not exist or it is not a repository, it prints a line starting withERRORand ends with 1. In the report, after the first line, write the results for the three places in succession, but the lines caught in the logs must show that file's path, and the lines caught in the repository must show that path. If the parent directory of the report path does not exist, create it. After giving it execute permission, run it once withbash /root/secrets-lab/secret-gate.sh /root/secrets-lab/logs oci:/root/secrets-lab/oci/app-clean:v1 /root/secrets-lab/repo /root/secrets-lab/rules.txt /root/secrets-lab/report/gate.txt. Finally, in/root/secrets-lab/revoke-checklist.txt, write exactly five lines in the form<순번> <열쇠> <한 줄 설명>(number, key, one-line explanation). The keys are in this order —revoke,reissue,rotate-consumers,audit-access,prevent-recurrence. Write each explanation in your own words, at least ten characters.
Notes
- The tokens used in this lab are all fake (they start with
sk-labhub-fake-). Do not bring real credentials into this Pod. - In this lab Pod, you cannot start containers — seccomp blocks creating new user namespaces. You do not use
docker,podman run,podman build,buildahorunshare -U. What you use are bash, git, python3.12, jq, skopeo 1.13.3, GNU tar, gzip, sha256sum, od, base64 and openssl (yq,make,go,gitleaksandtrufflehogare not available). - Measured: even without containers you can build an OCI image by hand. If you bundle with tar,
gzip -nit, put it intoblobs/sha256/<다이제스트>, and make the config, manifest, index.json and oci-layout withjq -n, thenskopeo inspectandskopeo copyread it as a real image. If even one digest or size is off, skopeo rejects it, so that is itself the check. - Measured:
skopeo copy <참조> dir:<디렉터리>works offline. The layer blobs and the configuration JSON land as plain files, so you can sweep through the inside of an image without starting a container. - Common mistake: shell tracing goes to standard error, so if you only do
> 파일, it does not land in the log. Give2>&1together with it. - Common mistake: the checker finds nothing, ends with 0 and reports 'clean'. The state with no rules and the state with no violations are different things.
- Common mistake: a tar contains NUL bytes, so
grepoutputs only 'Binary file matches'. Give-aor delete the NULs first. If the regular expression starts with-, you must mark the end of options as ingrep -E -- "$re". - OCI image layout · OCI descriptor · OCI layer (whiteouts) · Docker build secrets · Using secrets in GitHub Actions · git cat-file · git rev-list
When tracing was turned on to debug, the token was printed in the log as it is
The working directory is /root/secrets-lab. First create /root/secrets-lab/secret/api.token and write this lab's fake token sk-labhub-fake-9f3a2b1c8d7e6f5a4b3c2d1e0f9a8b7c on the first line. The file permission must be 600. Then create /root/secrets-lab/publish.sh <토큰파일> <산출물경로> (token file, artifact path). This script (1) reads the first line of the token file, (2) creates the parent directory of the artifact path if it does not exist and then writes one line published <산출물 파일 이름> (artifact file name) into that file, and (3) prints Authorization: Bearer <토큰> (token) with shell tracing turned on. Give it execute permission. Next run bash /root/secrets-lab/publish.sh /root/secrets-lab/secret/api.token /root/secrets-lab/out/app-1.0.0.txt and save standard output and standard error together to /root/secrets-lab/logs/publish.log, and in /root/secrets-lab/leak-count.txt write as a single number the number of lines in that log where the token is visible. It must be two or more lines.
Shell tracing is turned on with set -x and off with set +x. Tracing goes to standard error, so you must receive both together like > 파일 2>&1 to see it. There is the one line that prints the value and the one trace of that line, so the same value remains in two places. When you create a file with narrowed permissions, put umask 077 in a subshell or chmod after creating it. grep -c counts the number of lines.
Masking was attached, but the value converted to base64 passed straight through
Create /root/secrets-lab/mask.sh <토큰파일> (token file). It is a filter that reads standard input and replaces every string identical to the token with ***MASKED*** and sends it to standard output. If the token is empty, it prints a line starting with ERROR and ends with 2. Then create /root/secrets-lab/leaky-shapes.sh <토큰파일>. It prints the same value in four shapes, exactly four lines (the order is also as given): raw <토큰> (token), base64 <토큰을 줄바꿈 없이 base64 로 인코딩한 값> (the token encoded in base64 without a newline), split <앞 절반> <뒤 절반> (first half, second half), and hex <토큰 바이트를 16진수 소문자로 이어 붙인 값> (the token bytes concatenated as lowercase hexadecimal). Give both execute permission and save the result of bash /root/secrets-lab/leaky-shapes.sh /root/secrets-lab/secret/api.token | bash /root/secrets-lab/mask.sh /root/secrets-lab/secret/api.token to /root/secrets-lab/logs/mask-probe.log. Finally, in /root/secrets-lab/mask-report.txt, write four lines in the form <이름> <masked|leaked> (name, masked or leaked) — masked if a hidden spot is visible on that line, and leaked if the value is visible as it is.
If you substitute with sed, you have to escape each /, & and . in the value. It is safer to replace the string as it is with a single line of Python. base64 without a newline comes out with base64 -w0. For hexadecimal, delete the spaces and newlines from the output of od -An -tx1. Cut the halves with bash's substring ${변수:시작:길이}. Confirming with four lines that what masking hides is only the one 'known string' is all there is to this step.
It was deleted in the next layer, yet it was still in the earlier layer blob
You build an OCI image by hand without starting a container. Create /root/secrets-lab/build-image.sh <레이아웃디렉터리> <태그> <토큰파일> (layout directory, tag, token file). There are two layers. The root of the first layer contains app/main.py (the content is one line print("labhub demo")) and etc/build.env (the content is one line API_TOKEN=<토큰>), and the root of the second layer contains only the whiteout entry etc/.wh.build.env (an empty file) that deletes that file. In the configuration JSON, write architecture taken from uname -m (x86_64 becomes amd64, aarch64 becomes arm64), os as linux, config.Env as the two entries PATH=/usr/local/bin:/usr/bin:/bin and API_TOKEN=<토큰>, rootfs.diff_ids as the sha256 of the uncompressed tar in layer order, and history as two entries, with the command containing the token written in the first's created_by and the command that deletes that file written in the second's created_by. For the manifest's config and layers, the digest and size are post-compression values, and every blob goes at blobs/sha256/<접두어 없는 다이제스트> (digest without the prefix). Put one manifest in index.json with the second argument's tag written in the annotation org.opencontainers.image.ref.name, and oci-layout is {"imageLayoutVersion":"1.0.0"}. After giving it execute permission, build with bash /root/secrets-lab/build-image.sh /root/secrets-lab/oci/app v1 /root/secrets-lab/secret/api.token and save the output of skopeo inspect oci:/root/secrets-lab/oci/app:v1 to /root/secrets-lab/inspect.json. Finally, unpack the first layer blob and write the line of etc/build.env taken from it as it is to /root/secrets-lab/layer-leak.txt — it comes out even though it was deleted in the second layer.
A layer is just a tar. To bundle it reproducibly, give --sort=name --mtime=@1735689600 --owner=0 --group=0 --numeric-owner and compress with gzip -n -9. You need three values from one layer — the sha256 of the uncompressed tar (diff_id), the sha256 of the compressed bytes (the blob name and the manifest's digest), and the compressed size (size). If even one of the three is off, skopeo copy rejects it, so that is itself the check. If you make the JSON with jq -n --arg, you do not have to match the quotes by hand. The manifest blob also goes in the same place as the other blobs, and index.json points to its digest and size. Extracting just one file from a layer blob is gzip -dc <블롭> | tar -xO ./etc/build.env.
There was no need to dig through layers; it was written in the configuration's Env and history
You write the rules as data. In /root/secrets-lab/rules.txt, write three rules, one per line, as <이름> <확장정규식> (name, extended regular expression) (from the space after the name to the end of the line is the regular expression). The names are exactly labhub-fake-token, aws-access-key-id and private-key-header, and they must catch, respectively, this lab's fake token shape (sk-labhub-fake- followed by 32 hexadecimal digits), the AWS access key ID shape (AKIA followed by 16 uppercase letters and digits), and a PEM private key header (uppercase letters and spaces between -----BEGIN and PRIVATE KEY-----). Then create /root/secrets-lab/image-scan.sh <이미지참조> <규칙파일> (image reference, rules file). It sweeps the four places of the image and prints exactly four lines in this order — env, history, layer, manifest. Each line is <자리> hit <걸린 규칙 이름들> (place, hit, names of the matched rules) if caught, and <자리> clean if not. It ends with 1 if even one is caught and with 0 if none is caught. If the rules file does not exist or is empty or the image cannot be read, it prints a line whose first line starts with ERROR and ends with 2. After giving it execute permission, save the output of bash /root/secrets-lab/image-scan.sh oci:/root/secrets-lab/oci/app:v1 /root/secrets-lab/rules.txt to /root/secrets-lab/scan-v1.txt.
To look inside an image without a container, skopeo copy --insecure-policy <참조> dir:<임시디렉터리> is the easiest. Then manifest.json and the blobs land as plain files, and a blob file's name is that blob's digest. The configuration blob is pointed to by the manifest's .config.digest. A layer blob is gzip, so you have to unpack it to look, and a tar has NUL bytes mixed in, so give grep the -a option or delete the NULs beforehand. If the regular expression starts with -, you must mark the end of options as in grep -E -- "$re". A checker that ends with 0 even though the rules file is empty reports 'there is no problem' and 'it saw nothing' as the same thing — that is why you keep separate exit codes.
After taking the secret out of the image and giving it at run time, the same check went quiet
Create /root/secrets-lab/build-image-clean.sh <레이아웃디렉터리> <태그> (layout directory, tag). Not taking the token file as an argument is the heart of the design. There is one layer, and the root contains app/main.py (the same one line as in the earlier step) and etc/app.conf (the content is one line api_token_file=/run/secrets/api.token). The configuration's config.Env has the two entries PATH=/usr/local/bin:/usr/bin:/bin and API_TOKEN_FILE=/run/secrets/api.token, and history is one entry with no secret written in it. The rest of the structure (digests, sizes, diff_ids, index.json, oci-layout) is the same as in the earlier step. And create /root/secrets-lab/run.sh. It reads the file that the environment variable API_TOKEN_FILE (or /run/secrets/api.token if absent) points to and prints one line ready token_sha256=<그 값의 sha256 앞 12자리> (the first 12 digits of that value's sha256). It never prints the value itself. If the file does not exist or is empty, it prints a line starting with ERROR and ends with 2. After giving both execute permission, build the image with bash /root/secrets-lab/build-image-clean.sh /root/secrets-lab/oci/app-clean v1 and save the output of bash /root/secrets-lab/image-scan.sh oci:/root/secrets-lab/oci/app-clean:v1 /root/secrets-lab/rules.txt to /root/secrets-lab/scan-clean.txt. All four lines must be clean.
Copy the builder of the earlier step, cut the layers to one, and change the place where the secret used to go into a 'path to read'. What the image holds is not the value but a contract — a single path that promises where it will be. Bringing the value to that place is the execution environment's job, and so development and production can share the same image as it is. sha256sum accepts standard input too. Also confirm that when the checker gives clean on all four lines, the exit code is 0.
A secret passed as a file was still in that place after the job ended
Create /root/secrets-lab/with-secret.sh <토큰파일> <명령> [인자...] (token file, command, optional arguments). This wrapper (1) creates its own temporary directory and narrows its permissions to 700, (2) copies the first line of the token file into it as a file with permission 600, (3) prints secret_file <만든 파일 경로> (path of the created file) on the first line, (4) runs the command it was given with that path put into the environment variable API_TOKEN_FILE, (5) takes the command's exit code as it is as its own exit code, and (6) deletes that directory when it ends, whether it succeeds or fails. It does not print the secret value itself on the screen. If there is no command to run or the token file does not exist or is empty, it prints a line starting with ERROR and ends with 2. After giving it execute permission, save the output of bash /root/secrets-lab/with-secret.sh /root/secrets-lab/secret/api.token bash /root/secrets-lab/run.sh to /root/secrets-lab/logs/with-secret.log. And after setting the permission of the /root/secrets-lab/secret directory to 700, write two lines in /root/secrets-lab/perm-audit.txt in the form <모드> <경로> (mode, path) — the first line is /root/secrets-lab/secret and the second line is /root/secrets-lab/secret/api.token.
mktemp -d creates the directory in a place others cannot enter, but the permissions when you create a file inside it are decided by the umask — put ( umask 077; ... ) in a subshell or chmod after creating it. Leave the deleting to trap. If you set only EXIT, it remains when it dies from a signal, so also set INT and TERM. Even if the command fails, the wrapper must not die first, so temporarily release set -e, capture the exit code, and send it out as it is at the end. Do not forget either that narrowing only the file permission and leaving the directory open is useless. And even doing this is not the end — this wrapper cannot know what a process that has already read that value did.
A file deleted in a later commit came out as it was through git cat-file
Create a practice repository /root/secrets-lab/repo and stack three commits. The first commit is config/app.conf (no secret), the second commit is config/deploy.env (the content is one line DEPLOY_TOKEN=sk-labhub-fake-9f3a2b1c8d7e6f5a4b3c2d1e0f9a8b7c), and the third commit deletes that config/deploy.env. Then pull the deleted value back out of the history — in /root/secrets-lab/git-leak.txt write the content of that blob, and in /root/secrets-lab/git-leak-commit.txt the 40-character hash of the commit that first added that file. Finally, create /root/secrets-lab/scan-repo.sh <저장소> <규칙파일> (repository, rules file). It sweeps every blob attached to every commit, not the working tree, with the rules file from the earlier step, prints one line <규칙 이름> <블롭 해시> <경로> (rule name, blob hash, path) for each one caught, and ends with 1 if even one is caught and with 0 printing nothing if none. If it is not a repository or the rules file does not exist or is empty, it prints a line starting with ERROR and ends with 2. After giving it execute permission, save the output of bash /root/secrets-lab/scan-repo.sh /root/secrets-lab/repo /root/secrets-lab/rules.txt to /root/secrets-lab/repo-scan.txt.
When you create the repository, you must first set git config user.email and user.name inside the repository for commits to work. Every object in the history is output by git rev-list --objects --all as <해시> <경로> (hash, path). You can tell which of them are blobs in one go by feeding that list as it is into git cat-file --batch-check (%(rest) returns the path). The content is git cat-file -p <해시>. Find the commit that first added a file with git log --diff-filter=A. The most common mistake when building a checker is to find nothing, end with 0 and report 'clean' — separate the state with no rules and the state with no violations by exit code.
It set up a gate that sweeps three places at once and wrote down revocation first
Create /root/secrets-lab/secret-gate.sh <로그디렉터리> <이미지참조> <저장소> <규칙파일> <보고서파일> (log directory, image reference, repository, rules file, report file). It sweeps three places at once: every file under the log directory, the image, and the repository history (for the image and repository it calls the two checkers built in earlier steps as they are). If even one thing is caught, it prints SECRETS-FOUND <전체 건수> (total count) on the first line of the screen and of the report and ends with 3, and if none, it prints SECRETS-CLEAN and ends with 0. If the log directory does not exist or the rules file does not exist or it is not a repository, it prints a line starting with ERROR and ends with 1. In the report, after the first line, write the results for the three places in succession, but the lines caught in the logs must show that file's path, and the lines caught in the repository must show that path. If the parent directory of the report path does not exist, create it. After giving it execute permission, run it once with bash /root/secrets-lab/secret-gate.sh /root/secrets-lab/logs oci:/root/secrets-lab/oci/app-clean:v1 /root/secrets-lab/repo /root/secrets-lab/rules.txt /root/secrets-lab/report/gate.txt. Finally, in /root/secrets-lab/revoke-checklist.txt, write exactly five lines in the form <순번> <열쇠> <한 줄 설명> (number, key, one-line explanation). The keys are in this order — revoke, reissue, rotate-consumers, audit-access, prevent-recurrence. Write each explanation in your own words, at least ten characters.
When the gate calls the checkers next to it, it finds them from its own location — here=$(cd "$(dirname "$0")" && pwd). The two checkers' exit codes are three: 0 (clean), 1 (caught) and 2 (error), so if a 2 comes, the gate must stop too. The count is the sum of the lines from the three places. The image checker always prints four lines, so you must count only the lines among them that were caught. And the real conclusion of this step is not in the script but in the checklist — fixing and erasing the history has no effect on copies, forks, caches and backups that have already been cloned, and there is no way to know who read it in the meantime. That is why the first line is revocation.