[{"data":1,"prerenderedAt":2912},["ShallowReactive",2],{"/blog/how-gitlab-can-help-mitigate-deletion-open-source-images-docker-hub":3,"navigation-en-us":2125,"banner-en-us":2552,"footer-en-us":2562,"blog-post-authors-en-us-Michael Friedrich":2807,"blog-related-posts-en-us-how-gitlab-can-help-mitigate-deletion-open-source-images-docker-hub":2822,"blog-promotions-en-us":2848,"next-steps-en-us":2902},{"id":4,"title":5,"authors":6,"body":8,"category":2103,"date":2104,"description":2105,"extension":2106,"externalUrl":2107,"faq":2107,"featured":2108,"heroImage":2109,"meta":2110,"navigation":153,"path":2111,"seo":2112,"slug":2117,"stem":2118,"tags":2119,"template":2123,"updatedDate":2107,"__hash__":2124},"blogPosts/en-us/blog/how-gitlab-can-help-mitigate-deletion-open-source-images-docker-hub.md","How GitLab can help mitigate deletion of open source container images on Docker Hub",[7],"Michael Friedrich",{"type":9,"value":10,"toc":2092},"minimark",[11,22,34,51,56,109,257,288,291,296,311,328,337,1038,1045,1080,1083,1237,1240,1253,1260,1266,1370,1374,1383,1386,1403,1422,1475,1481,1486,1489,1493,1501,1509,1523,1618,1627,1634,1638,1650,1676,1680,1694,1700,1708,1721,1794,1797,1826,1834,1840,1845,1849,1864,1892,1900,1995,2001,2022,2036,2040,2049,2058,2065,2074,2088],[12,13,14,15,21],"p",{},"Docker, Inc. shared an email update to Docker Hub users that it will ",[16,17,20],"a",{"href":18,"rel":19},"https://www.infoworld.com/article/3690890/docker-sunsets-free-team-subscriptions-roiling-open-source-projects.html",[],"sunset Free Team organizations",". If accounts do not upgrade to a paid plan before April 14, 2023, their organization's images may be deleted after 30 days. This change can affect open source organizations that publish their images on Docker Hub, as well as consumers of these container images, used in CI/CD pipelines, Kubernetes cluster deployments, or docker-compose demo environments. This blog post discusses tools and features on the GitLab DevSecOps platform to help users analyze and mitigate the potential impact on production environments.",[12,23,24],{},[25,26,27,28,33],"em",{},"Update (March 20, 2023): Docker, Inc. ",[16,29,32],{"href":30,"rel":31},"https://www.docker.com/blog/we-apologize-we-did-a-terrible-job-announcing-the-end-of-docker-free-teams/",[],"published an apology blog post",", including a FAQ, and clarifies that the company will not delete container images by themselves. Maintainers can migrate to a personal account, join the Docker-sponsored open source program, or opt into a paid plan. If open source container image maintainers do nothing, this leads into another issue: Stale container images can become a security problem. The following blog post can help with security analysis and migration too.",[12,35,36],{},[25,37,38,39,44,45,50],{},"Update (March 27, 2023): On March 24, 2023, Docker, Inc. ",[16,40,43],{"href":41,"rel":42},"https://www.docker.com/blog/no-longer-sunsetting-the-free-team-plan/",[],"published another blog post"," announcing the reversal of the decision to sunset the Free team plan and updated its ",[16,46,49],{"href":47,"rel":48},"https://www.docker.com/developers/free-team-faq/",[],"FAQ for Free Team organization",". While this is a welcome development for the entire community, it is still crucial to ensure the reliability of your software development lifecycle by ensuring redundancies are in place for your container registries, as detailed in this blog post.",[52,53,55],"h3",{"id":54},"inventory-of-used-container-images","Inventory of used container images",[12,57,58,59,68,69,76,77,79,80,83,84,87,88,90,91,96,97,102,103,108],{},"CI/CD pipelines in GitLab can execute jobs in containers. This is specified by the ",[16,60,63,67],{"href":61,"rel":62},"https://docs.gitlab.com/ci/yaml/#image",[],[64,65,66],"code",{},"image"," keyword"," in jobs, job templates, or as a global ",[16,70,73],{"href":71,"rel":72},"https://docs.gitlab.com/ci/yaml/#default",[],[64,74,75],{},"default"," attribute. For the first iteration, you can clone a GitLab project locally, and search for the ",[64,78,66],{}," string in all CI/CD configuration files. The following example shows how to execute the ",[64,81,82],{},"find"," command on the command line interface (CLI), searching for files matching the name pattern ",[64,85,86],{},"*ci.yml",", and looking for the ",[64,89,66],{}," string in the file content. The command line prints a list of search pattern matches, and the corresponding file name to the standard output. The example inspects the ",[16,92,95],{"href":93,"rel":94},"https://gitlab.com/gitlab-com/www-gitlab-com",[],"project"," for the ",[16,98,101],{"href":99,"rel":100},"https://handbook.gitlab.com/handbook/",[],"GitLab handbook"," and ",[16,104,107],{"href":105,"rel":106},"https://about.gitlab.com/",[],"website"," to analyze whether its CI/CD deployment pipelines could be affected by the Docker Hub changes.",[110,111,116],"pre",{"className":112,"code":113,"language":114,"meta":115,"style":115},"language-bash shiki shiki-themes github-light","$ git clone https://gitlab.com/gitlab-com/www-gitlab-com && cd www-gitlab-com\n\n$ find . -type f -iname '*ci.yml' -exec sh -c \"grep 'image:' '{}' && echo {}\" \\;\n\n  image: registry.gitlab.com/gitlab-org/gitlab-build-images:www-gitlab-com-debian-${DEBIAN_VERSION}-ruby-3.0-node-16\n  image: alpine:edge\n  image: alpine:edge\n  image: debian:stable-slim\n  image: debian:stable-slim\n  image: registry.gitlab.com/gitlab-org/gitlab-build-images:danger\n./.gitlab-ci.yml\n","bash","",[64,117,118,148,155,193,198,213,221,228,236,243,251],{"__ignoreMap":115},[119,120,123,127,131,134,137,141,145],"span",{"class":121,"line":122},"line",1,[119,124,126],{"class":125},"s7eDp","$",[119,128,130],{"class":129},"sYBdl"," git",[119,132,133],{"class":129}," clone",[119,135,136],{"class":129}," https://gitlab.com/gitlab-com/www-gitlab-com",[119,138,140],{"class":139},"sgsFI"," && ",[119,142,144],{"class":143},"sYu0t","cd",[119,146,147],{"class":129}," www-gitlab-com\n",[119,149,151],{"class":121,"line":150},2,[119,152,154],{"emptyLinePlaceholder":153},true,"\n",[119,156,158,160,163,166,169,172,175,178,181,184,187,190],{"class":121,"line":157},3,[119,159,126],{"class":125},[119,161,162],{"class":129}," find",[119,164,165],{"class":129}," .",[119,167,168],{"class":143}," -type",[119,170,171],{"class":129}," f",[119,173,174],{"class":143}," -iname",[119,176,177],{"class":129}," '*ci.yml'",[119,179,180],{"class":143}," -exec",[119,182,183],{"class":129}," sh",[119,185,186],{"class":143}," -c",[119,188,189],{"class":129}," \"grep 'image:' '{}' && echo {}\"",[119,191,192],{"class":143}," \\;\n",[119,194,196],{"class":121,"line":195},4,[119,197,154],{"emptyLinePlaceholder":153},[119,199,201,204,207,210],{"class":121,"line":200},5,[119,202,203],{"class":125},"  image:",[119,205,206],{"class":129}," registry.gitlab.com/gitlab-org/gitlab-build-images:www-gitlab-com-debian-",[119,208,209],{"class":139},"${DEBIAN_VERSION}",[119,211,212],{"class":129},"-ruby-3.0-node-16\n",[119,214,216,218],{"class":121,"line":215},6,[119,217,203],{"class":125},[119,219,220],{"class":129}," alpine:edge\n",[119,222,224,226],{"class":121,"line":223},7,[119,225,203],{"class":125},[119,227,220],{"class":129},[119,229,231,233],{"class":121,"line":230},8,[119,232,203],{"class":125},[119,234,235],{"class":129}," debian:stable-slim\n",[119,237,239,241],{"class":121,"line":238},9,[119,240,203],{"class":125},[119,242,235],{"class":129},[119,244,246,248],{"class":121,"line":245},10,[119,247,203],{"class":125},[119,249,250],{"class":129}," registry.gitlab.com/gitlab-org/gitlab-build-images:danger\n",[119,252,254],{"class":121,"line":253},11,[119,255,256],{"class":125},"./.gitlab-ci.yml\n",[12,258,259,260,265,266,271,272,275,276,279,280,283,284,287],{},"A ",[16,261,264],{"href":262,"rel":263},"https://news.ycombinator.com/item?id=35168802",[],"discussion on Hacker News"," mentions that \"official Docker images\" are not affected, but this is not officially confirmed by Docker yet. ",[16,267,270],{"href":268,"rel":269},"https://hub.docker.com/u/library",[],"Official Docker images"," do not use a namespace prefix, i.e. ",[64,273,274],{},"namespace/imagename"," but instead ",[64,277,278],{},"debian:\u003Ctagname>"," for example. ",[64,281,282],{},"registry.gitlab.com/gitlab-org/gitlab-build-images:danger"," uses a full URL image string, which includes the image registry server domain, ",[64,285,286],{},"registry.gitlab.com"," in the shown example.",[12,289,290],{},"If there is no full URL prefix in the image string, this is an indicator that this image could be pulled from Docker Hub by default. There might be other infrastructure safety nets put in place, for example a cloud provider registry which caches the Docker Hub images (Google Cloud, AWS, Azure, etc.).",[292,293,295],"h4",{"id":294},"advanced-search-for-images","Advanced search for images",[12,297,298,299,304,305,310],{},"You can use the ",[16,300,303],{"href":301,"rel":302},"https://docs.gitlab.com/api/lint/#validate-a-projects-ci-configuration",[],"project lint API endpoint"," to fetch the CI configuration. The following script uses the ",[16,306,309],{"href":307,"rel":308},"https://python-gitlab.readthedocs.io/en/stable/gl_objects/ci_lint.html",[],"python-gitlab API library"," to implement the API endpoint:",[312,313,314,318,325],"ol",{},[315,316,317],"li",{},"Collect all projects from either a single project ID, a group ID with projects, or from the instance.",[315,319,320,321,324],{},"Run the ",[64,322,323],{},"project.ci_lint.get()"," method to get a merged yaml configuration for CI/CD from the current GitLab project.",[315,326,327],{},"Parse the yaml content and print only the job names, and the image keys.",[12,329,330,331,336],{},"The ",[16,332,335],{"href":333,"rel":334},"https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/get_all_cicd_job_images.py",[],"full script is located here",", and is open source, licensed under MIT.",[110,338,342],{"className":339,"code":340,"language":341,"meta":115,"style":115},"language-python shiki shiki-themes github-light","#!/usr/bin/env python\n\nimport gitlab\nimport os\nimport sys\nimport yaml\n\nGITLAB_SERVER = os.environ.get('GL_SERVER', 'https://gitlab.com')\nGITLAB_TOKEN = os.environ.get('GL_TOKEN') # token requires developer permissions\nPROJECT_ID = os.environ.get('GL_PROJECT_ID') #optional\n# https://gitlab.com/gitlab-da/use-cases/docker\nGROUP_ID = os.environ.get('GL_GROUP_ID', 65096153) #optional\n\n#################\n# Main\n\nif __name__ == \"__main__\":\n    if not GITLAB_TOKEN:\n        print(\"🤔 Please set the GL_TOKEN env variable.\")\n        sys.exit(1)\n\n    gl = gitlab.Gitlab(GITLAB_SERVER, private_token=GITLAB_TOKEN)\n\n    # Collect all projects, or prefer projects from a group id, or a project id\n    projects = []\n\n    # Direct project ID\n    if PROJECT_ID:\n        projects.append(gl.projects.get(PROJECT_ID))\n\n    # Groups and projects inside\n    elif GROUP_ID:\n        group = gl.groups.get(GROUP_ID)\n\n        for project in group.projects.list(include_subgroups=True, all=True):\n            # https://python-gitlab.readthedocs.io/en/stable/gl_objects/groups.html#examples\n            manageable_project = gl.projects.get(project.id)\n            projects.append(manageable_project)\n\n    # All projects on the instance (may take a while to process)\n    else:\n        projects = gl.projects.list(get_all=True)\n\n    print(\"# Summary of projects and their CI/CD image usage\")\n\n    # Loop over projects, fetch .gitlab-ci.yml, run the linter to get the full translated config, and extract the `image:` setting\n    for project in projects:\n\n        print(\"# Project: {name}, ID: {id}\\n\\n\".format(name=project.name_with_namespace, id=project.id))\n\n        # https://python-gitlab.readthedocs.io/en/stable/gl_objects/ci_lint.html\n        lint_result = project.ci_lint.get()\n\n        data = yaml.safe_load(lint_result.merged_yaml)\n\n        for d in data:\n            print(\"Job name: {n}\".format(n=d))\n            for attr in data[d]:\n                if 'image' in attr:\n                    print(\"Image: {i}\".format(i=data[d][attr]))\n\n        print(\"\\n\\n\")\n\nsys.exit(0)\n","python",[64,343,344,350,354,363,370,377,384,388,411,429,446,451,473,478,484,490,495,513,527,541,552,557,583,588,594,605,610,616,626,637,642,648,659,674,679,714,720,731,737,742,748,756,776,781,794,799,805,818,823,864,869,875,886,891,902,907,920,946,960,975,1001,1006,1022,1027],{"__ignoreMap":115},[119,345,346],{"class":121,"line":122},[119,347,349],{"class":348},"sAwPA","#!/usr/bin/env python\n",[119,351,352],{"class":121,"line":150},[119,353,154],{"emptyLinePlaceholder":153},[119,355,356,360],{"class":121,"line":157},[119,357,359],{"class":358},"sD7c4","import",[119,361,362],{"class":139}," gitlab\n",[119,364,365,367],{"class":121,"line":195},[119,366,359],{"class":358},[119,368,369],{"class":139}," os\n",[119,371,372,374],{"class":121,"line":200},[119,373,359],{"class":358},[119,375,376],{"class":139}," sys\n",[119,378,379,381],{"class":121,"line":215},[119,380,359],{"class":358},[119,382,383],{"class":139}," yaml\n",[119,385,386],{"class":121,"line":223},[119,387,154],{"emptyLinePlaceholder":153},[119,389,390,393,396,399,402,405,408],{"class":121,"line":230},[119,391,392],{"class":143},"GITLAB_SERVER",[119,394,395],{"class":358}," =",[119,397,398],{"class":139}," os.environ.get(",[119,400,401],{"class":129},"'GL_SERVER'",[119,403,404],{"class":139},", ",[119,406,407],{"class":129},"'https://gitlab.com'",[119,409,410],{"class":139},")\n",[119,412,413,416,418,420,423,426],{"class":121,"line":238},[119,414,415],{"class":143},"GITLAB_TOKEN",[119,417,395],{"class":358},[119,419,398],{"class":139},[119,421,422],{"class":129},"'GL_TOKEN'",[119,424,425],{"class":139},") ",[119,427,428],{"class":348},"# token requires developer permissions\n",[119,430,431,434,436,438,441,443],{"class":121,"line":245},[119,432,433],{"class":143},"PROJECT_ID",[119,435,395],{"class":358},[119,437,398],{"class":139},[119,439,440],{"class":129},"'GL_PROJECT_ID'",[119,442,425],{"class":139},[119,444,445],{"class":348},"#optional\n",[119,447,448],{"class":121,"line":253},[119,449,450],{"class":348},"# https://gitlab.com/gitlab-da/use-cases/docker\n",[119,452,454,457,459,461,464,466,469,471],{"class":121,"line":453},12,[119,455,456],{"class":143},"GROUP_ID",[119,458,395],{"class":358},[119,460,398],{"class":139},[119,462,463],{"class":129},"'GL_GROUP_ID'",[119,465,404],{"class":139},[119,467,468],{"class":143},"65096153",[119,470,425],{"class":139},[119,472,445],{"class":348},[119,474,476],{"class":121,"line":475},13,[119,477,154],{"emptyLinePlaceholder":153},[119,479,481],{"class":121,"line":480},14,[119,482,483],{"class":348},"#################\n",[119,485,487],{"class":121,"line":486},15,[119,488,489],{"class":348},"# Main\n",[119,491,493],{"class":121,"line":492},16,[119,494,154],{"emptyLinePlaceholder":153},[119,496,498,501,504,507,510],{"class":121,"line":497},17,[119,499,500],{"class":358},"if",[119,502,503],{"class":143}," __name__",[119,505,506],{"class":358}," ==",[119,508,509],{"class":129}," \"__main__\"",[119,511,512],{"class":139},":\n",[119,514,516,519,522,525],{"class":121,"line":515},18,[119,517,518],{"class":358},"    if",[119,520,521],{"class":358}," not",[119,523,524],{"class":143}," GITLAB_TOKEN",[119,526,512],{"class":139},[119,528,530,533,536,539],{"class":121,"line":529},19,[119,531,532],{"class":143},"        print",[119,534,535],{"class":139},"(",[119,537,538],{"class":129},"\"🤔 Please set the GL_TOKEN env variable.\"",[119,540,410],{"class":139},[119,542,544,547,550],{"class":121,"line":543},20,[119,545,546],{"class":139},"        sys.exit(",[119,548,549],{"class":143},"1",[119,551,410],{"class":139},[119,553,555],{"class":121,"line":554},21,[119,556,154],{"emptyLinePlaceholder":153},[119,558,560,563,566,569,571,573,577,579,581],{"class":121,"line":559},22,[119,561,562],{"class":139},"    gl ",[119,564,565],{"class":358},"=",[119,567,568],{"class":139}," gitlab.Gitlab(",[119,570,392],{"class":143},[119,572,404],{"class":139},[119,574,576],{"class":575},"sqxcx","private_token",[119,578,565],{"class":358},[119,580,415],{"class":143},[119,582,410],{"class":139},[119,584,586],{"class":121,"line":585},23,[119,587,154],{"emptyLinePlaceholder":153},[119,589,591],{"class":121,"line":590},24,[119,592,593],{"class":348},"    # Collect all projects, or prefer projects from a group id, or a project id\n",[119,595,597,600,602],{"class":121,"line":596},25,[119,598,599],{"class":139},"    projects ",[119,601,565],{"class":358},[119,603,604],{"class":139}," []\n",[119,606,608],{"class":121,"line":607},26,[119,609,154],{"emptyLinePlaceholder":153},[119,611,613],{"class":121,"line":612},27,[119,614,615],{"class":348},"    # Direct project ID\n",[119,617,619,621,624],{"class":121,"line":618},28,[119,620,518],{"class":358},[119,622,623],{"class":143}," PROJECT_ID",[119,625,512],{"class":139},[119,627,629,632,634],{"class":121,"line":628},29,[119,630,631],{"class":139},"        projects.append(gl.projects.get(",[119,633,433],{"class":143},[119,635,636],{"class":139},"))\n",[119,638,640],{"class":121,"line":639},30,[119,641,154],{"emptyLinePlaceholder":153},[119,643,645],{"class":121,"line":644},31,[119,646,647],{"class":348},"    # Groups and projects inside\n",[119,649,651,654,657],{"class":121,"line":650},32,[119,652,653],{"class":358},"    elif",[119,655,656],{"class":143}," GROUP_ID",[119,658,512],{"class":139},[119,660,662,665,667,670,672],{"class":121,"line":661},33,[119,663,664],{"class":139},"        group ",[119,666,565],{"class":358},[119,668,669],{"class":139}," gl.groups.get(",[119,671,456],{"class":143},[119,673,410],{"class":139},[119,675,677],{"class":121,"line":676},34,[119,678,154],{"emptyLinePlaceholder":153},[119,680,682,685,688,691,694,697,699,702,704,707,709,711],{"class":121,"line":681},35,[119,683,684],{"class":358},"        for",[119,686,687],{"class":139}," project ",[119,689,690],{"class":358},"in",[119,692,693],{"class":139}," group.projects.list(",[119,695,696],{"class":575},"include_subgroups",[119,698,565],{"class":358},[119,700,701],{"class":143},"True",[119,703,404],{"class":139},[119,705,706],{"class":575},"all",[119,708,565],{"class":358},[119,710,701],{"class":143},[119,712,713],{"class":139},"):\n",[119,715,717],{"class":121,"line":716},36,[119,718,719],{"class":348},"            # https://python-gitlab.readthedocs.io/en/stable/gl_objects/groups.html#examples\n",[119,721,723,726,728],{"class":121,"line":722},37,[119,724,725],{"class":139},"            manageable_project ",[119,727,565],{"class":358},[119,729,730],{"class":139}," gl.projects.get(project.id)\n",[119,732,734],{"class":121,"line":733},38,[119,735,736],{"class":139},"            projects.append(manageable_project)\n",[119,738,740],{"class":121,"line":739},39,[119,741,154],{"emptyLinePlaceholder":153},[119,743,745],{"class":121,"line":744},40,[119,746,747],{"class":348},"    # All projects on the instance (may take a while to process)\n",[119,749,751,754],{"class":121,"line":750},41,[119,752,753],{"class":358},"    else",[119,755,512],{"class":139},[119,757,759,762,764,767,770,772,774],{"class":121,"line":758},42,[119,760,761],{"class":139},"        projects ",[119,763,565],{"class":358},[119,765,766],{"class":139}," gl.projects.list(",[119,768,769],{"class":575},"get_all",[119,771,565],{"class":358},[119,773,701],{"class":143},[119,775,410],{"class":139},[119,777,779],{"class":121,"line":778},43,[119,780,154],{"emptyLinePlaceholder":153},[119,782,784,787,789,792],{"class":121,"line":783},44,[119,785,786],{"class":143},"    print",[119,788,535],{"class":139},[119,790,791],{"class":129},"\"# Summary of projects and their CI/CD image usage\"",[119,793,410],{"class":139},[119,795,797],{"class":121,"line":796},45,[119,798,154],{"emptyLinePlaceholder":153},[119,800,802],{"class":121,"line":801},46,[119,803,804],{"class":348},"    # Loop over projects, fetch .gitlab-ci.yml, run the linter to get the full translated config, and extract the `image:` setting\n",[119,806,808,811,813,815],{"class":121,"line":807},47,[119,809,810],{"class":358},"    for",[119,812,687],{"class":139},[119,814,690],{"class":358},[119,816,817],{"class":139}," projects:\n",[119,819,821],{"class":121,"line":820},48,[119,822,154],{"emptyLinePlaceholder":153},[119,824,826,828,830,833,836,839,842,845,848,851,853,856,859,861],{"class":121,"line":825},49,[119,827,532],{"class":143},[119,829,535],{"class":139},[119,831,832],{"class":129},"\"# Project: ",[119,834,835],{"class":143},"{name}",[119,837,838],{"class":129},", ID: ",[119,840,841],{"class":143},"{id}\\n\\n",[119,843,844],{"class":129},"\"",[119,846,847],{"class":139},".format(",[119,849,850],{"class":575},"name",[119,852,565],{"class":358},[119,854,855],{"class":139},"project.name_with_namespace, ",[119,857,858],{"class":575},"id",[119,860,565],{"class":358},[119,862,863],{"class":139},"project.id))\n",[119,865,867],{"class":121,"line":866},50,[119,868,154],{"emptyLinePlaceholder":153},[119,870,872],{"class":121,"line":871},51,[119,873,874],{"class":348},"        # https://python-gitlab.readthedocs.io/en/stable/gl_objects/ci_lint.html\n",[119,876,878,881,883],{"class":121,"line":877},52,[119,879,880],{"class":139},"        lint_result ",[119,882,565],{"class":358},[119,884,885],{"class":139}," project.ci_lint.get()\n",[119,887,889],{"class":121,"line":888},53,[119,890,154],{"emptyLinePlaceholder":153},[119,892,894,897,899],{"class":121,"line":893},54,[119,895,896],{"class":139},"        data ",[119,898,565],{"class":358},[119,900,901],{"class":139}," yaml.safe_load(lint_result.merged_yaml)\n",[119,903,905],{"class":121,"line":904},55,[119,906,154],{"emptyLinePlaceholder":153},[119,908,910,912,915,917],{"class":121,"line":909},56,[119,911,684],{"class":358},[119,913,914],{"class":139}," d ",[119,916,690],{"class":358},[119,918,919],{"class":139}," data:\n",[119,921,923,926,928,931,934,936,938,941,943],{"class":121,"line":922},57,[119,924,925],{"class":143},"            print",[119,927,535],{"class":139},[119,929,930],{"class":129},"\"Job name: ",[119,932,933],{"class":143},"{n}",[119,935,844],{"class":129},[119,937,847],{"class":139},[119,939,940],{"class":575},"n",[119,942,565],{"class":358},[119,944,945],{"class":139},"d))\n",[119,947,949,952,955,957],{"class":121,"line":948},58,[119,950,951],{"class":358},"            for",[119,953,954],{"class":139}," attr ",[119,956,690],{"class":358},[119,958,959],{"class":139}," data[d]:\n",[119,961,963,966,969,972],{"class":121,"line":962},59,[119,964,965],{"class":358},"                if",[119,967,968],{"class":129}," 'image'",[119,970,971],{"class":358}," in",[119,973,974],{"class":139}," attr:\n",[119,976,978,981,983,986,989,991,993,996,998],{"class":121,"line":977},60,[119,979,980],{"class":143},"                    print",[119,982,535],{"class":139},[119,984,985],{"class":129},"\"Image: ",[119,987,988],{"class":143},"{i}",[119,990,844],{"class":129},[119,992,847],{"class":139},[119,994,995],{"class":575},"i",[119,997,565],{"class":358},[119,999,1000],{"class":139},"data[d][attr]))\n",[119,1002,1004],{"class":121,"line":1003},61,[119,1005,154],{"emptyLinePlaceholder":153},[119,1007,1009,1011,1013,1015,1018,1020],{"class":121,"line":1008},62,[119,1010,532],{"class":143},[119,1012,535],{"class":139},[119,1014,844],{"class":129},[119,1016,1017],{"class":143},"\\n\\n",[119,1019,844],{"class":129},[119,1021,410],{"class":139},[119,1023,1025],{"class":121,"line":1024},63,[119,1026,154],{"emptyLinePlaceholder":153},[119,1028,1030,1033,1036],{"class":121,"line":1029},64,[119,1031,1032],{"class":139},"sys.exit(",[119,1034,1035],{"class":143},"0",[119,1037,410],{"class":139},[12,1039,330,1040,1044],{},[16,1041,1043],{"href":333,"rel":1042},[],"script"," requires Python (tested with 3.11) and the python-gitlab and pyyaml modules. Example on macOS with Homebrew:",[110,1046,1050],{"className":1047,"code":1048,"language":1049,"meta":115,"style":115},"language-shell shiki shiki-themes github-light","$ brew install python\n$ pip3 install python-gitlab pyyaml\n","shell",[64,1051,1052,1065],{"__ignoreMap":115},[119,1053,1054,1056,1059,1062],{"class":121,"line":122},[119,1055,126],{"class":125},[119,1057,1058],{"class":129}," brew",[119,1060,1061],{"class":129}," install",[119,1063,1064],{"class":129}," python\n",[119,1066,1067,1069,1072,1074,1077],{"class":121,"line":150},[119,1068,126],{"class":125},[119,1070,1071],{"class":129}," pip3",[119,1073,1061],{"class":129},[119,1075,1076],{"class":129}," python-gitlab",[119,1078,1079],{"class":129}," pyyaml\n",[12,1081,1082],{},"You can execute the script and set the different environment variables to control its behavior:",[110,1084,1086],{"className":1047,"code":1085,"language":1049,"meta":115,"style":115},"$ export GL_TOKEN=$GITLAB_TOKEN\n\n$ export GL_GROUP_ID=12345\n$ export GL_PROJECT_ID=98765\n\n$ python3 get_all_cicd_job_images.py\n\n# Summary of projects and their CI/CD image usage\n# Project: Developer Evangelism at GitLab  / use-cases / Docker Use cases  / Custom Container Image Python, ID: 44352983\n\nJob name: docker-build\nImage: docker:latest\n\n# Project: Developer Evangelism at GitLab  / use-cases / Docker Use cases  / Gitlab Dependency Proxy, ID: 44351128\n\nJob name: .test-python-version\nJob name: image-docker-hub\nImage: python:3.11\nJob name: image-docker-hub-dep-proxy\nImage: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/python:3.11\n",[64,1087,1088,1101,1105,1117,1129,1133,1143,1147,1152,1157,1161,1172,1180,1184,1189,1193,1202,1211,1218,1227],{"__ignoreMap":115},[119,1089,1090,1092,1095,1098],{"class":121,"line":122},[119,1091,126],{"class":125},[119,1093,1094],{"class":129}," export",[119,1096,1097],{"class":129}," GL_TOKEN=",[119,1099,1100],{"class":139},"$GITLAB_TOKEN\n",[119,1102,1103],{"class":121,"line":150},[119,1104,154],{"emptyLinePlaceholder":153},[119,1106,1107,1109,1111,1114],{"class":121,"line":157},[119,1108,126],{"class":125},[119,1110,1094],{"class":129},[119,1112,1113],{"class":129}," GL_GROUP_ID=",[119,1115,1116],{"class":143},"12345\n",[119,1118,1119,1121,1123,1126],{"class":121,"line":195},[119,1120,126],{"class":125},[119,1122,1094],{"class":129},[119,1124,1125],{"class":129}," GL_PROJECT_ID=",[119,1127,1128],{"class":143},"98765\n",[119,1130,1131],{"class":121,"line":200},[119,1132,154],{"emptyLinePlaceholder":153},[119,1134,1135,1137,1140],{"class":121,"line":215},[119,1136,126],{"class":125},[119,1138,1139],{"class":129}," python3",[119,1141,1142],{"class":129}," get_all_cicd_job_images.py\n",[119,1144,1145],{"class":121,"line":223},[119,1146,154],{"emptyLinePlaceholder":153},[119,1148,1149],{"class":121,"line":230},[119,1150,1151],{"class":348},"# Summary of projects and their CI/CD image usage\n",[119,1153,1154],{"class":121,"line":238},[119,1155,1156],{"class":348},"# Project: Developer Evangelism at GitLab  / use-cases / Docker Use cases  / Custom Container Image Python, ID: 44352983\n",[119,1158,1159],{"class":121,"line":245},[119,1160,154],{"emptyLinePlaceholder":153},[119,1162,1163,1166,1169],{"class":121,"line":253},[119,1164,1165],{"class":125},"Job",[119,1167,1168],{"class":129}," name:",[119,1170,1171],{"class":129}," docker-build\n",[119,1173,1174,1177],{"class":121,"line":453},[119,1175,1176],{"class":125},"Image:",[119,1178,1179],{"class":129}," docker:latest\n",[119,1181,1182],{"class":121,"line":475},[119,1183,154],{"emptyLinePlaceholder":153},[119,1185,1186],{"class":121,"line":480},[119,1187,1188],{"class":348},"# Project: Developer Evangelism at GitLab  / use-cases / Docker Use cases  / Gitlab Dependency Proxy, ID: 44351128\n",[119,1190,1191],{"class":121,"line":486},[119,1192,154],{"emptyLinePlaceholder":153},[119,1194,1195,1197,1199],{"class":121,"line":492},[119,1196,1165],{"class":125},[119,1198,1168],{"class":129},[119,1200,1201],{"class":129}," .test-python-version\n",[119,1203,1204,1206,1208],{"class":121,"line":497},[119,1205,1165],{"class":125},[119,1207,1168],{"class":129},[119,1209,1210],{"class":129}," image-docker-hub\n",[119,1212,1213,1215],{"class":121,"line":515},[119,1214,1176],{"class":125},[119,1216,1217],{"class":129}," python:3.11\n",[119,1219,1220,1222,1224],{"class":121,"line":529},[119,1221,1165],{"class":125},[119,1223,1168],{"class":129},[119,1225,1226],{"class":129}," image-docker-hub-dep-proxy\n",[119,1228,1229,1231,1234],{"class":121,"line":543},[119,1230,1176],{"class":125},[119,1232,1233],{"class":139}," ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}",[119,1235,1236],{"class":129},"/python:3.11\n",[12,1238,1239],{},"Please verify the script and fork it for your own analysis and mitigation. The missing parts are checking the image URLs, and doing a more sophisticated search. The code has been prepared to either check against a single project, a group with projects, or an instance (this may take very long, use with care).",[12,1241,1242,1243,1246,1247,1252],{},"You can perform a more history-focused analysis by fetching the CI/CD job logs from GitLab and search for the pulled container image to get an overview of past Docker executor runs – for example: ",[64,1244,1245],{},"Using Docker executor with image python:3.11 ...",". The screenshot shows the CI/CD job logs UI search – you can automate the search using the GitLab API, and the ",[16,1248,1251],{"href":1249,"rel":1250},"https://python-gitlab.readthedocs.io/en/stable/gl_objects/pipelines_and_jobs.html#jobs",[],"python-gitlab library",", for example.",[12,1254,1255],{},[1256,1257],"img",{"alt":1258,"src":1259},"GitLab CI/CD job logs, searching for the image keyword","https://res.cloudinary.com/about-gitlab-com/image/upload/v1782398193/blog/Content%20Images/docker-hub-oss-image-deletion-mitigation/cicd_gitlab_job_logs_search_image.png",[12,1261,1262,1263,1265],{},"This snippet can be used in combination with the code shared for the CI lint API endpoint. It fetches the job trace logs, and searches for the ",[64,1264,66],{}," keyword in the log. The missing parts are splitting the log line by line, and extracting the image key information. This is left as an exercise for the reader.",[110,1267,1269],{"className":339,"code":1268,"language":341,"meta":115,"style":115},"\n        for job in project.jobs.list():\n            log_trace = str(job.trace())\n\n            print(log_trace)\n\n            if 'image' in log_trace:\n                print(\"Job ID: {i}, URL {u}\".format(i=job.id, u=job.web_url))\n                print(log_trace)\n\n",[64,1270,1271,1275,1287,1300,1304,1311,1315,1327,1364],{"__ignoreMap":115},[119,1272,1273],{"class":121,"line":122},[119,1274,154],{"emptyLinePlaceholder":153},[119,1276,1277,1279,1282,1284],{"class":121,"line":150},[119,1278,684],{"class":358},[119,1280,1281],{"class":139}," job ",[119,1283,690],{"class":358},[119,1285,1286],{"class":139}," project.jobs.list():\n",[119,1288,1289,1292,1294,1297],{"class":121,"line":157},[119,1290,1291],{"class":139},"            log_trace ",[119,1293,565],{"class":358},[119,1295,1296],{"class":143}," str",[119,1298,1299],{"class":139},"(job.trace())\n",[119,1301,1302],{"class":121,"line":195},[119,1303,154],{"emptyLinePlaceholder":153},[119,1305,1306,1308],{"class":121,"line":200},[119,1307,925],{"class":143},[119,1309,1310],{"class":139},"(log_trace)\n",[119,1312,1313],{"class":121,"line":215},[119,1314,154],{"emptyLinePlaceholder":153},[119,1316,1317,1320,1322,1324],{"class":121,"line":223},[119,1318,1319],{"class":358},"            if",[119,1321,968],{"class":129},[119,1323,971],{"class":358},[119,1325,1326],{"class":139}," log_trace:\n",[119,1328,1329,1332,1334,1337,1339,1342,1345,1347,1349,1351,1353,1356,1359,1361],{"class":121,"line":230},[119,1330,1331],{"class":143},"                print",[119,1333,535],{"class":139},[119,1335,1336],{"class":129},"\"Job ID: ",[119,1338,988],{"class":143},[119,1340,1341],{"class":129},", URL ",[119,1343,1344],{"class":143},"{u}",[119,1346,844],{"class":129},[119,1348,847],{"class":139},[119,1350,995],{"class":575},[119,1352,565],{"class":358},[119,1354,1355],{"class":139},"job.id, ",[119,1357,1358],{"class":575},"u",[119,1360,565],{"class":358},[119,1362,1363],{"class":139},"job.web_url))\n",[119,1365,1366,1368],{"class":121,"line":238},[119,1367,1331],{"class":143},[119,1369,1310],{"class":139},[52,1371,1373],{"id":1372},"more-inventory-considerations","More inventory considerations",[12,1375,1376,1377,1382],{},"Similar to the API script for CI/CD navigating through all projects, you will need to analyze all Kubernetes manifest configuration files – using either a pull- or push-based approach. This can be achieved by using the ",[16,1378,1381],{"href":1379,"rel":1380},"https://python-gitlab.readthedocs.io/en/stable/gl_objects/projects.html#project-files",[],"python-gitlab methods to load files from the repository"," and searching the content in similar ways. Helm charts use container images, too, and will require additional analysis.",[12,1384,1385],{},"An additional search possibility: Custom-built container images that use Docker Hub images as a source. A project will consist of:",[312,1387,1388,1397],{},[315,1389,1390,1393,1394],{},[64,1391,1392],{},"Dockerfile"," file that uses ",[64,1395,1396],{},"FROM \u003Cimagename>",[315,1398,1399,1402],{},[64,1400,1401],{},".gitlab-ci.yml"," configuration file that builds container images (using Docker-in-Docker, Kaniko, etc.)",[12,1404,1405,1406,1411,1412,1417,1418,1421],{},"An alternative search method for customers is available by using the ",[16,1407,1410],{"href":1408,"rel":1409},"https://docs.gitlab.com/user/search/advanced_search/",[],"Advanced Search"," through the GitLab UI and API. The following example uses the ",[16,1413,1416],{"href":1414,"rel":1415},"https://docs.gitlab.com/api/search/#scope-blobs-premium-2",[],"scope: blobs"," to search for the ",[64,1419,1420],{},"FROM"," string:",[110,1423,1425],{"className":1047,"code":1424,"language":1049,"meta":115,"style":115},"$ export GITLAB_TOKEN=xxxxxxxxx\n\n# Search in https://gitlab.com/gitlab-da\n/use-cases/docker/custom-container-image-python\n\n$ curl --header \"PRIVATE-TOKEN: $GITLAB_TOKEN\" \"https://gitlab.com/api/v4/projects/44352983/search?scope=blobs&search=FROM%20filename:Dockerfile*\"\n",[64,1426,1427,1436,1440,1445,1450,1454],{"__ignoreMap":115},[119,1428,1429,1431,1433],{"class":121,"line":122},[119,1430,126],{"class":125},[119,1432,1094],{"class":129},[119,1434,1435],{"class":129}," GITLAB_TOKEN=xxxxxxxxx\n",[119,1437,1438],{"class":121,"line":150},[119,1439,154],{"emptyLinePlaceholder":153},[119,1441,1442],{"class":121,"line":157},[119,1443,1444],{"class":348},"# Search in https://gitlab.com/gitlab-da\n",[119,1446,1447],{"class":121,"line":195},[119,1448,1449],{"class":125},"/use-cases/docker/custom-container-image-python\n",[119,1451,1452],{"class":121,"line":200},[119,1453,154],{"emptyLinePlaceholder":153},[119,1455,1456,1458,1461,1464,1467,1470,1472],{"class":121,"line":215},[119,1457,126],{"class":125},[119,1459,1460],{"class":129}," curl",[119,1462,1463],{"class":143}," --header",[119,1465,1466],{"class":129}," \"PRIVATE-TOKEN: ",[119,1468,1469],{"class":139},"$GITLAB_TOKEN",[119,1471,844],{"class":129},[119,1473,1474],{"class":129}," \"https://gitlab.com/api/v4/projects/44352983/search?scope=blobs&search=FROM%20filename:Dockerfile*\"\n",[12,1476,1477],{},[1256,1478],{"alt":1479,"src":1480},"Command line output from Advanced Search API, scope blobs, search FROM in Dockerfile* file names.","https://res.cloudinary.com/about-gitlab-com/image/upload/v1782398193/blog/Content%20Images/docker-hub-oss-image-deletion-mitigation/cli_gitlab_advanced_search_api_dockerfile_from.png",[1482,1483,1485],"h2",{"id":1484},"mitigations-and-solutions","Mitigations and solutions",[12,1487,1488],{},"The following sections discuss potential mitigation strategies, and long-term solutions.",[52,1490,1492],{"id":1491},"mitigation-gitlab-dependency-proxy","Mitigation: GitLab dependency proxy",[12,1494,1495,1496,1500],{},"The dependency proxy provides a caching mechanism for Docker Hub images. It helps reduce the bandwidth and time required to download and pull the images. It also helped to ",[16,1497,1499],{"href":1498},"/blog/minor-breaking-change-dependency-proxy/","mitigate the Docker Hub pull rate limits introduced in 2020",". The dependency proxy can be configured for public and private projects.",[12,1502,330,1503,1508],{},[16,1504,1507],{"href":1505,"rel":1506},"https://docs.gitlab.com/user/packages/dependency_proxy/",[],"dependency proxy"," needs to be enabled for a group. It also needs to be enabled by an instance administrator for self-managed environments, if turned off.",[12,1510,1511,1512,102,1515,1518,1519,1522],{},"The following example creates two jobs: ",[64,1513,1514],{},"image-docker-hub",[64,1516,1517],{},"image-docker-hub-dep-proxy",". The dependency proxy job uses the ",[64,1520,1521],{},"CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX"," CI/CD variable to instruct GitLab to store the image in the cache, and only pull it once when not available.",[110,1524,1528],{"className":1525,"code":1526,"language":1527,"meta":115,"style":115},"language-yaml shiki shiki-themes github-light",".test-python-version:\n  script:\n    - echo \"Testing Python version:\"\n    - python --version\n\nimage-docker-hub:\n  extends: .test-python-version\n  image: python:3.11\n\nimage-docker-hub-dep-proxy:\n  extends: .test-python-version\n  image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/python:3.11\n\n","yaml",[64,1529,1530,1538,1545,1553,1560,1564,1570,1581,1591,1595,1601,1609],{"__ignoreMap":115},[119,1531,1532,1536],{"class":121,"line":122},[119,1533,1535],{"class":1534},"shJU0",".test-python-version",[119,1537,512],{"class":139},[119,1539,1540,1543],{"class":121,"line":150},[119,1541,1542],{"class":1534},"  script",[119,1544,512],{"class":139},[119,1546,1547,1550],{"class":121,"line":157},[119,1548,1549],{"class":139},"    - ",[119,1551,1552],{"class":129},"echo \"Testing Python version:\"\n",[119,1554,1555,1557],{"class":121,"line":195},[119,1556,1549],{"class":139},[119,1558,1559],{"class":129},"python --version\n",[119,1561,1562],{"class":121,"line":200},[119,1563,154],{"emptyLinePlaceholder":153},[119,1565,1566,1568],{"class":121,"line":215},[119,1567,1514],{"class":1534},[119,1569,512],{"class":139},[119,1571,1572,1575,1578],{"class":121,"line":223},[119,1573,1574],{"class":1534},"  extends",[119,1576,1577],{"class":139},": ",[119,1579,1580],{"class":129},".test-python-version\n",[119,1582,1583,1586,1588],{"class":121,"line":230},[119,1584,1585],{"class":1534},"  image",[119,1587,1577],{"class":139},[119,1589,1590],{"class":129},"python:3.11\n",[119,1592,1593],{"class":121,"line":238},[119,1594,154],{"emptyLinePlaceholder":153},[119,1596,1597,1599],{"class":121,"line":245},[119,1598,1517],{"class":1534},[119,1600,512],{"class":139},[119,1602,1603,1605,1607],{"class":121,"line":253},[119,1604,1574],{"class":1534},[119,1606,1577],{"class":139},[119,1608,1580],{"class":129},[119,1610,1611,1613,1615],{"class":121,"line":453},[119,1612,1585],{"class":1534},[119,1614,1577],{"class":139},[119,1616,1617],{"class":129},"${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/python:3.11\n",[12,1619,1620,1621,1626],{},"The configuration is available in ",[16,1622,1625],{"href":1623,"rel":1624},"https://gitlab.com/gitlab-da/use-cases/docker/gitlab-dependency-proxy",[],"this project",".",[12,1628,1629,1630,1633],{},"The stored container image is visible at the group level in the ",[64,1631,1632],{},"Package and container registries > Dependency Proxy"," menu.",[52,1635,1637],{"id":1636},"mitigation-container-registry-mirror","Mitigation: Container registry mirror",[12,1639,1640,1644,1645,1626],{},[16,1641,1643],{"href":1642},"/blog/mitigating-the-impact-of-docker-hub-pull-requests-limits/","This blog post"," describes how to run a local container registry mirror. Skopeo from Red Hat is another alternative for syncing container image registries, a practical example is described ",[16,1646,1649],{"href":1647,"rel":1648},"https://marcbrandner.com/blog/transporting-container-images-with-skopeo/",[],"in this article",[12,1651,1652,1653,102,1658,1663,1664,1669,1670,1675],{},"The GitLab Cloud Native installation (",[16,1654,1657],{"href":1655,"rel":1656},"https://docs.gitlab.com/charts/",[],"Helm charts",[16,1659,1662],{"href":1660,"rel":1661},"https://docs.gitlab.com/operator/",[],"Operator",") use a ",[16,1665,1668],{"href":1666,"rel":1667},"https://gitlab.com/gitlab-org/cloud-native/mirror/images",[],"mirror of tagged images"," consumed by the related projects. Other product stages follow a similar approach, the ",[16,1671,1674],{"href":1672,"rel":1673},"https://docs.gitlab.com/user/application_security/offline_deployments/#container-registries-and-package-repositories",[],"security scanners are shipped in container images"," maintained by GitLab. This also enables self-managed airgapped installations.",[52,1677,1679],{"id":1678},"mitigation-custom-images-in-gitlab-container-registry","Mitigation: Custom images in GitLab container registry",[12,1681,1682,1683,1688,1689,1626],{},"Reproducible builds and compliance requirements may have required you to create custom container images for CI/CD and Kubernetes already. This is also key to verify that no untested and untrusted images are being used in production. GitLab provides a fully integrated ",[16,1684,1687],{"href":1685,"rel":1686},"https://docs.gitlab.com/user/packages/container_registry/",[],"container registry",", which can be used natively within CI/CD pipelines and ",[16,1690,1693],{"href":1691,"rel":1692},"https://docs.gitlab.com/user/clusters/agent/gitops/",[],"GitOps workflows with the agent for Kubernetes",[12,1695,1696,1697,1699],{},"The following ",[64,1698,1392],{}," example extends an existing image layer, and installs additional tools using the Debian Apt package manager.",[110,1701,1706],{"className":1702,"code":1704,"language":1705,"meta":115},[1703],"language-text","FROM python:3.11-bullseye\n\nENV DEBIAN_FRONTEND noninteractive\n\nRUN apt update && apt -y install git curl jq && rm -rf /var/lib/apt/lists/*\n","text",[64,1707,1704],{"__ignoreMap":115},[12,1709,1710,1711,1716,1717,1720],{},"You can ",[16,1712,1715],{"href":1713,"rel":1714},"https://docs.gitlab.com/ci/docker/using_docker_build/",[],"use Docker to build container images",", and alternative options are Kaniko or Podman. On GitLab.com SaaS, you can use the Docker CI/CD template to build and push images. The following example modifies the ",[64,1718,1719],{},"docker-build"," job to only build the latest tag from the default branch:",[110,1722,1724],{"className":1525,"code":1723,"language":1527,"meta":115,"style":115},"include:\n  - template: Docker.gitlab-ci.yml\n\ndocker-build:\n  stage: build\n  rules:\n    - if: '$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG'\n      #when: manual\n      #allow_failure: true\n\n",[64,1725,1726,1733,1746,1750,1756,1766,1773,1784,1789],{"__ignoreMap":115},[119,1727,1728,1731],{"class":121,"line":122},[119,1729,1730],{"class":1534},"include",[119,1732,512],{"class":139},[119,1734,1735,1738,1741,1743],{"class":121,"line":150},[119,1736,1737],{"class":139},"  - ",[119,1739,1740],{"class":1534},"template",[119,1742,1577],{"class":139},[119,1744,1745],{"class":129},"Docker.gitlab-ci.yml\n",[119,1747,1748],{"class":121,"line":157},[119,1749,154],{"emptyLinePlaceholder":153},[119,1751,1752,1754],{"class":121,"line":195},[119,1753,1719],{"class":1534},[119,1755,512],{"class":139},[119,1757,1758,1761,1763],{"class":121,"line":200},[119,1759,1760],{"class":1534},"  stage",[119,1762,1577],{"class":139},[119,1764,1765],{"class":129},"build\n",[119,1767,1768,1771],{"class":121,"line":215},[119,1769,1770],{"class":1534},"  rules",[119,1772,512],{"class":139},[119,1774,1775,1777,1779,1781],{"class":121,"line":223},[119,1776,1549],{"class":139},[119,1778,500],{"class":1534},[119,1780,1577],{"class":139},[119,1782,1783],{"class":129},"'$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG'\n",[119,1785,1786],{"class":121,"line":230},[119,1787,1788],{"class":348},"      #when: manual\n",[119,1790,1791],{"class":121,"line":238},[119,1792,1793],{"class":348},"      #allow_failure: true\n",[12,1795,1796],{},"For this example, we specifically want to provide a Git tag that gets used for the container image tag as well.",[110,1798,1800],{"className":1047,"code":1799,"language":1049,"meta":115,"style":115},"$ git tag 3-11-bullseye\n$ git push --tags\n",[64,1801,1802,1814],{"__ignoreMap":115},[119,1803,1804,1806,1808,1811],{"class":121,"line":122},[119,1805,126],{"class":125},[119,1807,130],{"class":129},[119,1809,1810],{"class":129}," tag",[119,1812,1813],{"class":129}," 3-11-bullseye\n",[119,1815,1816,1818,1820,1823],{"class":121,"line":150},[119,1817,126],{"class":125},[119,1819,130],{"class":129},[119,1821,1822],{"class":129}," push",[119,1824,1825],{"class":143}," --tags\n",[12,1827,1828,1829,1833],{},"The image will be available at the GitLab container registry URL and the project namespace path.This path needs to be replaced in all projects that use a Python-based image. You can ",[16,1830,1832],{"href":1831},"/blog/efficient-devsecops-workflows-hands-on-python-gitlab-api-automation/","create scripts for the GitLab API"," to update files and create MRs automatically,",[110,1835,1838],{"className":1836,"code":1837,"language":1705,"meta":115},[1703],"image: registry.gitlab.com/gitlab-da/use-cases/docker/custom-container-image-python:3-11-bullseye\n",[64,1839,1837],{"__ignoreMap":115},[12,1841,1842],{},[25,1843,1844],{},"Note: This is a demo project and not actively maintained. Please fork/copy it for your own needs.",[1482,1846,1848],{"id":1847},"observability-and-security","Observability and security",[12,1850,330,1851,1856,1857,1859,1860],{},[16,1852,1855],{"href":1853,"rel":1854},"https://docs.gitlab.com/user/analytics/ci_cd_analytics/",[],"number of failed CI/CD pipelines"," can be a good service level indicator (SLI) to verify whether the environment is affected by the Docker Hub changes. The same SLI applies for CI/CD jobs that build container images, using a ",[64,1858,1392],{}," file, which is based on Docker Hub images (FROM ",[1861,1862,1863],"imagename",{},").",[12,1865,1866,1867,102,1870,1877,1878,1883,1884,1887,1888,1891],{},"A similar SLI applies to Kubernetes cluster deployments – if they continue to generate failures in GitOps pull or CI/CD push scenarios, additional analysis and actions are required. The pod status ",[64,1868,1869],{},"ErrImagePull",[16,1871,1874],{"href":1872,"rel":1873},"https://kubernetes.io/docs/concepts/containers/images/#imagepullbackoff",[],[64,1875,1876],{},"ImagePullBackOff"," will immediately show the problems. The ",[16,1879,1882],{"href":1880,"rel":1881},"https://kubernetes.io/docs/concepts/containers/images/#image-pull-policy",[],"image pull policy"," should also be revised – ",[64,1885,1886],{},"Always"," will immediately cause a problem, while ",[64,1889,1890],{},"IfNotPresent"," will use the local image cache.",[12,1893,1894,1899],{},[16,1895,1898],{"href":1896,"rel":1897},"https://awesome-prometheus-alerts.grep.to/rules.html#rule-kubernetes-1-18",[],"This alert rule example"," for Prometheus observing a Kubernetes cluster can help detect the pod state as not healthy.",[110,1901,1903],{"className":1525,"code":1902,"language":1527,"meta":115,"style":115},"\n  - alert: KubernetesPodNotHealthy\n    expr: sum by (namespace, pod) (kube_pod_status_phase{phase=~\"Pending|Unknown|Failed\"}) > 0\n    for: 15m\n    labels:\n      severity: critical\n    annotations:\n      summary: Kubernetes Pod not healthy (instance {{ $labels.instance }})\n      description: \"Pod has been in a non-ready state for longer than 15 minutes.\\n  VALUE = {{ $value }}\\n  LABELS = {{ $labels }}\"\n\n",[64,1904,1905,1909,1921,1931,1940,1947,1957,1964,1974],{"__ignoreMap":115},[119,1906,1907],{"class":121,"line":122},[119,1908,154],{"emptyLinePlaceholder":153},[119,1910,1911,1913,1916,1918],{"class":121,"line":150},[119,1912,1737],{"class":139},[119,1914,1915],{"class":1534},"alert",[119,1917,1577],{"class":139},[119,1919,1920],{"class":129},"KubernetesPodNotHealthy\n",[119,1922,1923,1926,1928],{"class":121,"line":157},[119,1924,1925],{"class":1534},"    expr",[119,1927,1577],{"class":139},[119,1929,1930],{"class":129},"sum by (namespace, pod) (kube_pod_status_phase{phase=~\"Pending|Unknown|Failed\"}) > 0\n",[119,1932,1933,1935,1937],{"class":121,"line":195},[119,1934,810],{"class":1534},[119,1936,1577],{"class":139},[119,1938,1939],{"class":129},"15m\n",[119,1941,1942,1945],{"class":121,"line":200},[119,1943,1944],{"class":1534},"    labels",[119,1946,512],{"class":139},[119,1948,1949,1952,1954],{"class":121,"line":215},[119,1950,1951],{"class":1534},"      severity",[119,1953,1577],{"class":139},[119,1955,1956],{"class":129},"critical\n",[119,1958,1959,1962],{"class":121,"line":223},[119,1960,1961],{"class":1534},"    annotations",[119,1963,512],{"class":139},[119,1965,1966,1969,1971],{"class":121,"line":230},[119,1967,1968],{"class":1534},"      summary",[119,1970,1577],{"class":139},[119,1972,1973],{"class":129},"Kubernetes Pod not healthy (instance {{ $labels.instance }})\n",[119,1975,1976,1979,1981,1984,1987,1990,1992],{"class":121,"line":238},[119,1977,1978],{"class":1534},"      description",[119,1980,1577],{"class":139},[119,1982,1983],{"class":129},"\"Pod has been in a non-ready state for longer than 15 minutes.",[119,1985,1986],{"class":143},"\\n",[119,1988,1989],{"class":129},"  VALUE = {{ $value }}",[119,1991,1986],{"class":143},[119,1993,1994],{"class":129},"  LABELS = {{ $labels }}\"\n",[12,1996,1997,1998,2000],{},"CI/CD pipeline linters and Git hooks can also be helpful to enforce using a GitLab registry URL prefix in all ",[64,1999,66],{}," tags, when new updates to CI/CD configurations are being pushed into merge requests.",[12,2002,2003,2004,2009,2010,2015,2016,2021],{},"Kubernetes deployment images can be controlled through additional integrations with the ",[16,2005,2008],{"href":2006,"rel":2007},"https://www.openpolicyagent.org/docs/latest/kubernetes-introduction/",[],"Open Policy Agent Gatekeeper"," or ",[16,2011,2014],{"href":2012,"rel":2013},"https://kyverno.io/policies/best-practices/restrict_image_registries/restrict_image_registries/",[],"Kyverno",". Kyverno also allows you to ",[16,2017,2020],{"href":2018,"rel":2019},"https://kyverno.io/policies/other/replace_image_registry/replace_image_registry/",[],"mutate the image registry location",", and redirect the pod image to trusted sources.",[12,2023,2024,2029,2030,2035],{},[16,2025,2028],{"href":2026,"rel":2027},"https://docs.gitlab.com/user/clusters/agent/vulnerabilities/",[],"Operational container scanning"," in Kubernetes clusters and ",[16,2031,2034],{"href":2032,"rel":2033},"https://docs.gitlab.com/user/application_security/container_scanning/",[],"container scanning in CI/CD pipelines"," are recommended. This ensures that all images do not expose security vulnerabilities.",[1482,2037,2039],{"id":2038},"long-term-solutions","Long-term solutions",[12,2041,2042,2043,2048],{},"As a long-term solution, analyze the affected Docker Hub organizations images and match them against your image usage inventory. Some organizations have raised their concerns in ",[16,2044,2047],{"href":2045,"rel":2046},"https://github.com/docker/hub-feedback/issues/2314",[],"this Docker Hub feedback issue",". Be sure to identify critical production CI/CD workflows and replace all external dependencies with local maintained images.",[12,2050,2051,2052,2057],{},"Fork/copy project Dockerfile files from the upstream Git repositories, and use them as the single source of truth for custom container builds. This will also require training and documentation for DevSecOps teams, for example optimizing container images for ",[16,2053,2056],{"href":2054,"rel":2055},"https://docs.gitlab.com/ci/pipelines/pipeline_efficiency/",[],"efficient CI/CD pipelines",". More DevSecOps efficiency tips can be found in my Chemnitz Linux Days talk about \"Efficient DevSecOps Pipelines in a Cloud Native World\" (slides).",[2059,2060],"iframe",{"src":2061,"frameBorder":1035,"width":2062,"height":2063,"allowFullScreen":2064,"mozallowfullscreen":2064,"webkitallowfullscreen":2064},"https://docs.google.com/presentation/d/e/2PACX-1vT3jcfpddKL2jq7leX01QX6S4Y8vfLLBZMz4L1ZHMLY3xzB4IGOOIExODLEzH8YQM1atCNPm07Bw9m_/embed?start=false&loop=true&delayms=3000",960,569,"true",[12,2066,2067,2068,2073],{},"Please share your ideas and thoughts about Docker Hub change mitigations and tools on the ",[16,2069,2072],{"href":2070,"rel":2071},"https://forum.gitlab.com/",[],"GitLab community forum",". Thank you!",[12,2075,2076,2077,2082,2083],{},"Cover image by ",[16,2078,2081],{"href":2079,"rel":2080},"https://unsplash.com/photos/sTOQyRD8m74",[],"Roger Hoyles"," on ",[16,2084,2087],{"href":2085,"rel":2086},"https://www.unsplash.com",[],"Unsplash",[2089,2090,2091],"style",{},"html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sqxcx, html code.shiki .sqxcx{--shiki-default:#E36209}html pre.shiki code .shJU0, html code.shiki .shJU0{--shiki-default:#22863A}",{"title":115,"searchDepth":150,"depth":150,"links":2093},[2094,2095,2096,2101,2102],{"id":54,"depth":157,"text":55},{"id":1372,"depth":157,"text":1373},{"id":1484,"depth":150,"text":1485,"children":2097},[2098,2099,2100],{"id":1491,"depth":157,"text":1492},{"id":1636,"depth":157,"text":1637},{"id":1678,"depth":157,"text":1679},{"id":1847,"depth":150,"text":1848},{"id":2038,"depth":150,"text":2039},"engineering","2023-03-16","CI/CD and Kubernetes deployments can be affected by Docker Hub tier changes. This tutorial walks through analysis, mitigations, and long-term solutions.","md",null,false,"https://res.cloudinary.com/about-gitlab-com/image/upload/v1749659883/Blog/Hero%20Images/post-cover-image.jpg",{},"/en-us/blog/how-gitlab-can-help-mitigate-deletion-open-source-images-docker-hub",{"title":2113,"description":2105,"ogTitle":2113,"ogDescription":2105,"noIndex":2108,"ogImage":2109,"ogUrl":2114,"ogSiteName":2115,"ogType":2116,"canonicalUrls":2114},"GitLab helps mitigate Docker Hub's open source image removal","https://about.gitlab.com/blog/how-gitlab-can-help-mitigate-deletion-open-source-images-docker-hub","https://about.gitlab.com","article","how-gitlab-can-help-mitigate-deletion-open-source-images-docker-hub","en-us/blog/how-gitlab-can-help-mitigate-deletion-open-source-images-docker-hub",[2120,2121,2122],"CI","kubernetes","open source","BlogPost","1AaJTZD7zsDGE4Wrl7I_ZofJEuY8pN5r4sOXBlG6gU4",{"logo":2126,"freeTrial":2131,"sales":2136,"login":2141,"items":2146,"search":2472,"minimal":2503,"duo":2522,"switchNav":2531,"pricingDeployment":2542},{"config":2127},{"href":2128,"dataGaName":2129,"dataGaLocation":2130},"/","gitlab logo","header",{"text":2132,"config":2133},"Get free trial",{"href":2134,"dataGaName":2135,"dataGaLocation":2130},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com&glm_content=default-saas-trial/","free trial",{"text":2137,"config":2138},"Request a demo",{"href":2139,"dataGaName":2140,"dataGaLocation":2130},"/sales/?contact-topic=request-demo","sales",{"text":2142,"config":2143},"Sign in",{"href":2144,"dataGaName":2145,"dataGaLocation":2130},"https://gitlab.com/users/sign_in/","sign in",[2147,2176,2276,2281,2394,2450],{"text":2148,"config":2149,"menu":2151},"Platform",{"dataNavLevelOne":2150},"platform",{"type":2152,"columns":2153},"cards",[2154,2160,2168],{"title":2148,"description":2155,"link":2156},"The intelligent orchestration platform for DevSecOps",{"text":2157,"config":2158},"Explore our Platform",{"href":2159,"dataGaName":2150,"dataGaLocation":2130},"/platform/",{"title":2161,"description":2162,"link":2163},"GitLab Duo Agent Platform","Agentic AI for the entire software lifecycle",{"text":2164,"config":2165},"Meet GitLab Duo",{"href":2166,"dataGaName":2167,"dataGaLocation":2130},"/gitlab-duo-agent-platform/","gitlab duo agent platform",{"title":2169,"description":2170,"link":2171},"Why GitLab","See the top reasons enterprises choose GitLab",{"text":2172,"config":2173},"Learn more",{"href":2174,"dataGaName":2175,"dataGaLocation":2130},"/why-gitlab/","why gitlab",{"text":2177,"left":153,"config":2178,"menu":2180},"Product",{"dataNavLevelOne":2179},"solutions",{"type":2181,"link":2182,"columns":2186,"feature":2255},"lists",{"text":2183,"config":2184},"View all Solutions",{"href":2185,"dataGaName":2179,"dataGaLocation":2130},"/solutions/",[2187,2211,2234],{"title":2188,"description":2189,"link":2190,"items":2195},"Automation","CI/CD and automation to accelerate deployment",{"config":2191},{"icon":2192,"href":2193,"dataGaName":2194,"dataGaLocation":2130},"AutomatedCodeAlt","/solutions/delivery-automation/","automated software delivery",[2196,2200,2203,2207],{"text":2197,"config":2198},"CI/CD",{"href":2199,"dataGaLocation":2130,"dataGaName":2197},"/solutions/continuous-integration/",{"text":2161,"config":2201},{"href":2166,"dataGaLocation":2130,"dataGaName":2202},"gitlab duo agent platform - product menu",{"text":2204,"config":2205},"Source Code Management",{"href":2206,"dataGaLocation":2130,"dataGaName":2204},"/solutions/source-code-management/",{"text":2208,"config":2209},"Automated Software Delivery",{"href":2193,"dataGaLocation":2130,"dataGaName":2210},"Automated software delivery",{"title":2212,"description":2213,"link":2214,"items":2219},"Security","Deliver code faster without compromising security",{"config":2215},{"href":2216,"dataGaName":2217,"dataGaLocation":2130,"icon":2218},"/solutions/application-security-testing/","security and compliance","ShieldCheckLight",[2220,2224,2229],{"text":2221,"config":2222},"Application Security Testing",{"href":2216,"dataGaName":2223,"dataGaLocation":2130},"Application security testing",{"text":2225,"config":2226},"Software Supply Chain Security",{"href":2227,"dataGaLocation":2130,"dataGaName":2228},"/solutions/supply-chain/","Software supply chain security",{"text":2230,"config":2231},"Software Compliance",{"href":2232,"dataGaName":2233,"dataGaLocation":2130},"/solutions/software-compliance/","software compliance",{"title":2235,"link":2236,"items":2241},"Measurement",{"config":2237},{"icon":2238,"href":2239,"dataGaName":2240,"dataGaLocation":2130},"DigitalTransformation","/solutions/visibility-measurement/","visibility and measurement",[2242,2246,2250],{"text":2243,"config":2244},"Visibility & Measurement",{"href":2239,"dataGaLocation":2130,"dataGaName":2245},"Visibility and Measurement",{"text":2247,"config":2248},"Value Stream Management",{"href":2249,"dataGaLocation":2130,"dataGaName":2247},"/solutions/value-stream-management/",{"text":2251,"config":2252},"Analytics & Insights",{"href":2253,"dataGaLocation":2130,"dataGaName":2254},"/solutions/analytics-and-insights/","Analytics and insights",{"title":2256,"type":2181,"items":2257},"GitLab for",[2258,2264,2270],{"text":2259,"config":2260},"Enterprise",{"icon":2261,"href":2262,"dataGaLocation":2130,"dataGaName":2263},"Building","/enterprise/","enterprise",{"text":2265,"config":2266},"Small Business",{"icon":2267,"href":2268,"dataGaLocation":2130,"dataGaName":2269},"Work","/small-business/","small business",{"text":2271,"config":2272},"Public Sector",{"icon":2273,"href":2274,"dataGaLocation":2130,"dataGaName":2275},"Organization","/solutions/public-sector/","public sector",{"text":2277,"config":2278},"Pricing",{"href":2279,"dataGaName":2280,"dataGaLocation":2130,"dataNavLevelOne":2280},"/pricing/","pricing",{"text":2282,"config":2283,"menu":2285},"Resources",{"dataNavLevelOne":2284},"resources",{"type":2181,"link":2286,"columns":2290,"feature":2383},{"text":2287,"config":2288},"View all resources",{"href":2289,"dataGaName":2284,"dataGaLocation":2130},"/resources/",[2291,2324,2351],{"title":2292,"items":2293},"Getting started",[2294,2299,2304,2309,2314,2319],{"text":2295,"config":2296},"Install",{"href":2297,"dataGaName":2298,"dataGaLocation":2130},"/install/","install",{"text":2300,"config":2301},"Quick start guides",{"href":2302,"dataGaName":2303,"dataGaLocation":2130},"/get-started/","quick setup checklists",{"text":2305,"config":2306},"Learn",{"href":2307,"dataGaLocation":2130,"dataGaName":2308},"https://university.gitlab.com/","learn",{"text":2310,"config":2311},"Product documentation",{"href":2312,"dataGaName":2313,"dataGaLocation":2130},"https://docs.gitlab.com/","product documentation",{"text":2315,"config":2316},"Best practice videos",{"href":2317,"dataGaName":2318,"dataGaLocation":2130},"/getting-started-videos/","best practice videos",{"text":2320,"config":2321},"Integrations",{"href":2322,"dataGaName":2323,"dataGaLocation":2130},"/integrations/","integrations",{"title":2325,"items":2326},"Discover",[2327,2332,2337,2342,2346],{"text":2328,"config":2329},"Customer success stories",{"href":2330,"dataGaName":2331,"dataGaLocation":2130},"/customers/","customer success stories",{"text":2333,"config":2334},"Blog",{"href":2335,"dataGaName":2336,"dataGaLocation":2130},"/blog/","blog",{"text":2338,"config":2339},"Demo Hub",{"href":2340,"dataGaName":2341,"dataGaLocation":2130},"/demo-hub/","demo hub",{"text":2343,"config":2344},"The Source",{"href":2345,"dataGaName":2336,"dataGaLocation":2130},"/the-source/",{"text":2347,"config":2348},"Remote",{"href":2349,"dataGaName":2350,"dataGaLocation":2130},"https://handbook.gitlab.com/handbook/company/culture/all-remote/","remote",{"title":2352,"items":2353},"Connect",[2354,2359,2364,2369,2373,2378],{"text":2355,"config":2356},"GitLab Services",{"href":2357,"dataGaName":2358,"dataGaLocation":2130},"/services/","services",{"text":2360,"config":2361},"Contribute",{"href":2362,"dataGaName":2363,"dataGaLocation":2130},"https://contributors.gitlab.com","contribute",{"text":2365,"config":2366},"Community",{"href":2367,"dataGaName":2368,"dataGaLocation":2130},"/community/","community",{"text":2370,"config":2371},"Forum",{"href":2070,"dataGaName":2372,"dataGaLocation":2130},"forum",{"text":2374,"config":2375},"Events",{"href":2376,"dataGaName":2377,"dataGaLocation":2130},"/events/","events",{"text":2379,"config":2380},"Partners",{"href":2381,"dataGaName":2382,"dataGaLocation":2130},"/partners/","partners",{"config":2384,"title":2387,"text":2388,"link":2389},{"background":2385,"textColor":2386},"url('https://res.cloudinary.com/about-gitlab-com/image/upload/v1777322348/qpq8yrgn8knii57omj0c.png')","#000","What’s new in GitLab","Stay updated with our latest features and improvements.",{"text":2390,"config":2391},"Read the latest",{"href":2392,"dataGaName":2393,"dataGaLocation":2130},"/whats-new/","whats new",{"text":2395,"config":2396,"menu":2398},"Company",{"dataNavLevelOne":2397},"company",{"type":2181,"columns":2399},[2400],{"items":2401},[2402,2407,2413,2415,2420,2425,2430,2435,2440,2445],{"text":2403,"config":2404},"About",{"href":2405,"dataGaName":2406,"dataGaLocation":2130},"/company/","about",{"text":2408,"config":2409,"footerGa":2412},"Jobs",{"href":2410,"dataGaName":2411,"dataGaLocation":2130},"/jobs/","jobs",{"dataGaName":2411},{"text":2374,"config":2414},{"href":2376,"dataGaName":2377,"dataGaLocation":2130},{"text":2416,"config":2417},"Leadership",{"href":2418,"dataGaName":2419,"dataGaLocation":2130},"/company/team/e-group/","leadership",{"text":2421,"config":2422},"Handbook",{"href":2423,"dataGaName":2424,"dataGaLocation":2130},"https://handbook.gitlab.com/","handbook",{"text":2426,"config":2427},"Investor relations",{"href":2428,"dataGaName":2429,"dataGaLocation":2130},"https://ir.gitlab.com/overview/default.aspx","investor relations",{"text":2431,"config":2432},"Trust Center",{"href":2433,"dataGaName":2434,"dataGaLocation":2130},"/security/","trust center",{"text":2436,"config":2437},"AI Transparency Center",{"href":2438,"dataGaName":2439,"dataGaLocation":2130},"/ai-transparency-center/","ai transparency center",{"text":2441,"config":2442},"Newsletter",{"href":2443,"dataGaName":2444,"dataGaLocation":2130},"/company/contact/#contact-forms","newsletter",{"text":2446,"config":2447},"Press",{"href":2448,"dataGaName":2449,"dataGaLocation":2130},"/press/","press",{"text":2451,"config":2452,"menu":2453},"Contact us",{"dataNavLevelOne":2397},{"type":2181,"columns":2454},[2455],{"items":2456},[2457,2462,2467],{"text":2458,"config":2459},"Talk to sales",{"href":2460,"dataGaName":2461,"dataGaLocation":2130},"/sales/","talk to sales",{"text":2463,"config":2464},"Support portal",{"href":2465,"dataGaName":2466,"dataGaLocation":2130},"https://support.gitlab.com/hc/en-us","support portal",{"text":2468,"config":2469},"Customer portal",{"href":2470,"dataGaName":2471,"dataGaLocation":2130},"https://customers.gitlab.com/customers/sign_in/","customer portal",{"close":2473,"login":2474,"suggestions":2481},"Close",{"text":2475,"link":2476},"To search repositories and projects, login to",{"text":2477,"config":2478},"gitlab.com",{"href":2144,"dataGaName":2479,"dataGaLocation":2480},"search login","search",{"text":2482,"default":2483},"Suggestions",[2484,2486,2490,2492,2496,2500],{"text":2161,"config":2485},{"href":2166,"dataGaName":2161,"dataGaLocation":2480},{"text":2487,"config":2488},"Code Suggestions (AI)",{"href":2489,"dataGaName":2487,"dataGaLocation":2480},"/solutions/code-suggestions/",{"text":2197,"config":2491},{"href":2199,"dataGaName":2197,"dataGaLocation":2480},{"text":2493,"config":2494},"GitLab on AWS",{"href":2495,"dataGaName":2493,"dataGaLocation":2480},"/partners/technology-partners/aws/",{"text":2497,"config":2498},"GitLab on Google Cloud",{"href":2499,"dataGaName":2497,"dataGaLocation":2480},"/partners/technology-partners/google-cloud-platform/",{"text":2501,"config":2502},"Why GitLab?",{"href":2174,"dataGaName":2501,"dataGaLocation":2480},{"freeTrial":2504,"mobileIcon":2509,"desktopIcon":2514,"secondaryButton":2517},{"text":2505,"config":2506},"Start free trial",{"href":2507,"dataGaName":2135,"dataGaLocation":2508},"https://gitlab.com/-/trials/new/","nav",{"altText":2510,"config":2511},"Gitlab Icon",{"src":2512,"dataGaName":2513,"dataGaLocation":2508},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203874/jypbw1jx72aexsoohd7x.svg","gitlab icon",{"altText":2510,"config":2515},{"src":2516,"dataGaName":2513,"dataGaLocation":2508},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203875/gs4c8p8opsgvflgkswz9.svg",{"text":2518,"config":2519},"Get Started",{"href":2520,"dataGaName":2521,"dataGaLocation":2508},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com/get-started/","get started",{"freeTrial":2523,"mobileIcon":2527,"desktopIcon":2529},{"text":2524,"config":2525},"Learn more about GitLab Duo",{"href":2166,"dataGaName":2526,"dataGaLocation":2508},"gitlab duo",{"altText":2510,"config":2528},{"src":2512,"dataGaName":2513,"dataGaLocation":2508},{"altText":2510,"config":2530},{"src":2516,"dataGaName":2513,"dataGaLocation":2508},{"button":2532,"mobileIcon":2537,"desktopIcon":2539},{"text":2533,"config":2534},"/switch",{"href":2535,"dataGaName":2536,"dataGaLocation":2508},"#contact","switch",{"altText":2510,"config":2538},{"src":2512,"dataGaName":2513,"dataGaLocation":2508},{"altText":2510,"config":2540},{"src":2541,"dataGaName":2513,"dataGaLocation":2508},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1773335277/ohhpiuoxoldryzrnhfrh.png",{"freeTrial":2543,"mobileIcon":2548,"desktopIcon":2550},{"text":2544,"config":2545},"Back to pricing",{"href":2279,"dataGaName":2546,"dataGaLocation":2508,"icon":2547},"back to pricing","GoBack",{"altText":2510,"config":2549},{"src":2512,"dataGaName":2513,"dataGaLocation":2508},{"altText":2510,"config":2551},{"src":2516,"dataGaName":2513,"dataGaLocation":2508},{"title":2553,"titleMobile":2554,"button":2555,"config":2560},"Duo Agent Platform delivers 400% ROI, per new Forrester Consulting study.","400% ROI: Forrester TEI for GitLab Duo",{"text":2172,"config":2556},{"href":2557,"dataGaName":2558,"dataGaLocation":2559},"https://about.gitlab.com/blog/gitlab-duo-agent-platform-delivers-400-percent-roi/","forrester-tei-dap-banner","global-banner",{"layout":2561,"disabled":2108},"release",{"data":2563},{"text":2564,"source":2565,"edit":2571,"contribute":2576,"config":2581,"items":2586,"minimal":2796},"Git is a trademark of Software Freedom Conservancy and our use of 'GitLab' is under license",{"text":2566,"config":2567},"View page source",{"href":2568,"dataGaName":2569,"dataGaLocation":2570},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/","page source","footer",{"text":2572,"config":2573},"Edit this page",{"href":2574,"dataGaName":2575,"dataGaLocation":2570},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/content/","web ide",{"text":2577,"config":2578},"Please contribute",{"href":2579,"dataGaName":2580,"dataGaLocation":2570},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/CONTRIBUTING.md/","please contribute",{"twitter":2582,"facebook":2583,"youtube":2584,"linkedin":2585},"https://twitter.com/gitlab","https://www.facebook.com/gitlab","https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg","https://www.linkedin.com/company/gitlab-com",[2587,2634,2688,2732,2764],{"title":2277,"links":2588,"subMenu":2603},[2589,2593,2598],{"text":2590,"config":2591},"View plans",{"href":2279,"dataGaName":2592,"dataGaLocation":2570},"view plans",{"text":2594,"config":2595},"Why Premium?",{"href":2596,"dataGaName":2597,"dataGaLocation":2570},"/pricing/premium/","why premium",{"text":2599,"config":2600},"Why Ultimate?",{"href":2601,"dataGaName":2602,"dataGaLocation":2570},"/pricing/ultimate/","why ultimate",[2604],{"title":2605,"links":2606},"Contact Us",[2607,2610,2612,2614,2619,2624,2629],{"text":2608,"config":2609},"Contact sales",{"href":2460,"dataGaName":2140,"dataGaLocation":2570},{"text":2463,"config":2611},{"href":2465,"dataGaName":2466,"dataGaLocation":2570},{"text":2468,"config":2613},{"href":2470,"dataGaName":2471,"dataGaLocation":2570},{"text":2615,"config":2616},"Status",{"href":2617,"dataGaName":2618,"dataGaLocation":2570},"https://status.gitlab.com/","status",{"text":2620,"config":2621},"Terms of use",{"href":2622,"dataGaName":2623,"dataGaLocation":2570},"/terms/","terms of use",{"text":2625,"config":2626},"Privacy statement",{"href":2627,"dataGaName":2628,"dataGaLocation":2570},"/privacy/","privacy statement",{"text":2630,"config":2631},"Cookie preferences",{"dataGaName":2632,"dataGaLocation":2570,"id":2633,"isOneTrustButton":153},"cookie preferences","ot-sdk-btn",{"title":2177,"links":2635,"subMenu":2644},[2636,2640],{"text":2637,"config":2638},"DevSecOps platform",{"href":2159,"dataGaName":2639,"dataGaLocation":2570},"devsecops platform",{"text":2641,"config":2642},"AI-Assisted Development",{"href":2166,"dataGaName":2643,"dataGaLocation":2570},"ai-assisted development",[2645],{"title":2646,"links":2647},"Topics",[2648,2653,2658,2663,2668,2673,2678,2683],{"text":2649,"config":2650},"CICD",{"href":2651,"dataGaName":2652,"dataGaLocation":2570},"/topics/ci-cd/","cicd",{"text":2654,"config":2655},"GitOps",{"href":2656,"dataGaName":2657,"dataGaLocation":2570},"/topics/gitops/","gitops",{"text":2659,"config":2660},"DevOps",{"href":2661,"dataGaName":2662,"dataGaLocation":2570},"/topics/devops/","devops",{"text":2664,"config":2665},"Version Control",{"href":2666,"dataGaName":2667,"dataGaLocation":2570},"/topics/version-control/","version control",{"text":2669,"config":2670},"DevSecOps",{"href":2671,"dataGaName":2672,"dataGaLocation":2570},"/topics/devsecops/","devsecops",{"text":2674,"config":2675},"Cloud Native",{"href":2676,"dataGaName":2677,"dataGaLocation":2570},"/topics/cloud-native/","cloud native",{"text":2679,"config":2680},"AI for Coding",{"href":2681,"dataGaName":2682,"dataGaLocation":2570},"/topics/devops/ai-for-coding/","ai for coding",{"text":2684,"config":2685},"Agentic AI",{"href":2686,"dataGaName":2687,"dataGaLocation":2570},"/topics/agentic-ai/","agentic ai",{"title":2689,"links":2690},"Solutions",[2691,2693,2695,2700,2704,2707,2711,2714,2716,2719,2722,2727],{"text":2221,"config":2692},{"href":2216,"dataGaName":2221,"dataGaLocation":2570},{"text":2210,"config":2694},{"href":2193,"dataGaName":2194,"dataGaLocation":2570},{"text":2696,"config":2697},"Agile development",{"href":2698,"dataGaName":2699,"dataGaLocation":2570},"/solutions/agile-delivery/","agile delivery",{"text":2701,"config":2702},"SCM",{"href":2206,"dataGaName":2703,"dataGaLocation":2570},"source code management",{"text":2649,"config":2705},{"href":2199,"dataGaName":2706,"dataGaLocation":2570},"continuous integration & delivery",{"text":2708,"config":2709},"Value stream management",{"href":2249,"dataGaName":2710,"dataGaLocation":2570},"value stream management",{"text":2654,"config":2712},{"href":2713,"dataGaName":2657,"dataGaLocation":2570},"/solutions/gitops/",{"text":2259,"config":2715},{"href":2262,"dataGaName":2263,"dataGaLocation":2570},{"text":2717,"config":2718},"Small business",{"href":2268,"dataGaName":2269,"dataGaLocation":2570},{"text":2720,"config":2721},"Public sector",{"href":2274,"dataGaName":2275,"dataGaLocation":2570},{"text":2723,"config":2724},"Education",{"href":2725,"dataGaName":2726,"dataGaLocation":2570},"/solutions/education/","education",{"text":2728,"config":2729},"Financial services",{"href":2730,"dataGaName":2731,"dataGaLocation":2570},"/solutions/finance/","financial services",{"title":2282,"links":2733},[2734,2736,2738,2740,2743,2745,2748,2750,2752,2754,2756,2758,2760,2762],{"text":2295,"config":2735},{"href":2297,"dataGaName":2298,"dataGaLocation":2570},{"text":2300,"config":2737},{"href":2302,"dataGaName":2303,"dataGaLocation":2570},{"text":2305,"config":2739},{"href":2307,"dataGaName":2308,"dataGaLocation":2570},{"text":2310,"config":2741},{"href":2312,"dataGaName":2742,"dataGaLocation":2570},"docs",{"text":2333,"config":2744},{"href":2335,"dataGaName":2336,"dataGaLocation":2570},{"text":2746,"config":2747},"What's new",{"href":2392,"dataGaName":2393,"dataGaLocation":2570},{"text":2328,"config":2749},{"href":2330,"dataGaName":2331,"dataGaLocation":2570},{"text":2347,"config":2751},{"href":2349,"dataGaName":2350,"dataGaLocation":2570},{"text":2355,"config":2753},{"href":2357,"dataGaName":2358,"dataGaLocation":2570},{"text":2360,"config":2755},{"href":2362,"dataGaName":2363,"dataGaLocation":2570},{"text":2365,"config":2757},{"href":2367,"dataGaName":2368,"dataGaLocation":2570},{"text":2370,"config":2759},{"href":2070,"dataGaName":2372,"dataGaLocation":2570},{"text":2374,"config":2761},{"href":2376,"dataGaName":2377,"dataGaLocation":2570},{"text":2379,"config":2763},{"href":2381,"dataGaName":2382,"dataGaLocation":2570},{"title":2395,"links":2765},[2766,2768,2770,2772,2774,2776,2780,2785,2787,2789,2791],{"text":2403,"config":2767},{"href":2405,"dataGaName":2397,"dataGaLocation":2570},{"text":2408,"config":2769},{"href":2410,"dataGaName":2411,"dataGaLocation":2570},{"text":2416,"config":2771},{"href":2418,"dataGaName":2419,"dataGaLocation":2570},{"text":2421,"config":2773},{"href":2423,"dataGaName":2424,"dataGaLocation":2570},{"text":2426,"config":2775},{"href":2428,"dataGaName":2429,"dataGaLocation":2570},{"text":2777,"config":2778},"Sustainability",{"href":2779,"dataGaName":2777,"dataGaLocation":2570},"/sustainability/",{"text":2781,"config":2782},"Diversity, inclusion and belonging (DIB)",{"href":2783,"dataGaName":2784,"dataGaLocation":2570},"/diversity-inclusion-belonging/","Diversity, inclusion and belonging",{"text":2431,"config":2786},{"href":2433,"dataGaName":2434,"dataGaLocation":2570},{"text":2441,"config":2788},{"href":2443,"dataGaName":2444,"dataGaLocation":2570},{"text":2446,"config":2790},{"href":2448,"dataGaName":2449,"dataGaLocation":2570},{"text":2792,"config":2793},"Modern Slavery Transparency Statement",{"href":2794,"dataGaName":2795,"dataGaLocation":2570},"https://handbook.gitlab.com/handbook/legal/modern-slavery-act-transparency-statement/","modern slavery transparency statement",{"items":2797},[2798,2801,2804],{"text":2799,"config":2800},"Terms",{"href":2622,"dataGaName":2623,"dataGaLocation":2570},{"text":2802,"config":2803},"Cookies",{"dataGaName":2632,"dataGaLocation":2570,"id":2633,"isOneTrustButton":153},{"text":2805,"config":2806},"Privacy",{"href":2627,"dataGaName":2628,"dataGaLocation":2570},[2808],{"id":2809,"title":7,"body":2107,"config":2810,"content":2812,"description":2107,"extension":2816,"meta":2817,"navigation":153,"path":2818,"seo":2819,"stem":2820,"__hash__":2821},"blogAuthors/en-us/blog/authors/michael-friedrich.yml",{"template":2811},"BlogAuthor",{"name":7,"config":2813},{"headshot":2814,"ctfId":2815},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1749659879/Blog/Author%20Headshots/dnsmichi-headshot.jpg","dnsmichi","yml",{},"/en-us/blog/authors/michael-friedrich",{},"en-us/blog/authors/michael-friedrich","lJ-nfRIhdG49Arfrxdn1Vv4UppwD51BB13S3HwIswt4",[2823,2832,2840],{"title":2824,"description":2825,"heroImage":2826,"category":2103,"date":2827,"authors":2828,"slug":2831,"externalUrl":2107},"Confidential AI for GitLab Self-Hosted","Give developers AI coding agents in GitLab Duo without source code leaving a hardware-encrypted boundary — no GPUs needed.","https://res.cloudinary.com/about-gitlab-com/image/upload/v1773866173/vte9qh8rriznvyclhkes.png","2026-08-06",[2829,2830],"Mathias Ewald","Martin Paloncy, Edgeless Systems","confidential-ai-for-gitlab-self-hosted",{"title":2833,"description":2834,"heroImage":2835,"category":2103,"date":2836,"authors":2837,"slug":2839,"externalUrl":2107},"Green DevOps: Why carbon measurement belongs in your CI/CD pipeline","CI/CD pipelines have a hidden carbon cost. Here's why measuring it matters, and how you can get started with Eco CI and Carmen in GitLab.","https://res.cloudinary.com/about-gitlab-com/image/upload/v1765809212/noh0mdfn9o94ry9ykura.png","2026-07-09",[2838],"Lysanne Pinto","green-devops-carbon-measurement-cicd-pipeline",{"title":2841,"description":2842,"heroImage":2843,"category":2103,"date":2844,"authors":2845,"slug":2847,"externalUrl":2107},"How to build CI/CD observability at scale","This practical guide to GitLab pipeline analytics helps self-managed users gain operational insights using Prometheus and Grafana.","https://res.cloudinary.com/about-gitlab-com/image/upload/v1774465167/n5hlvrsrheadeccyr1oz.png","2026-04-28",[2846],"Paul Meresanu","how-to-build-ci-cd-observability-at-scale",{"promotions":2849},[2850,2864,2876,2888],{"id":2851,"categories":2852,"header":2854,"text":2855,"button":2856,"image":2861},"ai-modernization",[2853],"ai","Is AI achieving its promise at scale?","Quiz will take 5 minutes or less",{"text":2857,"config":2858},"Get your AI maturity score",{"href":2859,"dataGaName":2860,"dataGaLocation":2336},"/assessments/ai-modernization-assessment/","modernization assessment",{"config":2862},{"src":2863},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1772138786/qix0m7kwnd8x2fh1zq49.png",{"id":2865,"categories":2866,"header":2868,"text":2855,"button":2869,"image":2873},"devops-modernization",[2867,2672],"product","Are you just managing tools or shipping innovation?",{"text":2870,"config":2871},"Get your DevOps maturity score",{"href":2872,"dataGaName":2860,"dataGaLocation":2336},"/assessments/devops-modernization-assessment/",{"config":2874},{"src":2875},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1772138785/eg818fmakweyuznttgid.png",{"id":2877,"categories":2878,"header":2880,"text":2855,"button":2881,"image":2885},"security-modernization",[2879],"security","Are you trading speed for security?",{"text":2882,"config":2883},"Get your security maturity score",{"href":2884,"dataGaName":2860,"dataGaLocation":2336},"/assessments/security-modernization-assessment/",{"config":2886},{"src":2887},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1772138786/p4pbqd9nnjejg5ds6mdk.png",{"id":2889,"paths":2890,"header":2893,"text":2894,"button":2895,"image":2900},"github-azure-migration",[2891,2892],"migration-from-azure-devops-to-gitlab","integrating-azure-devops-scm-and-gitlab","Is your team ready for GitHub's Azure move?","GitHub is already rebuilding around Azure. Find out what it means for you.",{"text":2896,"config":2897},"See how GitLab compares to GitHub",{"href":2898,"dataGaName":2899,"dataGaLocation":2336},"/compare/gitlab-vs-github/github-azure-migration/","github azure migration",{"config":2901},{"src":2875},{"header":2903,"blurb":2904,"button":2905,"secondaryButton":2910},"Start building faster today","See what your team can do with the intelligent orchestration platform for DevSecOps.\n",{"text":2906,"config":2907},"Get your free trial",{"href":2908,"dataGaName":2135,"dataGaLocation":2909},"https://gitlab.com/-/trial_registrations/new?glm_content=default-saas-trial&glm_source=about.gitlab.com/","feature",{"text":2608,"config":2911},{"href":2460,"dataGaName":2140,"dataGaLocation":2909},1786803751396]