Compare commits
23 Commits
1c61dcf599
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7be5dd910b | |||
| 07bf07f78e | |||
| 132f194432 | |||
| 9138b45459 | |||
| 5f56173ae9 | |||
| 0f6bdfd02b | |||
| b8594dff65 | |||
| e7ddb7bc82 | |||
| 40e42fd8c6 | |||
| 05e5ffdad4 | |||
| c9bbffc3bf | |||
| f6026656d0 | |||
| 9cda70f120 | |||
| 612222c222 | |||
| 1147582f48 | |||
| f4076d1cc7 | |||
| 97a9aa1d45 | |||
| c8e6459276 | |||
| ca2a6bc0ef | |||
| 2057150a05 | |||
| 4036fb658d | |||
| 6fb1f08915 | |||
| 9e0acd35de |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 720"><path fill="#0a0b0b" d="M350.4,9.6C141.8,20.5,4.1,184.1,12.8,390.4c3.8,90.3,40.1,168,48.7,253.7,2.2,22.2-4.2,49.6,21.4,59.3,31.5,11.9,79.8-8.1,106.2-26.4,9-6.1,17.6-13.2,24.2-22,27.3,18.1,53.2,35.6,85.7,43.4,143.1,34.3,299.9-44.2,369.6-170.3C799.6,291.2,622.5-4.6,350.4,9.6h0ZM269.4,504c-11.3,8.8-22.2,20.8-34.7,27.7-18.1,9.7-23.7-.4-30.5-16.4-21.4-50.9-24-137.6-11.5-190.9,16.8-72.5,72.9-136.3,150-143.1,78-6.9,150.4,32.7,183.1,104.2,72.4,159.1-112.9,316.2-256.4,218.6h0Z"/></svg>
|
||||
|
After Width: | Height: | Size: 544 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M220.9 123.3c1 .5 1.8 1.7 3 1.7 1.1 0 2.8-.4 2.9-1.5 .2-1.4-1.9-2.3-3.2-2.9-1.7-.7-3.9-1-5.5-.1-.4 .2-.8 .7-.6 1.1 .3 1.3 2.3 1.1 3.4 1.7zM199 125c1.2 0 2-1.2 3-1.7 1.1-.6 3.1-.4 3.5-1.6 .2-.4-.2-.9-.6-1.1-1.6-.9-3.8-.6-5.5 .1-1.3 .6-3.4 1.5-3.2 2.9 .1 1 1.8 1.5 2.8 1.4zM420 403.8c-3.6-4-5.3-11.6-7.2-19.7-1.8-8.1-3.9-16.8-10.5-22.4-1.3-1.1-2.6-2.1-4-2.9-1.3-.8-2.7-1.5-4.1-2 9.2-27.3 5.6-54.5-3.7-79.1-11.4-30.1-31.3-56.4-46.5-74.4-17.1-21.5-33.7-41.9-33.4-72 .5-45.9 5.1-131.2-75.8-131.3-102.4-.2-76.8 103.4-77.9 135.2-1.7 23.4-6.4 41.8-22.5 64.7-18.9 22.5-45.5 58.8-58.1 96.7-6 17.9-8.8 36.1-6.2 53.3-6.5 5.8-11.4 14.7-16.6 20.2-4.2 4.3-10.3 5.9-17 8.3s-14 6-18.5 14.5c-2.1 3.9-2.8 8.1-2.8 12.4 0 3.9 .6 7.9 1.2 11.8 1.2 8.1 2.5 15.7 .8 20.8-5.2 14.4-5.9 24.4-2.2 31.7 3.8 7.3 11.4 10.5 20.1 12.3 17.3 3.6 40.8 2.7 59.3 12.5 19.8 10.4 39.9 14.1 55.9 10.4 11.6-2.6 21.1-9.6 25.9-20.2 12.5-.1 26.3-5.4 48.3-6.6 14.9-1.2 33.6 5.3 55.1 4.1 .6 2.3 1.4 4.6 2.5 6.7l0 .1c8.3 16.7 23.8 24.3 40.3 23 16.6-1.3 34.1-11 48.3-27.9 13.6-16.4 36-23.2 50.9-32.2 7.4-4.5 13.4-10.1 13.9-18.3 .4-8.2-4.4-17.3-15.5-29.7zM223.8 87.3c9.8-22.2 34.2-21.8 44-.4 6.5 14.2 3.6 30.9-4.3 40.4-1.6-.8-5.9-2.6-12.6-4.9 1.1-1.2 3.1-2.7 3.9-4.6 4.8-11.8-.2-27-9.1-27.3-7.3-.5-13.9 10.8-11.8 23-4.1-2-9.4-3.5-13-4.4-1-6.9-.3-14.6 2.9-21.8zM183.1 75.8c10.1 0 20.8 14.2 19.1 33.5-3.5 1-7.1 2.5-10.2 4.6 1.2-8.9-3.3-20.1-9.6-19.6-8.4 .7-9.8 21.2-1.8 28.1 1 .8 1.9-.2-5.9 5.5-15.6-14.6-10.5-52.1 8.4-52.1zm-13.6 60.7c6.2-4.6 13.6-10 14.1-10.5 4.7-4.4 13.5-14.2 27.9-14.2 7.1 0 15.6 2.3 25.9 8.9 6.3 4.1 11.3 4.4 22.6 9.3 8.4 3.5 13.7 9.7 10.5 18.2-2.6 7.1-11 14.4-22.7 18.1-11.1 3.6-19.8 16-38.2 14.9-3.9-.2-7-1-9.6-2.1-8-3.5-12.2-10.4-20-15-8.6-4.8-13.2-10.4-14.7-15.3-1.4-4.9 0-9 4.2-12.3zm3.3 334c-2.7 35.1-43.9 34.4-75.3 18-29.9-15.8-68.6-6.5-76.5-21.9-2.4-4.7-2.4-12.7 2.6-26.4l0-.2c2.4-7.6 .6-16-.6-23.9-1.2-7.8-1.8-15 .9-20 3.5-6.7 8.5-9.1 14.8-11.3 10.3-3.7 11.8-3.4 19.6-9.9 5.5-5.7 9.5-12.9 14.3-18 5.1-5.5 10-8.1 17.7-6.9 8.1 1.2 15.1 6.8 21.9 16l19.6 35.6c9.5 19.9 43.1 48.4 41 68.9zm-1.4-25.9c-4.1-6.6-9.6-13.6-14.4-19.6 7.1 0 14.2-2.2 16.7-8.9 2.3-6.2 0-14.9-7.4-24.9-13.5-18.2-38.3-32.5-38.3-32.5-13.5-8.4-21.1-18.7-24.6-29.9s-3-23.3-.3-35.2c5.2-22.9 18.6-45.2 27.2-59.2 2.3-1.7 .8 3.2-8.7 20.8-8.5 16.1-24.4 53.3-2.6 82.4 .6-20.7 5.5-41.8 13.8-61.5 12-27.4 37.3-74.9 39.3-112.7 1.1 .8 4.6 3.2 6.2 4.1 4.6 2.7 8.1 6.7 12.6 10.3 12.4 10 28.5 9.2 42.4 1.2 6.2-3.5 11.2-7.5 15.9-9 9.9-3.1 17.8-8.6 22.3-15 7.7 30.4 25.7 74.3 37.2 95.7 6.1 11.4 18.3 35.5 23.6 64.6 3.3-.1 7 .4 10.9 1.4 13.8-35.7-11.7-74.2-23.3-84.9-4.7-4.6-4.9-6.6-2.6-6.5 12.6 11.2 29.2 33.7 35.2 59 2.8 11.6 3.3 23.7 .4 35.7 16.4 6.8 35.9 17.9 30.7 34.8-2.2-.1-3.2 0-4.2 0 3.2-10.1-3.9-17.6-22.8-26.1-19.6-8.6-36-8.6-38.3 12.5-12.1 4.2-18.3 14.7-21.4 27.3-2.8 11.2-3.6 24.7-4.4 39.9-.5 7.7-3.6 18-6.8 29-32.1 22.9-76.7 32.9-114.3 7.2zm257.4-11.5c-.9 16.8-41.2 19.9-63.2 46.5-13.2 15.7-29.4 24.4-43.6 25.5s-26.5-4.8-33.7-19.3c-4.7-11.1-2.4-23.1 1.1-36.3 3.7-14.2 9.2-28.8 9.9-40.6 .8-15.2 1.7-28.5 4.2-38.7 2.6-10.3 6.6-17.2 13.7-21.1 .3-.2 .7-.3 1-.5 .8 13.2 7.3 26.6 18.8 29.5 12.6 3.3 30.7-7.5 38.4-16.3 9-.3 15.7-.9 22.6 5.1 9.9 8.5 7.1 30.3 17.1 41.6 10.6 11.6 14 19.5 13.7 24.6zM173.4 148.7c2 1.9 4.7 4.5 8 7.1 6.6 5.2 15.8 10.6 27.3 10.6 11.6 0 22.5-5.9 31.8-10.8 4.9-2.6 10.9-7 14.8-10.4s5.9-6.3 3.1-6.6-2.6 2.6-6 5.1c-4.4 3.2-9.7 7.4-13.9 9.8-7.4 4.2-19.5 10.2-29.9 10.2s-18.7-4.8-24.9-9.7c-3.1-2.5-5.7-5-7.7-6.9-1.5-1.4-1.9-4.6-4.3-4.9-1.4-.1-1.8 3.7 1.7 6.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M219.8 171.3c3.9 .5 13.1 2.7 12.3 8.5-.8 5.9-9.2 9.1-14.2 8.4-4.7-.7-13.2-6.1-12.3-12.1l.3-2.1c4.2 0 8.3-3.5 13.9-2.7zm168.1-3.4c10.8 2.6-1.1 13.5-6.8 14.3-4.3 .6-12.1-2.2-12.8-7.4-1-7.4 15.7-7.9 19.7-6.9zm-22-140.8c35.7-.3 81.3 9.4 114.3 51.7 7.2 9.2 10 24 9.7 40.9-.8 49.6-26 129.4-70.7 197.4 4.5 2.9 17.6 7.4 51.1 .5 5.6-1.2 12.7-2.2 17.6 1.6 18.2 13.9-19.6 35.1-28.4 39.2-13.2 6.2-34.8 9.5-51.2 8.7-2.1-.2-4.3-.2-6.3-.6-5.1-1.1-7.4-1-8.3-.7-1.1 .3-1.4 2.9-1.6 3.8-2.8 24.9-7.7 64.7-10.7 82-2.8 16.3-7.7 29.3-17.2 39.2-9.5 9.9-22.8 15.7-40.6 19.5-22.3 4.8-37.9-.1-48.7-9.1-10.3-8.7-15.2-20.4-18-27.4-1.8-4.5-3-11.5-4-19.8-2.3-19.8-3.3-50.4-3-83.3-24.6 22.1-55 17.2-68.2 13.9-10.5-2.6-33-16.1-17.5-28.7 11.9-9.7 30.3-5.5 42.2-15 2.4-1.9 11.4-10.6 11.4-13.5-10-.3-19.6-2.9-28.1-7.5-13.5 14.5-26.4 29.5-39.3 44.6-8.3 9.9-17.4 15.8-27.4 16.2-9.9 .4-18.7-4.6-26.1-11.8-7.3-7.1-14.1-17.2-20.4-29-19-35.5-33.2-86.1-42.3-126.4-6-26.7-9.6-49.1-10.1-59-2.2-44.3 8-74.1 26-93.2 17.9-19 42.4-26 66.1-27.4 35.6-2 71 8.5 86.6 13.8l5 1.8c15.9-10.8 36.1-17.4 61.7-17 13.2 .2 25.5 2.2 36.7 4.2 18.6-7.1 39.8-9.5 59.4-9.7zm-96 20.5c-24.7-.4-42.9 6.5-56.6 16.8-.8 .6-1.8 1-2.8 1.2-14.4 11.8-23.9 28-30.3 44.8-7.2 19.1-10 38.5-11.1 51.5 7.6-4.3 17.9-8.7 28.7-11.2 10.5-2.4 22.4-3.2 32.7 .8 10.9 4.2 19 13.3 22.2 28.1 7.5 34.7 6.7 58.2 2.7 75.9-4.8 21-16.8 39.5-21.9 60.4 3.5-.9 7.1-.6 9.8 .1l7.2 2.9c7.7 4.4 12.8 13 14 21.7 2 6 .1 14.3 0 20.6 6.7 16.3 7.2 36.1 6.7 53.3-.7 25-1 40.2 3.2 51.7 2.9 7.9 4.7 16.4 10.4 22.8 2.6 3 6.1 5.5 10.9 6.8 18.5 5.1 44-4.7 56.6-18.2 7.7-8.2 12.3-19.3 13.4-33.9 1.1-13.6 4.2-27.6 6.3-41.1l2.9-8.8c1.7-14.8 3.4-29.6 5-44.4-.4-9.1 .9-16.1 3.9-21.5 3.1-5.7 7.6-8.9 11.8-10.8 1.8-.8 3.9-1.2 5.6-2-1.6-2.4-3.6-4.6-5.3-6.8-8.2-10.4-13.3-22.3-19.7-33.8-8.5-15.2-23.8-42.2-30.1-67.5-4.1-16.4-4.9-34.5 6-47 9.8-11.2 26.9-15.5 51.9-13-3.4-10-11.4-27.5-24.8-44.7-18-23-45.7-45.9-85.7-53.1-7.3-.9-15.2-1.5-23.6-1.7zm-32.2 282c-8 .7-15.5 18.2-21.7 23.1-6.2 4.9-14.5 7.6-30 10.7-4.5 .9-7.7 1.9-9.7 2.8 22.3 15.8 58.3 3.2 72.6-16.8 1.7-2.4 2.1-6 .5-10.2-1.7-4.5-6.4-10.1-11.9-9.6zM117.6 49.2c-21.6 1.2-42 7.5-56.4 22.8-14.4 15.2-24.2 40.6-22.1 82.5 .4 8.5 3.8 30 9.8 56.6 8.9 39.8 23.7 90.3 40.6 122.2 6 11.1 17.4 33 32.8 32.3 4.4-.2 10.1-2.8 17-11 12.6-14.8 25.2-29.5 38.5-43.7-17.9-15.4-28.5-40.3-24.8-67.2 3.4-24.3 .5-48.2 1.2-72.5 .4-11.9 2.3-38.7 12.6-65.8 5.9-15.5 14.5-31.3 27.4-44.2-16.4-5.4-47.1-13.5-76.6-11.9zM405.1 328.9c-3.8 1.1-6.7 2.2-8.7 5.8-1.3 2.4-2.5 6.7-2.2 14.2 4.8 3.9 14.1 3.3 19.8 3.2 13.9-.2 29.9-3.1 39.3-7.5 7.9-3.7 14.7-8.3 19-12.2-38.2 7.7-55.4 2.1-63.2-4.8-1.3 .4-2.9 .9-3.9 1.2zM225.3 164.9c-15.4-5.9-35.4 1.8-48.9 9.4-3.6 2-6.5 4-7.9 5.2 .4 8.8 2.8 36.1-1.4 66.3-5.1 36.6 21.7 66.6 52.5 66.7 5.1-20.8 17-39.2 21.8-60.1 3.5-15.3 4.5-36.6-2.7-69.7-2.3-10.5-7.4-15.4-13.4-17.7zM370.1 42.2c-15.2-.2-28.8 1.7-39.2 3.8 26 11.8 45.6 29.7 59.6 47.6 17.4 22.2 26.3 44.1 29 55 .7 2.7 1.3 5.7 .5 8.4 .6 18.9-4 31.3-4.6 49.2-.4 12.9 2.9 28.1 3.7 44.7 .8 15.7-1 32.5-11.2 49.5 .8 1 1.6 2.1 2.4 3.1 26.8-42.2 46-88.6 56.3-128.5 5.5-21.4 8.4-40.7 8.7-56.1 .2-15.2-2-25.4-5.8-30.4-28.3-36.1-66.8-45.8-99.3-46.2zm35.6 119.5c-25.3-3.2-37.3 1.5-42.8 7.7-6 6.8-6.8 18.5-2.9 34.1 5.7 22.6 20.1 48.6 28.7 64 3.6 6.4 6.4 13.1 10 19.4 5.5-11.7 6.6-23.5 6-35.5-.7-15-4.2-30.3-3.7-45.8l.4-7.4c1.2-14.3 4.1-24.6 4.2-36.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M31.5 63.5C0 95 0 145.7 0 247L0 265C0 366.3 0 417 31.5 448.5S113.7 480 215 480l17.9 0c101.4 0 152.1 0 183.5-31.5S448 366.3 448 265l0-17.9c0-101.4 0-152.1-31.5-183.5S334.3 32 233 32L215 32C113.7 32 63 32 31.5 63.5zM75.6 168.3l51.1 0c1.7 85.5 39.4 121.7 69.3 129.2l0-129.2 48.2 0 0 73.7c29.5-3.2 60.5-36.8 70.9-73.7l48.2 0c-3.9 19.2-11.8 37.3-23.1 53.3s-25.7 29.5-42.5 39.6c18.7 9.3 35.2 22.4 48.4 38.5s22.9 34.9 28.3 55l-53 0c-4.9-17.5-14.8-33.1-28.6-45s-30.7-19.4-48.7-21.6l0 66.6-5.8 0c-102.1 0-160.3-70-162.8-186.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 617 B |
|
After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 9.2 KiB |
@@ -22,11 +22,12 @@ title = "Олег Казанин"
|
||||
headline = "Infrastructure Engineer | Linux & Open Source"
|
||||
bio = "Строю полезную инфраструктуру на Open Source стеке. Документирую грабли, чтобы вы на них не наступали."
|
||||
links = [
|
||||
{ vk = "https://vk.com/oakazanin/" },
|
||||
{ email = "mailto:oakazanin@ya.ru" },
|
||||
{ telegram = "https://t.me/oa_msk" },
|
||||
{ link = "https://oakazanin.ru/" },
|
||||
{ gitea = "https://git.jn4.ru/astronit" },
|
||||
{ peertube = "https://obrtv.ru/a/chiefengineer" },
|
||||
{ peertube = "https://xn--80abjycsbu.xn--p1acf/a/chiefengineer/video-channels" },
|
||||
# { amazon = "https://www.amazon.com/hz/wishlist/ls/wishlist-id" },
|
||||
# { apple = "https://www.apple.com" },
|
||||
# { blogger = "https://username.blogspot.com/" },
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
# https://blowfish.page/docs/configuration/#theme-parameters
|
||||
|
||||
colorScheme = "blowfish"
|
||||
defaultAppearance = "dark" # valid options: light or dark
|
||||
defaultAppearance = "light" # valid options: light or dark
|
||||
autoSwitchAppearance = false
|
||||
|
||||
enableA11y = false
|
||||
enableA11y = true
|
||||
enableSearch = true
|
||||
enableCodeCopy = false
|
||||
enableCodeCopy = true
|
||||
enableStructuredBreadcrumbs = false
|
||||
# enableStyledScrollbar = true # disable to use native scrollbar style (defaults to true)
|
||||
|
||||
@@ -20,18 +20,19 @@ replyByEmail = false
|
||||
# mainSections = ["section1", "section2"]
|
||||
# robots = ""
|
||||
|
||||
disableImageOptimization = false
|
||||
disableImageOptimizationMD = false
|
||||
disableImageZoom = true
|
||||
disableImageOptimization = true
|
||||
# disableImageOptimizationMD = false
|
||||
disableTextInHeader = true
|
||||
# backgroundImageWidth = 1200
|
||||
|
||||
defaultBackgroundImage = "/img/background.png" # used as default for background images
|
||||
defaultFeaturedImage = "/img/background.png" # used as default for featured images in all articles
|
||||
defaultBackgroundImage = "/img/background.svg" # used as default for background images
|
||||
# defaultFeaturedImage = "/img/background.png" # used as default for featured images in all articles
|
||||
# defaultSocialImage = "/android-chrome-512x512.png" # used as default for social media sharing (Open Graph and Twitter)
|
||||
hotlinkFeatureImage = false
|
||||
# imagePosition = "50% 50%"
|
||||
|
||||
# highlightCurrentMenuArea = true
|
||||
highlightCurrentMenuArea = true
|
||||
# smartTOC = true
|
||||
# smartTOCHideUnfocusedChildren = true
|
||||
|
||||
@@ -54,65 +55,65 @@ forgejoDefaultServer = "https://v11.next.forgejo.org"
|
||||
layout = "background" # valid options: page, profile, hero, card, background, custom
|
||||
#homepageImage = "IMAGE.jpg" # used in: hero, and card
|
||||
showRecent = true
|
||||
showRecentItems = 18
|
||||
showRecentItems = 5
|
||||
showMoreLink = true
|
||||
showMoreLinkDest = "/posts"
|
||||
cardView = true
|
||||
cardView = false
|
||||
cardViewScreenWidth = false
|
||||
layoutBackgroundBlur = true # only used when layout equals background
|
||||
disableHeroImageFilter = false # only used when layout equals hero
|
||||
disableHeroImageFilter = true # only used when layout equals hero
|
||||
|
||||
[article]
|
||||
showDate = true
|
||||
showViews = true
|
||||
showLikes = true
|
||||
showDateOnlyInArticle = false
|
||||
showDateUpdated = false
|
||||
# showViews = false
|
||||
# showLikes = false
|
||||
# showDateOnlyInArticle = false
|
||||
# showDateUpdated = false
|
||||
showAuthor = true
|
||||
# showAuthorBottom = false
|
||||
showHero = true
|
||||
heroStyle = "background" # valid options: basic, big, background, thumbAndBackground
|
||||
layoutBackgroundBlur = true # only used when heroStyle equals background or thumbAndBackground
|
||||
layoutBackgroundHeaderSpace = true # only used when heroStyle equals background
|
||||
showBreadcrumbs = false
|
||||
#showBreadcrumbs = false
|
||||
showDraftLabel = true
|
||||
showEdit = false
|
||||
#showEdit = false
|
||||
# editURL = "https://github.com/username/repo/"
|
||||
editAppendPath = true
|
||||
# editAppendPath = true
|
||||
seriesOpened = true
|
||||
showHeadingAnchors = true
|
||||
showPagination = true
|
||||
invertPagination = false
|
||||
# invertPagination = false
|
||||
showReadingTime = true
|
||||
showTableOfContents = true
|
||||
showRelatedContent = true
|
||||
relatedContentLimit = 6
|
||||
showTaxonomies = true # use showTaxonomies OR showCategoryOnly, not both
|
||||
showCategoryOnly = false # use showTaxonomies OR showCategoryOnly, not both
|
||||
relatedContentLimit = 3
|
||||
# showTaxonomies = true # use showTaxonomies OR showCategoryOnly, not both
|
||||
showCategoryOnly = true # use showTaxonomies OR showCategoryOnly, not both
|
||||
showAuthorsBadges = false
|
||||
showWordCount = false
|
||||
# showWordCount = false
|
||||
# sharingLinks = [ "linkedin", "twitter", "bluesky", "mastodon", "reddit", "pinterest", "facebook", "email", "whatsapp", "telegram", "line"]
|
||||
sharingLinks = [ "reddit", "email", "telegram"]
|
||||
showZenMode = false
|
||||
# showZenMode = false
|
||||
# externalLinkForceNewTab = false # disable to allow external links in the same tab (defaults to true)
|
||||
# showComments = true
|
||||
showComments = true
|
||||
|
||||
[list]
|
||||
showHero = true
|
||||
heroStyle = "background" # valid options: basic, big, background, thumbAndBackground
|
||||
layoutBackgroundBlur = true # only used when heroStyle equals background or thumbAndBackground
|
||||
layoutBackgroundHeaderSpace = true # only used when heroStyle equals background
|
||||
showBreadcrumbs = false
|
||||
showSummary = false
|
||||
showViews = false
|
||||
showLikes = false
|
||||
showTableOfContents = false
|
||||
showCards = false
|
||||
orderByWeight = false
|
||||
groupByYear = false
|
||||
cardView = false
|
||||
cardViewScreenWidth = false
|
||||
constrainItemsWidth = false
|
||||
# showBreadcrumbs = false
|
||||
showSummary = true
|
||||
# showViews = false
|
||||
# showLikes = false
|
||||
# showTableOfContents = false
|
||||
showCards = true
|
||||
# orderByWeight = false
|
||||
# groupByYear = false
|
||||
# cardView = false
|
||||
# cardViewScreenWidth = false
|
||||
# constrainItemsWidth = false
|
||||
|
||||
[sitemap]
|
||||
excludedKinds = ["taxonomy", "term"]
|
||||
|
||||
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 5.2 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 5.2 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
@@ -29,3 +29,81 @@ layoutBackgroundHeaderSpace: false
|
||||
Дома - лаборатория. Нет, не стойка в подвале (хотя идея заманчивая). Один сервер, но серьёзный: виртуализация, Kubernetes, хранилище, мониторинг - всё по-взрослому, но если что-то упало - нет многомиллионных издержек и мир не остановился. Идеальный полигон: придумал, реализовал, сломал, починил, задокументировал. Полный цикл.
|
||||
|
||||
Этот блог - не enterprise-гайды для внедрения в банке. Это production, но в человеческом масштабе: домашний сервер, небольшая компания, стартап на трёх разработчиках. Решения, которые реально работают - просто без бюджета на команду SRE из десяти человек - инженеров надёжности, чьи зарплаты съедают бюджет стартапа за квартал. Документирую грабли, чтобы вы на них не наступали. Чем больнее наступил сам - тем подробнее статья.
|
||||
|
||||
# Прокачка скиллов
|
||||
|
||||
{{< timeline >}}
|
||||
|
||||
{{< timelineItem icon="postgresql" header="Администратор баз данных PostgreSQL" badge="2025">}}
|
||||
{{< gallery >}}
|
||||
<img src="gallery/admin_pgs.jpg" class="grid-w100" />
|
||||
{{< /gallery >}}
|
||||
{{< /timelineItem >}}
|
||||
|
||||
{{< timelineItem icon="code" header="Анализ данных на языке SQL" badge="2025">}}
|
||||
{{< gallery >}}
|
||||
<img src="gallery/sql_eng.gif" class="grid-w50" />
|
||||
<img src="gallery/sql_ru.gif" class="grid-w50" />
|
||||
{{< /gallery >}}
|
||||
{{< /timelineItem >}}
|
||||
|
||||
{{< timelineItem icon="postgresql" header="DBA1 - Администрирование PostgreSQL. Базовый курс" badge="2025" >}}
|
||||
{{< gallery >}}
|
||||
<img src="gallery/dba1_eng.gif" class="grid-w33" />
|
||||
<img src="gallery/dba1_ru.gif" class="grid-w33" />
|
||||
<img src="gallery/dba1_pgs.jpg" class="grid-w33" />
|
||||
{{< /gallery >}}
|
||||
{{< /timelineItem >}}
|
||||
|
||||
{{< timelineItem icon="postgresql" header="DBA2 - Администрирование PostgreSQL. Настройка и мониторинг" badge="2025" >}}
|
||||
{{< gallery >}}
|
||||
<img src="gallery/dba2_eng.gif" class="grid-w33" />
|
||||
<img src="gallery/dba2_ru.gif" class="grid-w33" />
|
||||
<img src="gallery/dba2_pgs.jpg" class="grid-w33" />
|
||||
{{< /gallery >}}
|
||||
{{< /timelineItem >}}
|
||||
|
||||
{{< timelineItem icon="postgresql" header="DBA3 - Администрирование PostgreSQL 16. Резервное копирование и репликация" badge="2025" >}}
|
||||
{{< gallery >}}
|
||||
<img src="gallery/dba3_eng.gif" class="grid-w33" />
|
||||
<img src="gallery/dba3_ru.gif" class="grid-w33" />
|
||||
<img src="gallery/dba3_pgs.jpg" class="grid-w33" />
|
||||
{{< /gallery >}}
|
||||
{{< /timelineItem >}}
|
||||
|
||||
{{< timelineItem icon="postgresql" header="QPT - PostgreSQL. Оптимизация запросов" badge="2025" >}}
|
||||
{{< gallery >}}
|
||||
<img src="gallery/qpt_eng.gif" class="grid-w33" />
|
||||
<img src="gallery/qpt_ru.gif" class="grid-w33" />
|
||||
<img src="gallery/qpt_pgs.jpg" class="grid-w33" />
|
||||
{{< /gallery >}}
|
||||
{{< /timelineItem >}}
|
||||
|
||||
{{< timelineItem icon="postgresql" header="PGPRO - Postgres Pro Enterprise" badge="2025" >}}
|
||||
{{< gallery >}}
|
||||
<img src="gallery/pgpro_eng.gif" class="grid-w33" />
|
||||
<img src="gallery/pgpro_ru.gif" class="grid-w33" />
|
||||
<img src="gallery/pgpro_pgs.jpg" class="grid-w33" />
|
||||
{{< /gallery >}}
|
||||
{{< /timelineItem >}}
|
||||
|
||||
{{< timelineItem icon="linux" header="Сетевое администрирование Astra Linux SE" badge="2024" >}}
|
||||
{{< gallery >}}
|
||||
<img src="gallery/alse-1604.jpg" class="grid-w33" />
|
||||
<img src="gallery/alse-1604_softline.jpg" class="grid-w33" />
|
||||
<img src="gallery/alse-1604_softline-cert.jpg" class="grid-w33" />
|
||||
{{< /gallery >}}
|
||||
{{< /timelineItem >}}
|
||||
|
||||
{{< timelineItem icon="linux" header="Astra Linux. Специальный курс" badge="2024" >}}
|
||||
{{< gallery >}}
|
||||
<img src="gallery/alse-1605.jpg" class="grid-w33" />
|
||||
<img src="gallery/alse-1605_softline.jpg" class="grid-w33" />
|
||||
<img src="gallery/alse-1605_softline-cert.jpg" class="grid-w33" />
|
||||
{{< /gallery >}}
|
||||
{{< /timelineItem >}}
|
||||
|
||||
|
||||
|
||||
{{< /timeline >}}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ slug: "shortcodes"
|
||||
tags: ["shortcodes", "mermaid", "icon", "lead", "docs"]
|
||||
series: ["Документация"]
|
||||
series_order: 8
|
||||
showComments: true
|
||||
---
|
||||
|
||||
Hugo даёт [стандартные shortcodes](https://gohugo.io/content-management/shortcodes/). Blowfish добавляет свои — для типичных задач блога.
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 MiB |
@@ -5,7 +5,6 @@ draft: false
|
||||
description: "Универсальная шпаргалка по работе с Hugo блогом через Git с двумя окружениями. Префиксы коммитов, откаты, типичные ошибки и решения."
|
||||
tags: ["git", "workflow", "hugo", "cheatsheet"]
|
||||
categories: ["Шпаргалки"]
|
||||
showComments: true
|
||||
---
|
||||
|
||||
## Структура проекта
|
||||
|
||||
|
Before Width: | Height: | Size: 1008 KiB |
@@ -4,7 +4,7 @@ date: 2026-01-03
|
||||
draft: false
|
||||
description: "Переезд с Jekyll на Hugo: выбор темы Blowfish, настройка K3s кластера и два окружения с нуля. Старт цикла о production блоге на Open Source стеке."
|
||||
tags: ["hugo", "blowfish", "gitea", "homelab", "devops"]
|
||||
categories: ["Веб-разработка"]
|
||||
categories: ["Мои проекты"]
|
||||
series: ["Блог на Hugo в K3s"]
|
||||
series_order: 1
|
||||
---
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 MiB |
@@ -4,7 +4,7 @@ date: 2026-01-08
|
||||
draft: false
|
||||
description: "Деплой Hugo в K3s: NFS хранилище, Hugo Builder с webhook, Nginx с Prometheus exporter и автоматический SSL от Let's Encrypt."
|
||||
tags: ["hugo", "k3s", "kubernetes", "nfs", "traefik", "cert-manager"]
|
||||
categories: ["Kubernetes", "Веб-разработка"]
|
||||
categories: ["Мои проекты"]
|
||||
series: ["Блог на Hugo в K3s"]
|
||||
series_order: 2
|
||||
---
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 MiB |
@@ -4,7 +4,7 @@ date: 2026-01-15
|
||||
draft: false
|
||||
description: "Dev окружение для Hugo в K3s: отдельный Hugo Builder, Nginx и Basic Auth через Traefik. Проверяем статьи до публикации в production."
|
||||
tags: ["hugo", "k3s", "kubernetes", "traefik", "basic-auth"]
|
||||
categories: ["Веб-разработка", "DevOps практики"]
|
||||
categories: ["Мои проекты"]
|
||||
series: ["Блог на Hugo в K3s"]
|
||||
series_order: 3
|
||||
---
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 MiB |
@@ -3,8 +3,9 @@ title: "Блог на Hugo в K3s: часть 4 - выбор Git workflow"
|
||||
date: 2026-02-16
|
||||
draft: false
|
||||
description: "Одна папка или две для dev и production? Разбираем Git workflow для Hugo блога в K3s — переключение веток против отдельных директорий."
|
||||
summary: "Одна папка или две для dev и production? Разбираем Git workflow для Hugo блога в K3s — переключение веток против отдельных директорий."
|
||||
tags: ["git", "workflow", "devops", "hugo"]
|
||||
categories: ["Веб-разработка", "DevOps практики"]
|
||||
categories: ["Мои проекты"]
|
||||
series: ["Блог на Hugo в K3s"]
|
||||
series_order: 4
|
||||
---
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 MiB |
@@ -4,7 +4,7 @@ date: 2026-02-17
|
||||
draft: false
|
||||
description: "Алгоритм диагностики Hugo блога в K3s за 5 минут: от DNS до пода. Сайт отдаёт 503 — находим причину без паники."
|
||||
tags: ["kubernetes", "k3s", "traefik", "debugging", "nginx", "troubleshooting"]
|
||||
categories: ["Kubernetes", "DevOps практики"]
|
||||
categories: ["Мои проекты"]
|
||||
series: ["Блог на Hugo в K3s"]
|
||||
series_order: 5
|
||||
---
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 MiB |
@@ -4,7 +4,7 @@ date: 2026-02-18
|
||||
draft: false
|
||||
description: "Переносим сервисы между namespace в Kubernetes: экспорт манифестов, миграция NFS данных, новый SSL за 32 секунды. Реальный пример с Gitea."
|
||||
tags: ["kubernetes", "k3s", "gitea", "devops", "homelab", "namespace"]
|
||||
categories: ["Kubernetes", "DevOps практики"]
|
||||
categories: ["Мои проекты"]
|
||||
series: ["Блог на Hugo в K3s"]
|
||||
series_order: 6
|
||||
---
|
||||
|
||||
@@ -0,0 +1,838 @@
|
||||
---
|
||||
title: "Debian 12: базовая настройка и hardening для production"
|
||||
date: 2026-05-22
|
||||
draft: false
|
||||
description: "Пошаговая настройка чистого Debian 12 (Bookworm) для production: SSH, firewall, fail2ban, swap, sysctl, автообновления, логи, бэкап конфигов."
|
||||
summary: "Пошаговая настройка чистого Debian 12 (Bookworm) для production: SSH, firewall, fail2ban, swap, sysctl, автообновления, логи, бэкап конфигов."
|
||||
tags: ["debian-12", "security", "hardening", "ssh", "ufw", "fail2ban", "sysctl", "production", "linux"]
|
||||
categories: ["Безопасность"]
|
||||
series: ["Linux Hardening"]
|
||||
series_order: 1
|
||||
---
|
||||
|
||||
# Debian 12: базовая настройка и hardening для production
|
||||
|
||||
Поставил чистый Debian 12 и думаешь сразу накатывать приложения? Не торопись. Час на базовую настройку сейчас - это недели сэкономленного времени на разгребание последствий взлома или падения.
|
||||
|
||||
Статья покрывает всё что нужно для production-сервера без контейнеров. Пройдёшь по шагам и получишь сервер с закрытым SSH, firewall, защитой от брутфорса, ограничением ресурсов ядра и автоматическими обновлениями безопасности. Docker - в следующей статье серии.
|
||||
|
||||
## Исходные данные
|
||||
|
||||
- Чистый Debian 12 (Bookworm)
|
||||
- Root доступ по SSH
|
||||
- Статический IP
|
||||
|
||||
Проверь версию:
|
||||
|
||||
```bash
|
||||
lsb_release -a
|
||||
```
|
||||
|
||||
Ожидаемый вывод:
|
||||
```
|
||||
Distributor ID: Debian
|
||||
Description: Debian GNU/Linux 12 (bookworm)
|
||||
Release: 12
|
||||
Codename: bookworm
|
||||
```
|
||||
|
||||
Все команды выполняются от `root` если не указано иное.
|
||||
|
||||
## Обновление системы
|
||||
|
||||
```bash
|
||||
apt update
|
||||
apt upgrade -y
|
||||
apt dist-upgrade -y
|
||||
apt autoremove -y
|
||||
```
|
||||
|
||||
Проверь нужна ли перезагрузка:
|
||||
|
||||
```bash
|
||||
[ -f /var/run/reboot-required ] && echo "Reboot needed" || echo "No reboot needed"
|
||||
```
|
||||
|
||||
Если нужна - перезагрузись:
|
||||
|
||||
```bash
|
||||
reboot
|
||||
```
|
||||
|
||||
## Базовые утилиты
|
||||
|
||||
```bash
|
||||
apt install -y \
|
||||
sudo curl wget git vim htop iotop iftop \
|
||||
net-tools dnsutils tcpdump screen tmux rsync \
|
||||
util-linux unzip ca-certificates gnupg2 lsb-release \
|
||||
smartmontools
|
||||
```
|
||||
|
||||
## Настройка системы
|
||||
|
||||
### Hostname
|
||||
|
||||
```bash
|
||||
hostnamectl set-hostname srv01.example.com
|
||||
```
|
||||
|
||||
Добавь в `/etc/hosts`:
|
||||
|
||||
```bash
|
||||
nano /etc/hosts
|
||||
```
|
||||
|
||||
```
|
||||
127.0.0.1 localhost
|
||||
203.0.113.10 srv01.example.com srv01
|
||||
|
||||
::1 localhost ip6-localhost ip6-loopback
|
||||
ff02::1 ip6-allnodes
|
||||
ff02::2 ip6-allrouters
|
||||
```
|
||||
|
||||
Замени `203.0.113.10` на реальный IP сервера, `srv01.example.com` - на своё имя.
|
||||
|
||||
Проверь FQDN:
|
||||
|
||||
```bash
|
||||
hostname -f
|
||||
# srv01.example.com
|
||||
```
|
||||
|
||||
### Timezone
|
||||
|
||||
```bash
|
||||
timedatectl set-timezone Europe/Moscow
|
||||
```
|
||||
|
||||
Проверь:
|
||||
|
||||
```bash
|
||||
timedatectl
|
||||
```
|
||||
|
||||
### Локаль
|
||||
|
||||
```bash
|
||||
apt install -y locales
|
||||
dpkg-reconfigure locales
|
||||
```
|
||||
|
||||
Выбери `en_US.UTF-8` как основную. Добавь `ru_RU.UTF-8` если нужна кириллица в системных сообщениях.
|
||||
|
||||
Проверь:
|
||||
|
||||
```bash
|
||||
locale
|
||||
# LANG=en_US.UTF-8
|
||||
```
|
||||
|
||||
## Пользователь и SSH
|
||||
|
||||
### Создание пользователя
|
||||
|
||||
Root сессия - одна ошибка без возможности откатиться. `sudo` даёт контроль и лог действий. Создай непривилегированного пользователя:
|
||||
|
||||
```bash
|
||||
adduser admin
|
||||
```
|
||||
|
||||
Добавь в sudo:
|
||||
|
||||
```bash
|
||||
usermod -aG sudo admin
|
||||
```
|
||||
|
||||
Проверь:
|
||||
|
||||
```bash
|
||||
groups admin
|
||||
# admin : admin sudo
|
||||
```
|
||||
|
||||
### SSH ключи
|
||||
|
||||
На своей рабочей машине сгенерируй ключ:
|
||||
|
||||
```bash
|
||||
# Linux/macOS
|
||||
ssh-keygen -t ed25519 -C "admin@srv01"
|
||||
```
|
||||
|
||||
```powershell
|
||||
# Windows
|
||||
ssh-keygen -t ed25519
|
||||
```
|
||||
|
||||
Скопируй публичный ключ на сервер:
|
||||
|
||||
```bash
|
||||
# Linux/macOS
|
||||
ssh-copy-id admin@203.0.113.10
|
||||
```
|
||||
|
||||
```powershell
|
||||
# Windows
|
||||
type $env:userprofile\.ssh\id_ed25519.pub | ssh admin@203.0.113.10 "mkdir -m 700 -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
|
||||
```
|
||||
|
||||
Проверь вход по ключу - не должен спрашивать пароль:
|
||||
|
||||
```bash
|
||||
ssh admin@203.0.113.10
|
||||
```
|
||||
|
||||
### SSH hardening
|
||||
|
||||
Залогинься под `admin` и открой конфиг:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/ssh/sshd_config
|
||||
```
|
||||
|
||||
Замени содержимое (или найди и измени каждый параметр):
|
||||
|
||||
```
|
||||
# Нестандартный порт - отсекает 90% ботов
|
||||
Port 2222
|
||||
|
||||
# Только ключи
|
||||
PubkeyAuthentication yes
|
||||
AuthorizedKeysFile .ssh/authorized_keys
|
||||
|
||||
# Запретить root логин
|
||||
PermitRootLogin no
|
||||
|
||||
# Только ключи, без паролей
|
||||
PasswordAuthentication no
|
||||
PermitEmptyPasswords no
|
||||
KbdInteractiveAuthentication no
|
||||
|
||||
# Ограничения сессий
|
||||
MaxAuthTries 3
|
||||
MaxSessions 3
|
||||
LoginGraceTime 30
|
||||
|
||||
# Таймауты - отключать неактивные сессии
|
||||
ClientAliveInterval 300
|
||||
ClientAliveCountMax 2
|
||||
|
||||
# Отключить лишнее
|
||||
X11Forwarding no
|
||||
PrintMotd no
|
||||
UsePAM yes
|
||||
AcceptEnv LANG LC_*
|
||||
|
||||
# Только IPv4 (убери если используешь IPv6)
|
||||
AddressFamily inet
|
||||
```
|
||||
|
||||
**КРИТИЧНО:** Перед перезапуском SSH открой вторую сессию и не закрывай её - страховка если что-то пойдёт не так.
|
||||
|
||||
Проверь конфиг на ошибки:
|
||||
|
||||
```bash
|
||||
sudo sshd -t
|
||||
```
|
||||
|
||||
Нет вывода - нет ошибок. Перезапусти:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart sshd
|
||||
```
|
||||
|
||||
В новом терминале проверь подключение на новом порту:
|
||||
|
||||
```bash
|
||||
ssh -p 2222 admin@203.0.113.10
|
||||
```
|
||||
|
||||
Если работает - старые сессии можно закрывать.
|
||||
|
||||
### SSH banner (опционально)
|
||||
|
||||
Создай предупреждение при входе:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/ssh/banner
|
||||
```
|
||||
|
||||
```
|
||||
###############################################################################
|
||||
# Доступ только для авторизованных пользователей. #
|
||||
# Все действия логируются. #
|
||||
# Несанкционированный доступ преследуется по закону. #
|
||||
###############################################################################
|
||||
```
|
||||
|
||||
Добавь в `sshd_config`:
|
||||
|
||||
```
|
||||
Banner /etc/ssh/banner
|
||||
```
|
||||
|
||||
Перезапусти SSH:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart sshd
|
||||
```
|
||||
|
||||
## Firewall (UFW)
|
||||
|
||||
**Важно:** сначала настраиваем правила, потом включаем. Иначе заблокируешь себя.
|
||||
|
||||
```bash
|
||||
sudo apt install -y ufw
|
||||
```
|
||||
|
||||
Политика по умолчанию:
|
||||
|
||||
```bash
|
||||
# Блокировать все входящие
|
||||
sudo ufw default deny incoming
|
||||
|
||||
# Разрешить все исходящие
|
||||
sudo ufw default allow outgoing
|
||||
```
|
||||
|
||||
Разрешаем SSH на новом порту:
|
||||
|
||||
```bash
|
||||
# Замени 2222 на свой порт если менял
|
||||
sudo ufw allow 2222/tcp comment 'SSH'
|
||||
```
|
||||
|
||||
Включаем firewall:
|
||||
|
||||
```bash
|
||||
sudo ufw enable
|
||||
```
|
||||
|
||||
Проверь статус:
|
||||
|
||||
```bash
|
||||
sudo ufw status verbose
|
||||
```
|
||||
|
||||
Ожидаемый вывод:
|
||||
```
|
||||
Status: active
|
||||
Logging: on (low)
|
||||
Default: deny (incoming), allow (outgoing), disabled (routed)
|
||||
New profiles: skip
|
||||
|
||||
To Action From
|
||||
-- ------ ----
|
||||
2222/tcp ALLOW IN Anywhere # SSH
|
||||
2222/tcp (v6) ALLOW IN Anywhere (v6) # SSH
|
||||
```
|
||||
|
||||
### Rate limiting на SSH
|
||||
|
||||
```bash
|
||||
# Блокировать IP после 6 подключений за 30 секунд
|
||||
sudo ufw limit 2222/tcp comment 'SSH rate limit'
|
||||
```
|
||||
|
||||
### Открытие дополнительных портов
|
||||
|
||||
Когда будешь ставить сервисы - открывай только нужные порты:
|
||||
|
||||
```bash
|
||||
# HTTP/HTTPS
|
||||
sudo ufw allow 80/tcp comment 'HTTP'
|
||||
sudo ufw allow 443/tcp comment 'HTTPS'
|
||||
|
||||
# PostgreSQL только с конкретного IP
|
||||
sudo ufw allow from 192.168.1.10 to any port 5432 comment 'PostgreSQL'
|
||||
```
|
||||
|
||||
Проверь что слушает на хосте:
|
||||
|
||||
```bash
|
||||
ss -tulnp
|
||||
```
|
||||
|
||||
Всё что слушает - должно быть либо закрыто firewall, либо намеренно открыто.
|
||||
|
||||
## Fail2ban
|
||||
|
||||
```bash
|
||||
sudo apt install -y fail2ban
|
||||
```
|
||||
|
||||
Создай локальный конфиг - не редактируй `jail.conf`, он перезаписывается при обновлении:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/fail2ban/jail.local
|
||||
```
|
||||
|
||||
```ini
|
||||
[DEFAULT]
|
||||
# Бан на 1 час
|
||||
bantime = 3600
|
||||
# За 10 минут
|
||||
findtime = 600
|
||||
# После 3 неудачных попыток
|
||||
maxretry = 3
|
||||
# Не банить свои IP
|
||||
ignoreip = 127.0.0.1/8 ::1
|
||||
# Явно указываем backend - важно для совместимости в серии Ubuntu/Rocky
|
||||
backend = systemd
|
||||
|
||||
[sshd]
|
||||
enabled = true
|
||||
port = 2222
|
||||
filter = sshd
|
||||
maxretry = 3
|
||||
bantime = 3600
|
||||
```
|
||||
|
||||
Замени `2222` на свой SSH порт.
|
||||
|
||||
Запусти:
|
||||
|
||||
```bash
|
||||
sudo systemctl enable fail2ban
|
||||
sudo systemctl start fail2ban
|
||||
```
|
||||
|
||||
Проверь статус:
|
||||
|
||||
```bash
|
||||
sudo fail2ban-client status
|
||||
```
|
||||
|
||||
```
|
||||
Status
|
||||
|- Number of jail: 1
|
||||
`- Jail list: sshd
|
||||
```
|
||||
|
||||
Проверь статус конкретного jail:
|
||||
|
||||
```bash
|
||||
sudo fail2ban-client status sshd
|
||||
```
|
||||
|
||||
## Swap
|
||||
|
||||
Swap в виде файла гибче раздела: можно увеличить или уменьшить без переразбивки диска.
|
||||
|
||||
### Создание swap файла
|
||||
|
||||
Проверь текущий swap:
|
||||
|
||||
```bash
|
||||
free -h
|
||||
sudo swapon --show
|
||||
```
|
||||
|
||||
Создай файл (пример - 2GB):
|
||||
|
||||
```bash
|
||||
# Создать файл нужного размера
|
||||
sudo fallocate -l 2G /swapfile
|
||||
```
|
||||
|
||||
Если `fallocate` не поддерживается файловой системой:
|
||||
|
||||
```bash
|
||||
sudo dd if=/dev/zero of=/swapfile bs=1M count=2048 status=progress
|
||||
```
|
||||
|
||||
Установи права - swap файл не должен читаться другими пользователями:
|
||||
|
||||
```bash
|
||||
sudo chmod 600 /swapfile
|
||||
```
|
||||
|
||||
Инициализируй и активируй:
|
||||
|
||||
```bash
|
||||
# Разметить как swap
|
||||
sudo mkswap /swapfile
|
||||
|
||||
# Включить
|
||||
sudo swapon /swapfile
|
||||
```
|
||||
|
||||
Проверь:
|
||||
|
||||
```bash
|
||||
free -h
|
||||
sudo swapon --show
|
||||
```
|
||||
|
||||
Добавь в `/etc/fstab` для автомонтирования после перезагрузки:
|
||||
|
||||
```bash
|
||||
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
|
||||
```
|
||||
|
||||
### Настройка swappiness
|
||||
|
||||
`swappiness` определяет агрессивность использования swap. Для production снижаем - swap только когда RAM заполнена на 90%+:
|
||||
|
||||
```bash
|
||||
# Проверить текущее значение (по умолчанию 60)
|
||||
cat /proc/sys/vm/swappiness
|
||||
```
|
||||
|
||||
Установи постоянно:
|
||||
|
||||
```bash
|
||||
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.d/99-swap.conf
|
||||
sudo sysctl -p /etc/sysctl.d/99-swap.conf
|
||||
```
|
||||
|
||||
### Изменение размера swap (когда потребуется)
|
||||
|
||||
```bash
|
||||
# Отключить текущий swap
|
||||
sudo swapoff /swapfile
|
||||
|
||||
# Изменить размер (пример - увеличить до 4GB)
|
||||
sudo fallocate -l 4G /swapfile
|
||||
|
||||
# Переинициализировать
|
||||
sudo mkswap /swapfile
|
||||
|
||||
# Включить
|
||||
sudo swapon /swapfile
|
||||
```
|
||||
|
||||
Проверь результат:
|
||||
|
||||
```bash
|
||||
free -h
|
||||
```
|
||||
|
||||
Запись в `/etc/fstab` менять не нужно - путь тот же.
|
||||
|
||||
### Удаление swap (если потребуется)
|
||||
|
||||
```bash
|
||||
sudo swapoff /swapfile
|
||||
sudo rm /swapfile
|
||||
```
|
||||
|
||||
Удали строку из `/etc/fstab`:
|
||||
|
||||
```bash
|
||||
sudo sed -i '/\/swapfile/d' /etc/fstab
|
||||
```
|
||||
|
||||
## Sysctl - защита ядра
|
||||
|
||||
Создай отдельный файл - чище чем писать в `/etc/sysctl.conf`:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/sysctl.d/99-hardening.conf
|
||||
```
|
||||
|
||||
```
|
||||
# Защита от SYN flood
|
||||
net.ipv4.tcp_syncookies = 1
|
||||
net.ipv4.tcp_max_syn_backlog = 2048
|
||||
net.ipv4.tcp_synack_retries = 2
|
||||
|
||||
# Игнорировать ICMP redirects (защита от MITM)
|
||||
net.ipv4.conf.all.accept_redirects = 0
|
||||
net.ipv4.conf.default.accept_redirects = 0
|
||||
net.ipv6.conf.all.accept_redirects = 0
|
||||
net.ipv6.conf.default.accept_redirects = 0
|
||||
|
||||
# Не отправлять ICMP redirects
|
||||
net.ipv4.conf.all.send_redirects = 0
|
||||
net.ipv4.conf.default.send_redirects = 0
|
||||
|
||||
# Игнорировать source routed packets
|
||||
net.ipv4.conf.all.accept_source_route = 0
|
||||
net.ipv6.conf.all.accept_source_route = 0
|
||||
|
||||
# Защита от IP spoofing
|
||||
net.ipv4.conf.all.rp_filter = 1
|
||||
net.ipv4.conf.default.rp_filter = 1
|
||||
|
||||
# Логировать подозрительные пакеты
|
||||
net.ipv4.conf.all.log_martians = 1
|
||||
|
||||
# Защита от time-wait assassination
|
||||
net.ipv4.tcp_rfc1337 = 1
|
||||
|
||||
# Отключить IPv6 если не используешь
|
||||
# net.ipv6.conf.all.disable_ipv6 = 1
|
||||
# net.ipv6.conf.default.disable_ipv6 = 1
|
||||
```
|
||||
|
||||
Примени:
|
||||
|
||||
```bash
|
||||
sudo sysctl -p /etc/sysctl.d/99-hardening.conf
|
||||
```
|
||||
|
||||
Проверь что применилось:
|
||||
|
||||
```bash
|
||||
sudo sysctl net.ipv4.tcp_syncookies
|
||||
# net.ipv4.tcp_syncookies = 1
|
||||
```
|
||||
|
||||
## Автообновления безопасности
|
||||
|
||||
```bash
|
||||
sudo apt install -y unattended-upgrades apt-listchanges
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo nano /etc/apt/apt.conf.d/50unattended-upgrades
|
||||
```
|
||||
|
||||
Раскомментируй/измени:
|
||||
|
||||
```
|
||||
Unattended-Upgrade::Mail "root";
|
||||
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
|
||||
Unattended-Upgrade::Remove-Unused-Dependencies "true";
|
||||
Unattended-Upgrade::Automatic-Reboot "false";
|
||||
```
|
||||
|
||||
`Automatic-Reboot "false"` - важно для production. Перезагрузку после обновления ядра делать вручную в удобное время.
|
||||
|
||||
Включи:
|
||||
|
||||
```bash
|
||||
sudo dpkg-reconfigure -plow unattended-upgrades
|
||||
```
|
||||
|
||||
Выбери `Yes`.
|
||||
|
||||
Настрой расписание:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/apt/apt.conf.d/10periodic
|
||||
```
|
||||
|
||||
```
|
||||
APT::Periodic::Update-Package-Lists "1";
|
||||
APT::Periodic::Download-Upgradeable-Packages "1";
|
||||
APT::Periodic::AutocleanInterval "7";
|
||||
APT::Periodic::Unattended-Upgrade "1";
|
||||
```
|
||||
|
||||
Проверь что сервис работает:
|
||||
|
||||
```bash
|
||||
sudo systemctl status unattended-upgrades
|
||||
```
|
||||
|
||||
## Отключение ненужных сервисов (при наличии)
|
||||
|
||||
Посмотри что запущено:
|
||||
|
||||
```bash
|
||||
systemctl list-units --type=service --state=running
|
||||
```
|
||||
|
||||
Стандартные кандидаты на отключение - проверь что не используешь перед отключением:
|
||||
|
||||
```bash
|
||||
# Bluetooth - на сервере не нужен
|
||||
sudo systemctl disable --now bluetooth.service
|
||||
|
||||
# avahi-daemon - mDNS/DNS-SD, нужен редко
|
||||
sudo systemctl disable --now avahi-daemon.service
|
||||
|
||||
# ModemManager - управление модемами
|
||||
sudo systemctl disable --now ModemManager.service
|
||||
```
|
||||
|
||||
Проверь что слушает на сетевых портах:
|
||||
|
||||
```bash
|
||||
ss -tulnp
|
||||
```
|
||||
|
||||
Всё незнакомое - выясни что это и реши нужно ли.
|
||||
|
||||
## Логи и ротация
|
||||
|
||||
### Ограничение journald
|
||||
|
||||
```bash
|
||||
sudo nano /etc/systemd/journald.conf
|
||||
```
|
||||
|
||||
Раскомментируй/измени:
|
||||
|
||||
```
|
||||
SystemMaxUse=500M
|
||||
SystemMaxFileSize=100M
|
||||
MaxRetentionSec=1month
|
||||
```
|
||||
|
||||
Перезапусти:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart systemd-journald
|
||||
```
|
||||
|
||||
Проверь размер:
|
||||
|
||||
```bash
|
||||
sudo journalctl --disk-usage
|
||||
```
|
||||
|
||||
На чистом Debian 12 rsyslog не установлен - логи пишутся только в journald. Настройки выше достаточно.
|
||||
Если устанавливаешь rsyslog отдельно - создай /etc/logrotate.d/rsyslog с правилами ротации вручную.
|
||||
|
||||
## Мониторинг диска
|
||||
|
||||
На VPS имя устройства зависит от гипервизора - не всегда `/dev/sda`. Определи актуальное:
|
||||
|
||||
```bash
|
||||
# Найти дисковые устройства
|
||||
lsblk -d -o NAME,TYPE | grep disk
|
||||
# NAME TYPE
|
||||
# vda disk
|
||||
#
|
||||
# Результат зависит от гипервизора и провайдера:
|
||||
# /dev/sda - KVM у некоторых провайдеров, Xen
|
||||
# /dev/vda - KVM (наиболее распространённый вариант)
|
||||
# /dev/xvda - Xen
|
||||
```
|
||||
|
||||
Проверь состояние:
|
||||
|
||||
```bash
|
||||
# Замени vda на своё устройство
|
||||
sudo smartctl -H /dev/vda
|
||||
```
|
||||
|
||||
Ожидаемый вывод:
|
||||
```
|
||||
# ATA диски:
|
||||
# SMART overall-health self-assessment test result: PASSED
|
||||
#
|
||||
# SCSI/NVMe диски (типично для VPS):
|
||||
# SMART Health Status: OK
|
||||
#
|
||||
# Оба варианта означают одно: диск здоров
|
||||
```
|
||||
|
||||
Если `FAILED` - диск умирает. Срочно бэкап и замена.
|
||||
|
||||
Детали SMART:
|
||||
|
||||
```bash
|
||||
sudo smartctl -a /dev/vda
|
||||
```
|
||||
|
||||
Обращай внимание на `Reallocated_Sector_Ct`, `Pending_Sector_Count`, `Offline_Uncorrectable` - если ненулевые, диск проблемный.
|
||||
|
||||
Примечание:
|
||||
На виртуальных дисках (VMware, KVM) SMART может быть недоступен: `SMART support is: Unavailable - device lacks SMART capability.`
|
||||
В этом случае smartctl полезен только для физических серверов.
|
||||
На VPS ориентируйся на мониторинг от провайдера.
|
||||
|
||||
## Бэкап конфигов
|
||||
|
||||
Конфиги на диске - не бэкап. Скрипт архивирует `/etc/` и SSH ключи.
|
||||
|
||||
```bash
|
||||
nano ~/backup-configs.sh
|
||||
```
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
BACKUP_DIR="$HOME/backups"
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_FILE="$BACKUP_DIR/config_backup_$DATE.tar.gz"
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# Сохранить список установленных пакетов
|
||||
dpkg --get-selections > "$HOME/installed-packages.txt"
|
||||
|
||||
# Архивировать конфиги и SSH ключи
|
||||
sudo tar -czf "$BACKUP_FILE" \
|
||||
/etc/ \
|
||||
/root/.ssh/ \
|
||||
/home/*/.ssh/ \
|
||||
"$HOME/installed-packages.txt" \
|
||||
2>/dev/null
|
||||
|
||||
echo "Backup: $BACKUP_FILE"
|
||||
ls -lh "$BACKUP_FILE"
|
||||
|
||||
# Удалить бэкапы старше 30 дней
|
||||
find "$BACKUP_DIR" -name "config_backup_*.tar.gz" -mtime +30 -delete
|
||||
```
|
||||
|
||||
Права и первый запуск:
|
||||
|
||||
```bash
|
||||
chmod +x ~/backup-configs.sh
|
||||
~/backup-configs.sh
|
||||
```
|
||||
|
||||
Ожидаемый вывод:
|
||||
```
|
||||
Backup: /home/admin/backups/config_backup_20260522_030000.tar.gz
|
||||
-rw-r--r-- 1 root root 1.2M May 22 03:00 /home/admin/backups/config_backup_20260522_030000.tar.gz
|
||||
```
|
||||
|
||||
Автоматизация через cron (каждое воскресенье в 3:00):
|
||||
|
||||
```bash
|
||||
crontab -e
|
||||
```
|
||||
|
||||
```
|
||||
0 3 * * 0 /home/admin/backup-configs.sh >> /var/log/backup-configs.log 2>&1
|
||||
```
|
||||
|
||||
### Восстановление
|
||||
|
||||
Восстановить конфиги:
|
||||
|
||||
```bash
|
||||
sudo tar -xzf ~/backups/config_backup_ДАТА.tar.gz -C /
|
||||
```
|
||||
|
||||
Восстановить список пакетов:
|
||||
|
||||
```bash
|
||||
sudo dpkg --set-selections < ~/installed-packages.txt
|
||||
sudo apt-get dselect-upgrade
|
||||
```
|
||||
|
||||
## Финальная проверка
|
||||
```bash
|
||||
# Открытые порты
|
||||
ss -tulnp
|
||||
|
||||
# Статус firewall
|
||||
sudo ufw status verbose
|
||||
|
||||
# Статус fail2ban
|
||||
sudo fail2ban-client status sshd
|
||||
|
||||
# Состояние диска
|
||||
sudo smartctl -H /dev/vda
|
||||
|
||||
# Swap
|
||||
free -h
|
||||
```
|
||||
|
||||
## Что дальше
|
||||
|
||||
Сервер готов к установке приложений. База одинакова для любой нагрузки - веб, база данных, почта.
|
||||
|
||||
Если следующий шаг - контейнеры, читай продолжение серии: [Debian 12: настройка Docker-хоста для production](/posts/debian-12-docker-hardening/).
|
||||
|
||||
**Стек этой статьи:** Debian 12 (Bookworm) · UFW · Fail2ban · OpenSSH · unattended-upgrades · systemd · smartmontools
|
||||
@@ -0,0 +1,870 @@
|
||||
---
|
||||
title: "Debian 12: настройка Docker-хоста для production"
|
||||
date: 2026-05-22
|
||||
draft: false
|
||||
description: "Пошаговая настройка Debian 12 как Docker-хоста для production: установка Docker, настройка daemon, UFW + Docker интеграция, изоляция контейнеров, docker.sock, ресурсные ограничения."
|
||||
summary: "Пошаговая настройка Debian 12 как Docker-хоста для production: установка Docker, настройка daemon, UFW + Docker интеграция, изоляция контейнеров, docker.sock, ресурсные ограничения."
|
||||
tags: ["debian-12", "docker", "security", "hardening", "ufw", "production", "containers", "linux"]
|
||||
categories: ["Безопасность"]
|
||||
series: ["Linux Hardening"]
|
||||
series_order: 2
|
||||
---
|
||||
|
||||
# Debian 12: настройка Docker-хоста для production
|
||||
|
||||
Статья предполагает что базовая настройка уже выполнена: SSH на порту 2222, UFW включён, fail2ban настроен, sysctl применён.
|
||||
Если нет - сначала:
|
||||
{{< article link="/posts/debian-12-base-hardening/">}}
|
||||
|
||||
Здесь ставим Docker и закрываем типичные дыры: daemon без избыточных привилегий, UFW который не обходится Docker, изоляция контейнеров по сетям. Плюс разбираем docker.sock - самое опасное место на Docker-хосте.
|
||||
|
||||
## Исходные данные
|
||||
|
||||
- Debian 12 с выполненным базовым hardening
|
||||
- Минимум 2GB RAM, 20GB диска
|
||||
|
||||
Проверь версию:
|
||||
|
||||
```bash
|
||||
lsb_release -a
|
||||
```
|
||||
|
||||
```
|
||||
Distributor ID: Debian
|
||||
Description: Debian GNU/Linux 12 (bookworm)
|
||||
Release: 12
|
||||
Codename: bookworm
|
||||
```
|
||||
|
||||
Все команды выполняются с `sudo` если не указано иное.
|
||||
|
||||
## Установка Docker
|
||||
|
||||
### Удаление старых версий
|
||||
|
||||
Если был установлен Docker из репозиториев Debian - удали. Версия там устаревшая:
|
||||
|
||||
```bash
|
||||
sudo apt remove -y docker docker-engine docker.io containerd runc docker-compose docker-doc podman-docker
|
||||
```
|
||||
|
||||
### Установка из официального репозитория
|
||||
|
||||
>Команды установки актуальны на момент написания статьи. Docker меняет процедуру добавления репозитория - перед установкой сверяйся с официальной документацией: docs.docker.com/engine/install/debian
|
||||
|
||||
```bash
|
||||
# Установить зависимости
|
||||
sudo apt update
|
||||
sudo apt install -y ca-certificates curl
|
||||
|
||||
# Создать директорию для ключей
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
|
||||
# Добавить GPG ключ Docker
|
||||
sudo curl -fsSL https://download.docker.com/linux/debian/gpg \
|
||||
-o /etc/apt/keyrings/docker.asc
|
||||
sudo chmod a+r /etc/apt/keyrings/docker.asc
|
||||
|
||||
# Добавить репозиторий
|
||||
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
|
||||
Types: deb
|
||||
URIs: https://download.docker.com/linux/debian
|
||||
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
|
||||
Components: stable
|
||||
Architectures: $(dpkg --print-architecture)
|
||||
Signed-By: /etc/apt/keyrings/docker.asc
|
||||
EOF
|
||||
|
||||
sudo apt update
|
||||
|
||||
# Установить Docker
|
||||
sudo apt install -y docker-ce docker-ce-cli containerd.io \
|
||||
docker-buildx-plugin docker-compose-plugin
|
||||
```
|
||||
|
||||
Проверь версию:
|
||||
|
||||
```bash
|
||||
docker --version
|
||||
```
|
||||
|
||||
```
|
||||
Docker version 29.x.x, build xxxxxxx
|
||||
```
|
||||
|
||||
Проверь что daemon запущен:
|
||||
|
||||
```bash
|
||||
sudo systemctl status docker
|
||||
```
|
||||
|
||||
### Добавление пользователя в группу docker
|
||||
|
||||
```bash
|
||||
sudo usermod -aG docker $USER
|
||||
# Активировать группу без перелогина
|
||||
newgrp docker
|
||||
```
|
||||
|
||||
**Важно:** членство в группе `docker` эквивалентно sudo без пароля - через docker.sock можно получить полный root на хосте. Добавляй только пользователей которым полностью доверяешь. Подробнее - в разделе про docker.sock ниже.
|
||||
|
||||
```bash
|
||||
docker run --rm hello-world
|
||||
```
|
||||
|
||||
Ожидаемый вывод: `Hello from Docker!`
|
||||
|
||||
## docker.sock - главный риск
|
||||
|
||||
Прежде чем настраивать daemon - разберёмся с самым опасным местом.
|
||||
|
||||
`/var/run/docker.sock` - Unix сокет Docker daemon. Любой процесс с доступом к нему имеет **полный root на хосте**. Не "почти root" - именно root. Через один вызов API можно запустить контейнер с bind mount `/:/host` и получить доступ ко всей файловой системе хоста:
|
||||
|
||||
```bash
|
||||
# Пример атаки - одна команда и ты root на хосте (выполнение не требуется)
|
||||
docker run --rm -v /:/host alpine chroot /host
|
||||
```
|
||||
|
||||
Проверь кто имеет доступ к сокету:
|
||||
|
||||
```bash
|
||||
# Права на сокет
|
||||
ls -la /var/run/docker.sock
|
||||
```
|
||||
|
||||
```
|
||||
srw-rw---- 1 root docker 0 May 22 10:00 /var/run/docker.sock
|
||||
```
|
||||
|
||||
Проверь кто в группе `docker`:
|
||||
|
||||
```bash
|
||||
getent group docker
|
||||
```
|
||||
|
||||
Только пользователи которым доверяешь полный root должны быть в этой группе.
|
||||
|
||||
## Настройка daemon.json
|
||||
|
||||
Создай файл:
|
||||
```bash
|
||||
sudo nano /etc/docker/daemon.json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "50m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"live-restore": true,
|
||||
"userland-proxy": false,
|
||||
"no-new-privileges": true,
|
||||
"storage-driver": "overlay2",
|
||||
"default-ulimits": {
|
||||
"nofile": {
|
||||
"Name": "nofile",
|
||||
"Hard": 65536,
|
||||
"Soft": 65536
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Что и почему:
|
||||
|
||||
`log-driver: json-file` + `max-size/max-file` - ограничение логов контейнеров. Без этого `/var/lib/docker/containers/` забивает весь диск за несколько дней на активном сервисе. 50MB × 3 файла = 150MB максимум на контейнер.
|
||||
|
||||
`live-restore: true` - контейнеры продолжают работать пока daemon перезапускается. Без этого `systemctl restart docker` роняет все контейнеры.
|
||||
|
||||
`userland-proxy: false` - iptables вместо userland proxy для проброса портов. Быстрее и меньше процессов.
|
||||
|
||||
`no-new-privileges: true` - контейнеры не могут поднять привилегии через setuid/setgid бинарники.
|
||||
|
||||
`storage-driver: overlay2` - современный и быстрый драйвер. На Debian 12 используется по умолчанию, но лучше зафиксировать явно.
|
||||
|
||||
### Почему нет userns-remap
|
||||
|
||||
`userns-remap` - user namespace remapping. Root в контейнере (UID 0) маппится на непривилегированный UID на хосте (например 100000). Звучит безопасно.
|
||||
|
||||
На практике: ломает все bind mounts - файлы на хосте принадлежат UID 1000, а контейнер видит UID 100000 и получает `Permission denied`. Redis, PostgreSQL, nginx - у каждого свой UID, каждый нужно пересчитывать вручную через `chown -R 100000:100000`. Для production с несколькими сервисами это постоянная головная боль. Включай только если чётко понимаешь все последствия и готов управлять UID маппингом вручную.
|
||||
|
||||
### Почему нет icc:false
|
||||
|
||||
`icc: false` (inter-container communication) - глобальный запрет общения между контейнерами на уровне daemon.
|
||||
|
||||
Проблема: Docker Compose создаёт изолированную bridge сеть для каждого стека. Контейнеры одного стека (`app` + `postgres` + `redis`) общаются через эту сеть. С `icc: false` на уровне daemon они перестают видеть друг друга - стек перестаёт работать. Правильный подход - изоляция через сети Docker Compose. Описано ниже.
|
||||
|
||||
Примени конфиг:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart docker
|
||||
```
|
||||
|
||||
Проверь что daemon запустился с нужными параметрами:
|
||||
|
||||
```bash
|
||||
docker info | grep -E "Storage Driver|Logging Driver|Live Restore"
|
||||
```
|
||||
|
||||
```
|
||||
Storage Driver: overlay2
|
||||
Logging Driver: json-file
|
||||
Live Restore Enabled: true
|
||||
```
|
||||
|
||||
## Sysctl для Docker-хоста
|
||||
|
||||
Docker требует дополнительных параметров ядра помимо базового hardening:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/sysctl.d/99-docker.conf
|
||||
```
|
||||
|
||||
```
|
||||
# IP forwarding - обязательно для работы сети контейнеров
|
||||
# Docker включает это сам при старте, фиксируем явно чтобы не зависеть от порядка сервисов
|
||||
net.ipv4.ip_forward = 1
|
||||
|
||||
# IPv6 forwarding - только если используешь IPv6 в контейнерах
|
||||
# net.ipv6.conf.all.forwarding = 1
|
||||
|
||||
# Лимиты inotify - для контейнеров которые следят за файлами
|
||||
fs.inotify.max_user_instances = 512
|
||||
fs.inotify.max_user_watches = 524288
|
||||
|
||||
# Для Elasticsearch и других Java приложений в контейнерах
|
||||
vm.max_map_count = 262144
|
||||
```
|
||||
|
||||
Примени:
|
||||
|
||||
```bash
|
||||
sudo sysctl -p /etc/sysctl.d/99-docker.conf
|
||||
```
|
||||
|
||||
Проверь forwarding:
|
||||
|
||||
```bash
|
||||
sudo sysctl net.ipv4.ip_forward
|
||||
# net.ipv4.ip_forward = 1
|
||||
```
|
||||
|
||||
## UFW + Docker
|
||||
|
||||
>Этот раздел - справочный. Конкретные настройки применяются при разворачивании каждого сервиса.
|
||||
|
||||
Docker создаёт свои iptables правила и **обходит UFW**. Порт опубликованный через `-p 80:80` становится доступен снаружи даже если UFW его не разрешал. Это не баг - это архитектурное решение Docker.
|
||||
|
||||
Два подхода к решению:
|
||||
|
||||
### Подход 1 - рекомендуемый: порты на localhost
|
||||
|
||||
Публикуй порты только на localhost, снаружи открывай через reverse proxy (nginx, Traefik):
|
||||
|
||||
```bash
|
||||
# Плохо - порт открыт на всех интерфейсах, обходит UFW
|
||||
docker run -p 80:80 nginx
|
||||
|
||||
# Хорошо - порт только на localhost
|
||||
docker run -p 127.0.0.1:80:80 nginx
|
||||
```
|
||||
|
||||
В docker-compose.yml:
|
||||
|
||||
```yaml
|
||||
ports:
|
||||
- "127.0.0.1:8080:80"
|
||||
```
|
||||
|
||||
Reverse proxy слушает на `0.0.0.0:443` и проксирует на `127.0.0.1:8080`. UFW разрешает только 443. Docker UFW не обходит - нечего обходить.
|
||||
|
||||
Для большинства production сетапов достаточно этого подхода - он проще и надёжнее.
|
||||
|
||||
### Подход 2: DOCKER-USER chain
|
||||
|
||||
Docker оставляет пустую цепочку `DOCKER-USER` для пользовательских правил. Правила в ней применяются до правил Docker. Нужен когда по каким-то причинам нельзя использовать reverse proxy:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/ufw/after.rules
|
||||
```
|
||||
|
||||
Добавь в конец файла:
|
||||
|
||||
```
|
||||
# BEGIN UFW AND DOCKER
|
||||
*filter
|
||||
:DOCKER-USER - [0:0]
|
||||
|
||||
# Разрешить трафик из локальных сетей
|
||||
-A DOCKER-USER -s 10.0.0.0/8 -j RETURN
|
||||
-A DOCKER-USER -s 172.16.0.0/12 -j RETURN
|
||||
-A DOCKER-USER -s 192.168.0.0/16 -j RETURN
|
||||
|
||||
# Применить правила UFW к трафику идущему в контейнеры
|
||||
-A DOCKER-USER -j ufw-user-forward
|
||||
|
||||
# Блокировать новые соединения снаружи к частным подсетям
|
||||
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
|
||||
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
|
||||
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
|
||||
|
||||
-A DOCKER-USER -j RETURN
|
||||
COMMIT
|
||||
# END UFW AND DOCKER
|
||||
```
|
||||
|
||||
Перезагрузи UFW:
|
||||
|
||||
```bash
|
||||
sudo ufw reload
|
||||
```
|
||||
|
||||
## Сетевая изоляция контейнеров
|
||||
|
||||
>Этот раздел - справочный. Сети создаются при разворачивании каждого сервиса.
|
||||
|
||||
Вместо глобального `icc: false` - изоляция через сети.
|
||||
|
||||
По умолчанию все контейнеры запущенные без явной сети попадают в `bridge` сеть и видят друг друга. Создавай отдельную сеть для каждого приложения:
|
||||
|
||||
```bash
|
||||
# Создать изолированную сеть для приложения
|
||||
docker network create --driver bridge app_network
|
||||
|
||||
# Контейнеры видят только соседей по той же сети
|
||||
docker run -d --name app --network app_network myapp
|
||||
docker run -d --name db --network app_network postgres
|
||||
```
|
||||
|
||||
Контейнеры из разных сетей не могут общаться - это и есть правильная изоляция без глобального `icc: false`.
|
||||
|
||||
В docker-compose.yml сети создаются автоматически для каждого стека. Явно задавай сети если нужна дополнительная изоляция внутри стека.
|
||||
|
||||
## Безопасность образов
|
||||
|
||||
### Сканирование уязвимостей - Trivy
|
||||
|
||||
```bash
|
||||
# Добавить репозиторий Trivy
|
||||
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | \
|
||||
gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null
|
||||
|
||||
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] \
|
||||
https://aquasecurity.github.io/trivy-repo/deb \
|
||||
$(lsb_release -sc) main" | \
|
||||
sudo tee /etc/apt/sources.list.d/trivy.list > /dev/null
|
||||
|
||||
sudo apt update
|
||||
sudo apt install -y trivy
|
||||
```
|
||||
|
||||
Проверь образ:
|
||||
|
||||
```bash
|
||||
trivy image nginx:alpine
|
||||
```
|
||||
|
||||
Вывод:
|
||||
```bash
|
||||
admin@vps01:~$ trivy image nginx:alpine
|
||||
2026-06-09T12:22:41+03:00 INFO [vulndb] Need to update DB
|
||||
2026-06-09T12:22:41+03:00 INFO [vulndb] Downloading vulnerability DB...
|
||||
2026-06-09T12:22:41+03:00 INFO [vulndb] Downloading artifact... repo="mirror.gcr.io/aquasec/trivy-db:2"
|
||||
95.58 MiB / 95.58 MiB [---------------------------------------------------------------------] 100.00% 763.08 KiB p/s 2m8s
|
||||
2026-06-09T12:24:52+03:00 INFO [vulndb] Artifact successfully downloaded repo="mirror.gcr.io/aquasec/trivy-db:2"
|
||||
2026-06-09T12:24:52+03:00 INFO [vuln] Vulnerability scanning is enabled
|
||||
2026-06-09T12:24:52+03:00 INFO [secret] Secret scanning is enabled
|
||||
2026-06-09T12:24:52+03:00 INFO [secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning
|
||||
2026-06-09T12:24:52+03:00 INFO [secret] Please see https://trivy.dev/docs/v0.71/guide/scanner/secret#recommendation for faster secret detection
|
||||
2026-06-09T12:25:11+03:00 INFO Detected OS family="alpine" version="3.23.4"
|
||||
2026-06-09T12:25:11+03:00 INFO [alpine] Detecting vulnerabilities... os_version="3.23" repository="3.23" pkg_num=71
|
||||
2026-06-09T12:25:11+03:00 INFO Number of language-specific files num=0
|
||||
|
||||
Report Summary
|
||||
|
||||
┌──────────────────────────────┬────────┬─────────────────┬─────────┐
|
||||
│ Target │ Type │ Vulnerabilities │ Secrets │
|
||||
├──────────────────────────────┼────────┼─────────────────┼─────────┤
|
||||
│ nginx:alpine (alpine 3.23.4) │ alpine │ 1 │ - │
|
||||
└──────────────────────────────┴────────┴─────────────────┴─────────┘
|
||||
Legend:
|
||||
- '-': Not scanned
|
||||
- '0': Clean (no security findings detected)
|
||||
|
||||
|
||||
nginx:alpine (alpine 3.23.4)
|
||||
|
||||
Total: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 1, CRITICAL: 0)
|
||||
|
||||
┌─────────┬───────────────┬──────────┬────────┬───────────────────┬───────────────┬─────────────────────────────────────────────────┐
|
||||
│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │
|
||||
├─────────┼───────────────┼──────────┼────────┼───────────────────┼───────────────┼─────────────────────────────────────────────────┤
|
||||
│ libxml2 │ CVE-2026-6732 │ HIGH │ fixed │ 2.13.9-r0 │ 2.13.9-r1 │ libxml2: libxml2: Denial of Service via crafted │
|
||||
│ │ │ │ │ │ │ XSD-validated document │
|
||||
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-6732 │
|
||||
└─────────┴───────────────┴──────────┴────────┴───────────────────┴───────────────┴─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
>Status `fixed` - исправление уже есть, обнови образ
|
||||
>Status `affected` - исправления нет, принимай решение осознанно
|
||||
|
||||
Правило для production: образы с CRITICAL уязвимостями не идут в production. HIGH - анализировать и принимать решение.
|
||||
|
||||
Сканировать перед каждым деплоем:
|
||||
|
||||
```bash
|
||||
# Выйти с ненулевым кодом если есть CRITICAL/HIGH - удобно для CI
|
||||
trivy image --exit-code 1 --severity CRITICAL,HIGH nginx:alpine
|
||||
```
|
||||
|
||||
### Аудит событий Docker
|
||||
|
||||
Auditd фиксирует все обращения к Docker на уровне ядра: запуск контейнеров, изменения конфигурации, доступ к docker.sock. Когда что-то пойдёт не так - это первое место где искать что произошло и когда.
|
||||
|
||||
```bash
|
||||
sudo apt install -y auditd
|
||||
```
|
||||
|
||||
Создай правила:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/audit/rules.d/docker.rules
|
||||
```
|
||||
|
||||
```
|
||||
-w /usr/bin/docker -p rwxa -k docker
|
||||
-w /var/lib/docker -p rwxa -k docker
|
||||
-w /etc/docker -p rwxa -k docker
|
||||
-w /var/run/docker.sock -p rwxa -k docker
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo systemctl enable auditd
|
||||
sudo systemctl restart auditd
|
||||
```
|
||||
|
||||
Просматривай события:
|
||||
|
||||
```bash
|
||||
sudo ausearch -k docker | tail -20
|
||||
```
|
||||
|
||||
### Правила выбора образов
|
||||
|
||||
>Этот раздел - справочный. Применяй при выборе базового образа для своих Dockerfile.
|
||||
|
||||
Alpine-based или distroless - меньше attack surface:
|
||||
|
||||
```dockerfile
|
||||
# 200MB лишних пакетов и уязвимостей
|
||||
FROM ubuntu:24.04
|
||||
|
||||
# 5MB, минимум уязвимостей
|
||||
FROM alpine:3.20
|
||||
|
||||
# Ещё лучше - distroless (нет shell, нет пакетного менеджера)
|
||||
FROM gcr.io/distroless/static-debian12
|
||||
```
|
||||
|
||||
Всегда указывай конкретный тег - никогда `latest`:
|
||||
|
||||
```bash
|
||||
# Плохо - непредсказуемо, сломается при следующем релизе образа
|
||||
docker pull nginx:latest
|
||||
|
||||
# Хорошо - предсказуемо
|
||||
docker pull nginx:1.27-alpine
|
||||
```
|
||||
|
||||
## Изоляция контейнеров
|
||||
|
||||
>Этот раздел - справочный.
|
||||
|
||||
### Capabilities
|
||||
|
||||
Контейнер по умолчанию имеет набор Linux capabilities которые ему не нужны. Убирай всё лишнее:
|
||||
|
||||
```bash
|
||||
# Убрать все capabilities, добавить только нужные
|
||||
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx
|
||||
```
|
||||
|
||||
Минимальный набор для большинства веб-сервисов: `NET_BIND_SERVICE` (порты < 1024). Если сервис не биндит привилегированные порты - вообще ничего добавлять не нужно, только `--cap-drop=ALL`.
|
||||
|
||||
### Read-only filesystem
|
||||
|
||||
```bash
|
||||
# Контейнер не может писать в FS кроме явно указанных точек
|
||||
docker run --read-only --tmpfs /tmp --tmpfs /var/run nginx
|
||||
```
|
||||
|
||||
В docker-compose.yml:
|
||||
|
||||
```yaml
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
- /var/run
|
||||
```
|
||||
|
||||
Не все образы поддерживают read-only без дополнительной настройки - смотри документацию образа.
|
||||
|
||||
### Seccomp
|
||||
|
||||
Docker применяет seccomp профиль по умолчанию - блокирует ~44 опасных syscall. Проверь что профиль применяется:
|
||||
|
||||
```bash
|
||||
docker info | grep seccomp
|
||||
# Security Options: ... seccomp
|
||||
```
|
||||
|
||||
### Не запускай контейнеры с --privileged
|
||||
|
||||
```bash
|
||||
# НИКОГДА в production
|
||||
docker run --privileged nginx
|
||||
```
|
||||
|
||||
`--privileged` даёт контейнеру доступ ко всем устройствам хоста и отключает все механизмы изоляции. Если видишь `--privileged` в чужом docker-compose - это красный флаг.
|
||||
|
||||
## Ресурсные ограничения
|
||||
|
||||
>Этот раздел - справочный.
|
||||
|
||||
Без ограничений один контейнер может съесть всю память или CPU хоста и положить остальные сервисы.
|
||||
|
||||
>OOM (Out Of Memory) killer - механизм ядра Linux: когда процесс превышает лимит памяти, ядро принудительно его завершает. Код завершения контейнера 137 (128 + сигнал 9) означает что сработал OOM killer, а не штатное завершение.
|
||||
|
||||
```bash
|
||||
# Максимум 512MB RAM - если превысит, OOM (Out Of Memory) killer завершит контейнер
|
||||
docker run -m 512m --memory-reservation 256m nginx
|
||||
|
||||
# Максимум 0.5 ядра
|
||||
docker run --cpus="0.5" nginx
|
||||
|
||||
# Защита от fork bomb - контейнер не создает бесконечное количество процессов
|
||||
docker run --pids-limit 200 nginx
|
||||
```
|
||||
|
||||
Если контейнер неожиданно останавливается - первым делом проверяй OOM (Out Of Memory):
|
||||
|
||||
```bash
|
||||
dmesg | grep -i "oom\|killed process"
|
||||
docker inspect CONTAINER --format='{{.State.OOMKilled}}'
|
||||
```
|
||||
|
||||
## Docker Compose с настройками безопасности
|
||||
|
||||
>Этот раздел - справочный.
|
||||
|
||||
Шаблон `docker-compose.yml` для production: сети, лимиты, изоляция:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
image: nginx:1.27-alpine
|
||||
container_name: web
|
||||
restart: unless-stopped
|
||||
|
||||
# Порт только на localhost - reverse proxy снаружи
|
||||
ports:
|
||||
- "127.0.0.1:8080:80"
|
||||
|
||||
# Ресурсные ограничения
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: "0.5"
|
||||
memory: 256M
|
||||
reservations:
|
||||
memory: 128M
|
||||
|
||||
# Изоляция
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
- NET_BIND_SERVICE
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
- /var/cache/nginx
|
||||
- /var/run
|
||||
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
pids_limit: 100
|
||||
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
- frontend
|
||||
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
container_name: db
|
||||
restart: unless-stopped
|
||||
|
||||
environment:
|
||||
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
|
||||
|
||||
volumes:
|
||||
- db_data:/var/lib/postgresql/data
|
||||
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: "1.0"
|
||||
memory: 512M
|
||||
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
- SETUID
|
||||
- SETGID
|
||||
- DAC_OVERRIDE
|
||||
- CHOWN
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
pids_limit: 200
|
||||
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
# Только backend сеть - не доступен снаружи
|
||||
networks:
|
||||
- backend
|
||||
|
||||
app:
|
||||
image: myapp:1.2.3
|
||||
container_name: app
|
||||
restart: unless-stopped
|
||||
|
||||
# Подключена к обеим сетям - мост между web и db
|
||||
networks:
|
||||
- frontend
|
||||
- backend
|
||||
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
cap_drop:
|
||||
- ALL
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
pids_limit: 100
|
||||
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
frontend:
|
||||
driver: bridge
|
||||
backend:
|
||||
driver: bridge
|
||||
internal: true # backend сеть без доступа к интернету
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
|
||||
secrets:
|
||||
db_password:
|
||||
file: ./secrets/db_password.txt
|
||||
```
|
||||
|
||||
`internal: true` для backend сети - контейнеры в ней не имеют доступа к внешним сетям. База данных не должна ходить в интернет.
|
||||
|
||||
## Регулярные задачи
|
||||
|
||||
>Этот раздел - справочный.
|
||||
|
||||
### Проверка контейнеров с docker.sock
|
||||
|
||||
Проверь запущенные контейнеры у которых docker.sock смонтирован:
|
||||
|
||||
```bash
|
||||
# Контейнеры с docker.sock = контейнеры с root-доступом к хосту
|
||||
docker ps --format '{{.Names}}' | xargs -I {} docker inspect {} \
|
||||
--format '{{.Name}}: {{range .Mounts}}{{if eq .Source "/var/run/docker.sock"}}DOCKER_SOCK_MOUNTED{{end}}{{end}}' \
|
||||
2>/dev/null | grep DOCKER_SOCK_MOUNTED
|
||||
```
|
||||
Запускай после добавления каждого нового сервиса. Пустой вывод - норма.
|
||||
|
||||
Никогда не монтируй docker.sock в контейнер без явной необходимости. Если сервис требует docker.sock (Portainer, Watchtower, Traefik с автообнаружением) - осознавай что это сервис с root-доступом к хосту.
|
||||
|
||||
### Очистка
|
||||
|
||||
Docker накапливает мусор: остановленные контейнеры, неиспользуемые образы, dangling volumes:
|
||||
|
||||
```bash
|
||||
# Показать что занимает место
|
||||
docker system df
|
||||
|
||||
# Удалить всё неиспользуемое (остановленные контейнеры, dangling образы, неиспользуемые сети)
|
||||
docker system prune -f
|
||||
|
||||
# Удалить всё включая неиспользуемые образы
|
||||
docker system prune -a -f
|
||||
|
||||
# Удалить неиспользуемые volumes (ОСТОРОЖНО - данные удалятся безвозвратно)
|
||||
docker volume prune -f
|
||||
```
|
||||
|
||||
Автоматизируй через cron:
|
||||
|
||||
```bash
|
||||
sudo crontab -e
|
||||
```
|
||||
|
||||
```
|
||||
# Еженедельная очистка - каждое воскресенье в 4:00
|
||||
0 4 * * 0 docker system prune -f >> /var/log/docker-prune.log 2>&1
|
||||
```
|
||||
|
||||
### Мониторинг ресурсов
|
||||
|
||||
```bash
|
||||
# Статистика всех запущенных контейнеров в реальном времени
|
||||
docker stats
|
||||
|
||||
# Один снимок без потока
|
||||
docker stats --no-stream
|
||||
|
||||
# Конкретный контейнер
|
||||
docker stats web --no-stream
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Контейнер не видит DNS
|
||||
|
||||
**Симптом:** `curl: Could not resolve host: example.com` внутри контейнера.
|
||||
|
||||
**Причина:** DNS не передан контейнеру.
|
||||
|
||||
**Решение:** Добавь DNS в daemon.json:
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": ["8.8.8.8", "8.8.4.4"]
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo systemctl restart docker
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Docker обходит UFW
|
||||
|
||||
**Симптом:** Порт опубликованный через `-p 80:80` доступен снаружи несмотря на UFW deny.
|
||||
|
||||
**Причина:** Docker пишет напрямую в iptables, обходя UFW.
|
||||
|
||||
**Решение:** Использовать `127.0.0.1:80:80` вместо `80:80` - подход 1 из раздела UFW + Docker выше.
|
||||
|
||||
---
|
||||
|
||||
### Permission denied на bind mount
|
||||
|
||||
**Симптом:** Контейнер падает с `Permission denied` при обращении к примонтированной директории.
|
||||
|
||||
**Причина:** UID процесса в контейнере не совпадает с владельцем файлов на хосте.
|
||||
|
||||
**Решение:** Проверь под каким UID работает процесс в контейнере:
|
||||
|
||||
```bash
|
||||
docker run --rm IMAGE id
|
||||
```
|
||||
|
||||
Установи правильного владельца на хосте:
|
||||
|
||||
```bash
|
||||
sudo chown -R UID:GID /path/to/mount
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### OOM killer убивает контейнер
|
||||
|
||||
**Симптом:** Контейнер неожиданно останавливается, `docker ps` показывает `Exited (137)`.
|
||||
|
||||
**Причина:** Превышен memory limit, ядро завершило процесс.
|
||||
|
||||
**Диагностика:**
|
||||
|
||||
```bash
|
||||
# Проверить OOM события
|
||||
dmesg | grep -i "oom\|killed process"
|
||||
|
||||
# Статус контейнера
|
||||
docker inspect CONTAINER --format='{{.State.OOMKilled}}'
|
||||
```
|
||||
|
||||
**Решение:** Увеличить `memory` limit или оптимизировать приложение.
|
||||
|
||||
---
|
||||
|
||||
### Диск забит логами контейнеров
|
||||
|
||||
**Симптом:** `df -h` показывает 100% на `/var/lib/docker`.
|
||||
|
||||
**Диагностика:**
|
||||
|
||||
```bash
|
||||
# Найти самые большие лог файлы
|
||||
sudo find /var/lib/docker/containers/ -name "*.log" -exec du -sh {} \; | sort -rh | head -10
|
||||
```
|
||||
|
||||
**Экстренное решение:**
|
||||
|
||||
```bash
|
||||
# Обрезать лог конкретного контейнера (не удаляет файл)
|
||||
sudo truncate -s 0 /var/lib/docker/containers/CONTAINER_ID/CONTAINER_ID-json.log
|
||||
```
|
||||
|
||||
**Постоянное решение:** `max-size` и `max-file` в daemon.json - уже настроено выше.
|
||||
|
||||
## Финальная проверка
|
||||
```bash
|
||||
# Статус daemon
|
||||
sudo systemctl status docker
|
||||
|
||||
# Конфигурация
|
||||
docker info | grep -E "Storage Driver|Logging Driver|Live Restore|no-new-privileges"
|
||||
|
||||
# Открытые порты
|
||||
ss -tulnp | grep docker
|
||||
|
||||
# Размер данных Docker
|
||||
docker system df
|
||||
|
||||
# Контейнеры с docker.sock
|
||||
docker ps -q | xargs docker inspect --format '{{.Name}}: {{range .Mounts}}{{if eq .Source "/var/run/docker.sock"}}SOCK{{end}}{{end}}' 2>/dev/null | grep SOCK
|
||||
```
|
||||
|
||||
## Что дальше
|
||||
|
||||
Docker-хост готов к production нагрузке. Следующий шаг - разворачиваем сервисы поверх этой базы.
|
||||
|
||||
- Peertube на VPS - следующая статья серии: собственный видеохостинг на этом Docker-хосте
|
||||
- Reverse proxy - Traefik или nginx перед контейнерами
|
||||
- Мониторинг - Prometheus + cAdvisor + Grafana
|
||||
- Централизованные логи - Loki + Promtail
|
||||
|
||||
**Стек этой статьи:** Debian 12 (Bookworm) · Docker CE · UFW · auditd · Trivy · docker-compose
|
||||
@@ -0,0 +1,454 @@
|
||||
---
|
||||
title: "Диск забит на 100%: как Loki съел 24 ГБ логов, а df -i почти ввёл в заблуждение"
|
||||
date: 2026-06-24
|
||||
draft: false
|
||||
description: "Расследование заполненного диска на Debian 12: от алерта Zabbix через df, du и docker system df до найденного виновника - логов Loki без лимита ротации. Чек-лист подозреваемых и настройка лимита логов Docker."
|
||||
summary: "Расследование заполненного диска на Debian 12: от алерта Zabbix через df, du и docker system df до найденного виновника - логов Loki без лимита ротации."
|
||||
tags: ["debian-12", "docker", "zabbix", "мониторинг", "диагностика", "логи", "xfs", "linux"]
|
||||
categories: ["Файловые системы и хранилище"]
|
||||
series: ["Linux: эксплуатация и обслуживание"]
|
||||
series_order: 1
|
||||
---
|
||||
|
||||
# Диск забит на 100%: как Loki съел 24 ГБ логов, а df -i почти ввёл в заблуждение
|
||||
|
||||
Zabbix присылает алерт: на проде закончилось место в корне файловой системы. Сервер - Debian 12, что на нём крутится и почему диск кончился - неизвестно, в тикете только хостнейм и факт.
|
||||
|
||||
Эта статья - не теория про docker logs и не абстрактный гайд по очистке диска. Это реальное расследование: от первого `df -h` до настроенного лимита логов, со всеми тупиками по дороге, включая два моих собственных, а не системы. Один из них почти увёл диагностику в сторону несуществующей проблемы с инодами.
|
||||
|
||||
Если у вас диск забился по другой причине - в конце статьи отдельный чек-лист по всем проверенным подозреваемым. Он работает как самостоятельный справочник, независимо от того, что нашлось в этом конкретном случае.
|
||||
|
||||
## Старт: два предела сразу
|
||||
|
||||
"Место кончилось" в Linux бывает по двум разным причинам - не хватило байтов или не хватило инодов. Алерт Zabbix не уточняет, какая именно, так что смотрим сразу на обе.
|
||||
|
||||
Занятость по байтам на всех точках монтирования:
|
||||
|
||||
```bash
|
||||
df -h
|
||||
```
|
||||
|
||||
```
|
||||
Filesystem Size Used Avail Use% Mounted on
|
||||
/dev/mapper/vg01-root 29G 29G 16K 100% /
|
||||
/dev/sda2 944M 147M 732M 17% /boot
|
||||
/dev/mapper/vg02-opt 500G 22G 478G 5% /opt
|
||||
```
|
||||
|
||||
Корень (`vg01-root`) забит под потолок - 29G из 29G, 16K свободно. `/opt` - отдельный том на 500G, занят на 5%, чист. Разделение на тома здесь сыграло на руку: не разведи их администратор раньше, искать причину было бы сложнее.
|
||||
|
||||
Та же проверка по инодам - те же 29G можно забить и десятком огромных файлов, и миллионом мелких, и метод диагностики для этих случаев разный:
|
||||
|
||||
```bash
|
||||
df -i
|
||||
```
|
||||
|
||||
```
|
||||
Filesystem Inodes IUsed IFree IUse% Mounted on
|
||||
/dev/mapper/vg01-root 67424 66740 684 99% /
|
||||
/dev/mapper/vg02-opt 262141952 486209 261655743 1% /opt
|
||||
```
|
||||
|
||||
Деталь, которая зацепила взгляд: всего 67424 инода на 29-гигабайтном томе - по привычке принял это за фиксированное значение, заданное при создании файловой системы, как это работает в ext4. При 99% занятости с таким скромным лимitom легко вылететь по инодам даже без аномального количества файлов. Отложил мысль на потом - и, как выяснится в финале статьи, отложил с неверной гипотезой в основе.
|
||||
|
||||
## Первая ошибка: du без -x
|
||||
|
||||
Чтобы понять, что внутри корня весит больше всего, логично пройтись `du` по верхним каталогам:
|
||||
|
||||
```bash
|
||||
du -h --max-depth=1 / 2>/dev/null | sort -rh
|
||||
```
|
||||
|
||||
```
|
||||
23G /
|
||||
19G /opt
|
||||
2.2G /usr
|
||||
1.1G /home
|
||||
765M /var
|
||||
147M /boot
|
||||
```
|
||||
|
||||
Результат бессмысленный, и вот почему: без флага `-x` `du` пересекает точки монтирования. `/opt` и `/boot` - отдельные файловые системы, но `du` зашёл туда и насчитал их объём как часть `/`. Корень - это `vg01-root`, а в выводе сидят чужие 19G и 147M с других дисков.
|
||||
|
||||
Повторяем с `-x`, чтобы `du` не покидал текущую файловую систему:
|
||||
|
||||
```bash
|
||||
du -hx --max-depth=1 / 2>/dev/null | sort -rh
|
||||
```
|
||||
|
||||
```
|
||||
4.0G /
|
||||
2.2G /usr
|
||||
1.1G /home
|
||||
765M /var
|
||||
```
|
||||
|
||||
Сумма едва дотягивает до 4G, а `df -h` показывает 29G использовано. Разрыв в 25G - и тут я слишком быстро потянулся к экзотической гипотезе: удалённый, но открытый файл, который процесс держит дескриптором, а в дереве каталогов его уже нет.
|
||||
|
||||
## Слепая зона: du без root
|
||||
|
||||
Гипотеза была преждевременной. Команда выполнялась без sudo, а `2>/dev/null` заглушил не только мусор, а заодно все "Permission denied". Если `du` не смог зайти в каталог с ограниченными правами, он молча посчитал его как 0 байт - и 4.0G превращаются в недостоверное число, а не в доказательство призрачного файла.
|
||||
|
||||
Сначала проверяем явно, что именно `du` не смог прочитать - без скрытия ошибок:
|
||||
|
||||
```bash
|
||||
sudo du -hx --max-depth=1 / 2>&1 1>/dev/null | grep -i denied
|
||||
```
|
||||
|
||||
Вывод пустой - но это ничего не доказывает: команда уже шла через sudo, то есть не воспроизводила условия первого, проблемного запуска. Сама проверка получилась нерелевантной собственной ошибке, которую должна была вскрыть.
|
||||
|
||||
Пересчитываем с правами root, без слепых зон:
|
||||
|
||||
```bash
|
||||
sudo du -hx --max-depth=1 / 2>/dev/null | sort -rh
|
||||
```
|
||||
|
||||
```
|
||||
29G /
|
||||
26G /var
|
||||
2.2G /usr
|
||||
1.1G /home
|
||||
```
|
||||
|
||||
26G + 2.2G + 1.1G с мелочью почти точно сходится с 29G из `df -h`. Гипотеза про удалённый-но-открытый файл не подтвердилась - фиксирую как часть истории, а не прячу. Разрыв был не призраком, а банальной слепой зоной из-за прав доступа в моей же команде.
|
||||
|
||||
**Золотое правило:** диагностику диска без `sudo` лучше не начинать. Любая Permission denied молча обнулит реальный объём каталога, и вы будете гоняться за несуществующей проблемой.
|
||||
|
||||
## /var/lib: Docker номер один
|
||||
|
||||
Раскрываем `/var` на уровень глубже:
|
||||
|
||||
```bash
|
||||
du -hx --max-depth=1 /var 2>/dev/null | sort -rh
|
||||
```
|
||||
|
||||
```
|
||||
26G /var/lib
|
||||
242M /var/log
|
||||
203M /var/cache
|
||||
728K /var/backups
|
||||
```
|
||||
|
||||
Весь вес - в `/var/lib`, остальное несущественно. `/var/lib` - общий контейнер для данных приложений, туда попадает и Docker, и базы данных, и системные служебные данные. Раскрываем ещё на уровень:
|
||||
|
||||
```bash
|
||||
du -hx --max-depth=1 /var/lib 2>/dev/null | sort -rh
|
||||
```
|
||||
|
||||
```
|
||||
25G /var/lib/docker
|
||||
283M /var/lib/apt
|
||||
18M /var/lib/dpkg
|
||||
17M /var/lib/plocate
|
||||
```
|
||||
|
||||
`/var/lib/docker` - 25G из 26G `/var/lib`. Остальное суммарно тянет на 350M. Виновник почти найден.
|
||||
|
||||
## docker system df врёт - не считает логи
|
||||
|
||||
У Docker есть встроенная команда, дающая разбиение по образам, контейнерам, volumes и build cache сразу:
|
||||
|
||||
```bash
|
||||
docker system df -v
|
||||
```
|
||||
|
||||
```
|
||||
Images space usage:
|
||||
REPOSITORY TAG SIZE
|
||||
grafana/grafana latest 485MB
|
||||
grafana/loki 2.9.2 74.6MB
|
||||
hello-world latest 13.3kB
|
||||
|
||||
Containers space usage:
|
||||
CONTAINER IMAGE CREATED STATUS SIZE
|
||||
loki grafana/loki:2.9.2 4 months ago Up 4 months 0B
|
||||
grafana grafana/grafana:latest 4 months ago Up 4 months 0B
|
||||
|
||||
Local Volumes space usage: (нет volumes)
|
||||
Build cache usage: 0B
|
||||
```
|
||||
|
||||
Суммарно - около 560MB. А `/var/lib/docker` весит 25G. Разница больше 24G, и `docker system df` её просто не видит.
|
||||
|
||||
Это известная засада: **`docker system df` не считает логи контейнеров**. Команда показывает только образы, writable-слои, volumes и build cache - JSON-логи, которые Docker пишет по умолчанию без лимита, в эту статистику не попадают вообще.
|
||||
|
||||
Проверяем напрямую, сколько весят лог-файлы контейнеров:
|
||||
|
||||
```bash
|
||||
du -h /var/lib/docker/containers/*/*-json.log 2>/dev/null | sort -rh
|
||||
```
|
||||
|
||||
```
|
||||
24G .../containers/<loki>/<loki>-json.log
|
||||
257M .../containers/<grafana>/<grafana>-json.log
|
||||
```
|
||||
|
||||
Оба контейнера работают по 4 месяца без перезапуска - времени накопить логи без ротации хватало обоим. Но разница в 93 раза говорит не просто об "отсутствии ротации" - что-то заставляет именно Loki писать аномально много.
|
||||
|
||||
## Почему именно Loki
|
||||
|
||||
Сначала смотрим характер записей - последние строки лога:
|
||||
|
||||
```bash
|
||||
CID=$(docker ps -qf "name=loki")
|
||||
tail -n 20 /var/lib/docker/containers/$CID/$CID-json.log
|
||||
```
|
||||
|
||||
Все видимые строки - `level=info`, без единой ошибки. Но паттерн заметный: лог крутится по последовательным индексным таблицам компактора - `index_20407` → `index_20406` → `index_20405`, каждая обрабатывается за пару миллисекунд. Это не разовая фоновая задача, это что-то, что молотит постоянно и очень быстро.
|
||||
|
||||
Ищем настоящие ошибки, с ограничением по времени, чтобы не зависнуть на полном проходе по 24G:
|
||||
|
||||
```bash
|
||||
timeout 15 grep -m 1 'level=error' /var/lib/docker/containers/$CID/$CID-json.log
|
||||
```
|
||||
|
||||
Нашлась одна:
|
||||
|
||||
```
|
||||
level=error ts=2026-01-26T12:36:53Z msg="error asking ring for who should run the compactor, will check again" err="at least 1 healthy replica required, could only find 0"
|
||||
```
|
||||
|
||||
Классика для однонодового Loki: ring-based compactor рассчитан на кластер, а здесь единственный инстанс, и lifecycler не успел зарегистрироваться в ring при старте. Но дата - 26 января, почти 5 месяцев назад. Похоже на разовый сбой при старте, а не на текущую причину - нужно проверить, повторяется ли она сейчас.
|
||||
|
||||
Дальше - оценка темпа записи. Первая попытка была неудачной, признаю сразу: `timeout` после 10 секунд убил всю цепочку процессов сигналом, и `wc -l` не успел вывести даже ноль - результат недостоверен, метод сломан, аргумент в любую сторону строить на нём нельзя.
|
||||
|
||||
Первая попытка - через docker logs в реальном времени:
|
||||
|
||||
```bash
|
||||
timeout 10 docker logs -f --tail 0 $CID | wc -l
|
||||
```
|
||||
|
||||
Второй подход - без живого слежения, через временные метки в самом файле:
|
||||
|
||||
```bash
|
||||
tail -n 2000 /var/lib/docker/containers/$CID/$CID-json.log | head -n 1
|
||||
tail -n 1 /var/lib/docker/containers/$CID/$CID-json.log
|
||||
```
|
||||
|
||||
Разница между 2000-й-с-конца записью и последней - 7.48 секунды. 2000 строк за 7.48с - это около 267 строк в секунду. При среднем объёме 24G за 4 месяца (~200MB в день, ~2.3KB/сек, то есть 10-15 строк/сек) пойманное значение в 20+ раз выше среднего - это всплеск компактора, а не стабильный фон. Экстраполировать всплеск на все 4 месяца было бы нечестно по отношению к методу.
|
||||
|
||||
Проверяем ошибки за недавний период без скана всех 24G - берём последние 500MB через seek по байтам:
|
||||
|
||||
```bash
|
||||
tail -c 500M /var/lib/docker/containers/$CID/$CID-json.log | grep -c 'level=error'
|
||||
```
|
||||
|
||||
```
|
||||
0
|
||||
```
|
||||
|
||||
Ноль ошибок в недавней истории. Гипотеза "контейнер сломан и спамит ошибками" закрыта - дело не в этом.
|
||||
|
||||
## Главная причина: лимит логов никто не настраивал
|
||||
|
||||
Раз дело не в ошибках, причина - в объёме обычного info-логирования без всякого предела. Смотрим, ограничен ли размер логов хоть на каком-то уровне.
|
||||
|
||||
Глобальная настройка драйвера логов:
|
||||
|
||||
```bash
|
||||
cat /etc/docker/daemon.json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"registry-mirrors": ["https://mirror.example.com"]
|
||||
}
|
||||
```
|
||||
|
||||
Настройка конкретного контейнера:
|
||||
|
||||
```bash
|
||||
docker inspect --format '{{json .HostConfig.LogConfig}}' $CID
|
||||
```
|
||||
|
||||
```json
|
||||
{"Type":"json-file","Config":{}}
|
||||
```
|
||||
|
||||
Оба пустые. Используется дефолт Docker - драйвер `json-file` без `max-size`/`max-file`. Компактор болтлив на уровне info, лимита нет, контейнер живёт 4 месяца - 24G неизбежны. Причина подтверждена документально, не догадкой.
|
||||
|
||||
## Остальные подозреваемые - для полноты
|
||||
|
||||
Виновник найден, но по уговору формата добиваем весь список систематически - это даёт читателю с другой причиной полную картину.
|
||||
|
||||
| Подозреваемый | Команда | Результат |
|
||||
|---|---|---|
|
||||
| journald | `journalctl --disk-usage` | 245.9M, чисто |
|
||||
| Старые ядра | `dpkg -l 'linux-image-*' \| grep ^ii` | 3 версии, но на отдельном `/boot`, не влияет на `/` |
|
||||
| Кэш apt | `du -sh /var/cache/apt/archives` | 118M, чисто |
|
||||
| Удалённые, но открытые файлы | `lsof +L1` | пустой вывод, suspect закрыт окончательно |
|
||||
|
||||
Последний пункт стоит выделить отдельно: пустой `lsof +L1` - это не "цифры совпали", а прямое подтверждение, что ни один процесс не держит открытым файл с нулевым числом ссылок. Гипотеза про "призрачный файл" была отвергнута на двух независимых основаниях - арифметикой `du`/`df` раньше и прямым инструментом сейчас.
|
||||
|
||||
## Лечение: освобождаем место
|
||||
|
||||
Truncate файла "на горячую" безопасен для драйвера `json-file` - Docker продолжит дописывать в тот же inode, ничего не упадёт:
|
||||
|
||||
```bash
|
||||
truncate -s 0 /var/lib/docker/containers/$CID/$CID-json.log
|
||||
```
|
||||
|
||||
Проверяем результат:
|
||||
|
||||
```bash
|
||||
df -h /
|
||||
```
|
||||
|
||||
```
|
||||
Filesystem Size Used Avail Use% Mounted on
|
||||
/dev/mapper/vg01-root 29G 5.1G 24G 18% /
|
||||
```
|
||||
|
||||
Освободилось 23.9G - почти точное совпадение с размером усечённого файла. Это не просто "стало легче", а математическое подтверждение, что причина определена верно.
|
||||
|
||||
## Настройка лимита - и неожиданный нюанс
|
||||
|
||||
Чтобы это не повторилось ни с Loki, ни с любым другим контейнером, добавляем лимит в `daemon.json`:
|
||||
|
||||
```bash
|
||||
cat > /etc/docker/daemon.json << 'EOF'
|
||||
{
|
||||
"registry-mirrors": ["https://mirror.example.com"],
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "20m",
|
||||
"max-file": "5"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
systemctl restart docker
|
||||
```
|
||||
|
||||
Проверяем, применилось ли:
|
||||
|
||||
```bash
|
||||
docker inspect --format '{{json .HostConfig.LogConfig}}' $CID
|
||||
```
|
||||
|
||||
```json
|
||||
{"Type":"json-file","Config":{}}
|
||||
```
|
||||
|
||||
Всё ещё пусто. **Глобальный `log-opts` действует только для новых контейнеров**, созданных после изменения - не для существующих. `LogConfig` фиксируется в момент создания контейнера и не обновляется при `restart docker`, только при пересоздании.
|
||||
|
||||
Чтобы пересоздать контейнер без потери параметров, сначала смотрим, как он был запущен - через лейблы compose-проекта:
|
||||
|
||||
```bash
|
||||
docker inspect --format '{{json .Config.Labels}}' $CID
|
||||
```
|
||||
|
||||
В выводе - `com.docker.compose.project.working_dir` и `com.docker.compose.service: loki`. Контейнер управляется через compose, путь известен:
|
||||
|
||||
```bash
|
||||
cd /home/deploy/docker
|
||||
docker compose up -d --force-recreate loki
|
||||
```
|
||||
|
||||
```
|
||||
✔ Container loki Started
|
||||
```
|
||||
|
||||
Проверяем лимит на пересозданном контейнере:
|
||||
|
||||
```bash
|
||||
docker inspect --format '{{json .HostConfig.LogConfig}}' loki
|
||||
```
|
||||
|
||||
```json
|
||||
{"Type":"json-file","Config":{"max-file":"5","max-size":"20m"}}
|
||||
```
|
||||
|
||||
Применилось. Тот же compose-проект поднимает и `grafana` - у неё на момент инцидента всего 257M логов, но без лимита она повторила бы путь Loki за более долгий срок. Раз инструмент уже в руках, закрываем сразу, не оставляя половину работы:
|
||||
|
||||
```bash
|
||||
docker compose up -d --force-recreate grafana
|
||||
```
|
||||
|
||||
## Побочный квест: ложная тревога с инодами
|
||||
|
||||
Возвращаемся к инодовой детали, отложенной в начале. Сверяем `df -i` сейчас с замером в начале расследования:
|
||||
|
||||
```bash
|
||||
df -i /
|
||||
```
|
||||
|
||||
```
|
||||
Filesystem Inodes IUsed IFree IUse% Mounted on
|
||||
/dev/mapper/vg01-root 15224832 66741 15158091 1% /
|
||||
```
|
||||
|
||||
| Замер | Inodes всего | IUsed | IFree | IUse% |
|
||||
|---|---|---|---|---|
|
||||
| В начале | 67424 | 66740 | 684 | 99% |
|
||||
| Сейчас | 15224832 | 66741 | 15158091 | 1% |
|
||||
|
||||
`IUsed` почти не изменился, а общее число инодов выросло в 225 раз. В ext4 количество инодов фиксируется в суперблоке при `mkfs` и не меняется без явного `resize2fs` - а размер тома не менялся вообще. Гипотеза "том создан с искусственно малым лимитом инодов" построена на ext4-логике, и она оказалась неверной - я строил её, не проверив тип файловой системы.
|
||||
|
||||
Смотрим напрямую в суперблок:
|
||||
|
||||
```bash
|
||||
tune2fs -l /dev/mapper/vg01-root | grep -i inode
|
||||
```
|
||||
|
||||
```
|
||||
tune2fs: Bad magic number in super-block while trying to open /dev/mapper/vg01-root
|
||||
```
|
||||
|
||||
Это не ошибка диагностики - это разоблачение неверной гипотезы. `tune2fs` понимает только ext2/ext3/ext4. Если суперблок не найден - файловая система другая.
|
||||
|
||||
Подтверждаем тип ФС напрямую:
|
||||
|
||||
```bash
|
||||
findmnt -no FSTYPE /
|
||||
```
|
||||
|
||||
```
|
||||
xfs
|
||||
```
|
||||
|
||||
Подтверждено: XFS. У XFS иноды выделяются динамически, а не фиксируются при создании. Столбец "Inodes" в `df -i` для XFS - это не константа, а оценка: текущие занятые иноды плюс расчёт, сколько ещё теоретически можно выделить исходя из свободного места на диске. При почти 100% занятости диска эта оценка падала почти до нуля, создавая видимость жёсткого лимита. После truncate освободилось 24G - и оценка свободных инодов резко выросла, потому что физически появилось место для их размещения.
|
||||
|
||||
Никакой аномалии не было - был артефакт интерпретации `df -i` на XFS при заполненном диске. Прячущая деталь: `IUsed` остался почти неизменным (66740 → 66741), и именно это надо было проверить раньше, чем строить гипотезу про лимит.
|
||||
|
||||
## Итог
|
||||
|
||||
Финальная сверка по диску:
|
||||
|
||||
```bash
|
||||
df -h /
|
||||
df -i /
|
||||
```
|
||||
|
||||
```
|
||||
Filesystem Size Used Avail Use% Mounted on
|
||||
/dev/mapper/vg01-root 29G 4.9G 25G 17% /
|
||||
|
||||
Filesystem Inodes IUsed IFree IUse% Mounted on
|
||||
/dev/mapper/vg01-root 15224832 66741 15158091 1% /
|
||||
```
|
||||
|
||||
Было 29G из 29G, 100%. Стало 4.9G из 29G, 17%. Оба контейнера теперь ограничены лимитом логов - повторения с тем же сценарием не будет.
|
||||
|
||||
## Чек-лист: куда смотреть при заполненном диске
|
||||
|
||||
*Этот раздел - справочный.*
|
||||
|
||||
| Подозреваемый | Команда проверки | На что обратить внимание |
|
||||
|---|---|---|
|
||||
| Байты vs иноды | `df -h`, `df -i` | Какой именно лимит исчерпан; на XFS число инодов в `df -i` - оценка, а не константа |
|
||||
| `du` без -x | `du -hx --max-depth=1 /` | Без `-x` команда пересечёт точки монтирования и даст мусорные числа |
|
||||
| `du` без root | `sudo du -hx ...` | Без прав молча обнуляет недоступные каталоги |
|
||||
| Docker логи контейнеров | `du -h /var/lib/docker/containers/*/*-json.log` | `docker system df` логи контейнеров не считает вообще |
|
||||
| Лимит логов Docker | `docker inspect --format '{{json .HostConfig.LogConfig}}' <id>` | Пустой `Config{}` значит лимита нет; глобальный `daemon.json` не действует на уже созданные контейнеры |
|
||||
| journald | `journalctl --disk-usage` | Может разрастись без `SystemMaxUse` в `journald.conf` |
|
||||
| Старые ядра | `dpkg -l 'linux-image-*'` | Чистить через `apt autoremove --purge`, обычно живут в `/boot` отдельным томом |
|
||||
| Кэш apt | `du -sh /var/cache/apt/archives` | `apt clean` освобождает мгновенно |
|
||||
| Удалённые, но открытые файлы | `lsof +L1` | Процесс держит дескриптор - место не освободится без перезапуска процесса |
|
||||
|
||||
## Стек
|
||||
|
||||
Debian 12, XFS, Docker, Loki 2.9.2, Grafana, Docker Compose 2.29.7, Zabbix.
|
||||
|
||||
## Что дальше
|
||||
|
||||
Следующая статья серии - тот же сценарий для RHEL-подобных систем: dnf-кэш вместо apt, другие пути логов, и проверка, какие из подозреваемых из этого списка вообще не применимы на RHEL-like.
|
||||
|
Before Width: | Height: | Size: 1.4 MiB |
@@ -4,7 +4,7 @@ date: 2025-10-14
|
||||
draft: false
|
||||
description: "K3s HA кластер для homelab: архитектура с 3 master нодами и embedded etcd. 50MB бинарник вместо 1.5GB зависимостей — почему это работает."
|
||||
tags: ["kubernetes", "k3s", "homelab", "proxmox", "architecture", "ha", "devops"]
|
||||
categories: ["Kubernetes", "Homelab"]
|
||||
categories: ["devops-практики"]
|
||||
series: ["K3s HA кластер для homelab"]
|
||||
series_order: 1
|
||||
---
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 MiB |
@@ -4,7 +4,7 @@ date: 2025-10-21
|
||||
draft: false
|
||||
description: "Готовим инфраструктуру для K3s в Proxmox: 5 VM с Debian 12, настройка сети, отключение swap. Пошаговая инструкция без сюрпризов."
|
||||
tags: ["kubernetes", "k3s", "homelab", "proxmox", "infrastructure", "debian", "devops"]
|
||||
categories: ["Kubernetes", "Homelab"]
|
||||
categories: ["devops-практики"]
|
||||
series: ["K3s HA кластер для homelab"]
|
||||
series_order: 2
|
||||
---
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 MiB |
@@ -4,7 +4,7 @@ date: 2025-11-02
|
||||
draft: false
|
||||
description: "Устанавливаем K3s HA кластер: 3 master и 2 worker ноды за 15 минут. Один curl-скрипт на ноду — и кластер готов к работе."
|
||||
tags: ["kubernetes", "k3s", "homelab", "installation", "ha", "etcd", "devops"]
|
||||
categories: ["Kubernetes", "Homelab", "DevOps практики"]
|
||||
categories: ["devops-практики"]
|
||||
series: ["K3s HA кластер для homelab"]
|
||||
series_order: 3
|
||||
---
|
||||
|
||||
@@ -0,0 +1,624 @@
|
||||
---
|
||||
title: "Свой почтовый сервер на Debian 12 вместо Gmail: полное руководство. Часть 1 - Начало"
|
||||
date: 2026-03-05
|
||||
draft: false
|
||||
description: "Зачем поднимать свой mail-сервер вместо Gmail. Анатомия почтового сервера: Postfix, Dovecot, антиспам, антивирус. Системные требования и чек-лист готовности."
|
||||
summaty: "Зачем поднимать свой mail-сервер вместо Gmail. Анатомия почтового сервера: Postfix, Dovecot, антиспам, антивирус. Системные требования и чек-лист готовности."
|
||||
tags: ["postfix", "dovecot", "smtp", "imap", "self-hosting"]
|
||||
categories: ["Почтовые системы"]
|
||||
series: ["Почтовый сервер на Debian 12"]
|
||||
series_order: 1
|
||||
---
|
||||
|
||||
Решил поднять свой почтовый сервер? Отлично. Сейчас объясню, почему это одновременно лучшее и худшее решение, которое ты можешь принять для своей инфраструктуры.
|
||||
|
||||
## Зачем вообще заморачиваться
|
||||
|
||||
> Что говорят в интернете:
|
||||
> "Gmail бесплатный и работает отлично, зачем изобретать велосипед?"
|
||||
|
||||
### Как есть на самом деле:
|
||||
|
||||
**Gmail и прочие публичные сервисы - это хорошо до тех пор, пока:**
|
||||
- Тебя не смущает, что твою переписку читают для таргетинга рекламы
|
||||
- Ты не против того, что твой домен висит на чужой инфраструктуре
|
||||
- Тебе не критично, когда Google решит внезапно заблокировать аккаунт без объяснений
|
||||
- Ты готов платить за каждый ящик в корпоративном тарифе
|
||||
- Тебе норм, что лимиты на размер ящика устанавливает кто-то другой
|
||||
|
||||
**Свой почтовый сервер дает:**
|
||||
- **Полный контроль** - ты сам решаешь кто, что и как
|
||||
- **Безлимитные ящики** - сколько нужно, столько и создашь
|
||||
- **Любой размер** - ограничение только в железе
|
||||
- **Свои правила** - никаких внезапных "обновлений политики"
|
||||
- **Прозрачность** - ты знаешь где лежит твоя почта и кто к ней имеет доступ
|
||||
|
||||
Но есть нюанс.
|
||||
|
||||
## Реальность: во что ты ввязываешься
|
||||
|
||||
### Первые 48 часов после запуска:
|
||||
|
||||
`Час 1:` Сервер работает, письма ходят, ты доволен собой.
|
||||
|
||||
`Час 3:` Gmail отправляет твои письма в спам. Начинаешь разбираться с SPF.
|
||||
|
||||
`Час 6:` SPF настроен. Письма все равно в спаме. Погружаешься в DKIM.
|
||||
|
||||
`Час 12:` DKIM работает. Половина писем доходит. Открываешь документацию DMARC.
|
||||
|
||||
`Час 24:` Понимаешь, что твой IP попал в какой-то DNSBL. Гуглишь что это вообще такое.
|
||||
|
||||
`Час 48:` Письма наконец-то доходят до inbox. Пользователь жалуется: "Я не получил письмо от клиента". Начинаешь копаться в логах.
|
||||
|
||||
`Неделя 1:` Обнаруживаешь, что сервер стал источником спама. Как это получилось - вопрос другой.
|
||||
|
||||
`Месяц 1:` Осознаешь, что забыл настроить бэкапы. Молишься, чтобы диск не "помер".
|
||||
|
||||
### Это нормально
|
||||
|
||||
Я не пугаю. Я готовлю к реальности. Почтовый сервер - это не "поставил и забыл". Это инфраструктура, которая требует:
|
||||
- Начальной настройки (4-8 часов чистого времени)
|
||||
- Отладки репутации (первые 2-4 недели)
|
||||
- Регулярного мониторинга (15-30 минут в день)
|
||||
- Периодического обслуживания (2-4 часа в месяц)
|
||||
|
||||
Но когда оно работает - работает **как часы**.
|
||||
|
||||
## Что ты получишь в итоге
|
||||
|
||||
### Техническая часть:
|
||||
|
||||
```
|
||||
ТВОЙ ПОЧТОВЫЙ СЕРВЕР
|
||||
│
|
||||
├─ Прием и отправка почты (Postfix)
|
||||
├─ Доступ к ящикам через IMAP (Dovecot)
|
||||
├─ Веб-интерфейс (RoundCube)
|
||||
├─ Защита от вирусов (ClamAV)
|
||||
├─ Защита от спама (SpamAssassin)
|
||||
├─ Защита от взлома (Fail2ban)
|
||||
├─ Шифрование (Let's Encrypt)
|
||||
├─ Подписи писем (DKIM)
|
||||
└─ Мониторинг и логи (Pflogsumm)
|
||||
```
|
||||
|
||||
### Функциональная часть:
|
||||
|
||||
**Отправка почты:**
|
||||
- С любого почтового клиента (Thunderbird, Outlook, Apple Mail, K-9)
|
||||
- Через веб-интерфейс из любой точки мира
|
||||
- С защищенным соединением (TLS)
|
||||
- С цифровой подписью (твои письма не подделать)
|
||||
|
||||
**Прием почты:**
|
||||
- От любых отправителей
|
||||
- С проверкой на вирусы
|
||||
- С фильтрацией спама
|
||||
- С пользовательскими правилами (Sieve)
|
||||
|
||||
**Управление:**
|
||||
- Неограниченное количество доменов
|
||||
- Неограниченное количество ящиков
|
||||
- Псевдонимы (алиасы)
|
||||
- Пересылки
|
||||
- Автоответчики
|
||||
- Квоты на размер (если нужно)
|
||||
|
||||
**Безопасность:**
|
||||
- Шифрование при передаче (никто не прочитает по пути)
|
||||
- Защита от подбора паролей
|
||||
- Блокировка спамеров
|
||||
- Проверка отправителей
|
||||
|
||||
## Из чего это состоит: анатомия почтового сервера
|
||||
|
||||
Почтовый сервер - это не одна программа. Это экосистема из нескольких компонентов, каждый из которых делает свою работу.
|
||||
|
||||
### Postfix - почтальон
|
||||
|
||||
**Что делает:** Принимает письма от отправителей и доставляет их получателям.
|
||||
|
||||
**Как работает:**
|
||||
1. Кто-то отправляет письмо на user@твой-домен.ru
|
||||
2. Postfix принимает письмо на порт 25
|
||||
3. Проверяет: "А должен ли я вообще принимать почту для этого домена?"
|
||||
4. Проверяет отправителя через кучу правил
|
||||
5. Если все ОК - передает письмо дальше (Dovecot или другому Postfix)
|
||||
6. Если что-то не так - отклоняет с кодом ошибки
|
||||
|
||||
**Конфиг:** `/etc/postfix/main.cf` - туда ты будешь лезть чаще всего.
|
||||
|
||||
### Dovecot - хранитель ящиков
|
||||
|
||||
**Что делает:** Дает доступ к почтовым ящикам по протоколам IMAP/POP3.
|
||||
|
||||
**Как работает:**
|
||||
1. Почтовый клиент подключается к Dovecot (порт 143/993)
|
||||
2. Вводит логин и пароль
|
||||
3. Dovecot проверяет учетные данные
|
||||
4. Открывает доступ к почтовому ящику
|
||||
5. Клиент качает письма
|
||||
|
||||
**Дополнительно:**
|
||||
- Работает как LDA (Local Delivery Agent) - складывает письма в ящики
|
||||
- Поддерживает Sieve - пользовательские фильтры
|
||||
- Управляет квотами
|
||||
|
||||
**Конфиг:** `/etc/dovecot/dovecot.conf` и куча подфайлов в `/etc/dovecot/conf.d/`.
|
||||
|
||||
### Amavis + ClamAV - санитары
|
||||
|
||||
**Что делает Amavis:** Прослойка между Postfix и антивирусом/антиспамом.
|
||||
|
||||
**Что делает ClamAV:** Сканирует вложения на вирусы.
|
||||
|
||||
**Как работает:**
|
||||
1. Postfix получает письмо
|
||||
2. Отправляет его в Amavis (порт 10024)
|
||||
3. Amavis передает в ClamAV
|
||||
4. ClamAV сканирует
|
||||
5. Если вирус найден - письмо в карантин
|
||||
6. Если чисто - Amavis возвращает в Postfix (порт 10025)
|
||||
7. Postfix доставляет получателю
|
||||
|
||||
**Конфиг:** `/etc/amavis/conf.d/50-user`
|
||||
|
||||
### SpamAssassin - фильтр
|
||||
|
||||
**Что делает:** Определяет спам по куче признаков.
|
||||
|
||||
**Как работает:**
|
||||
1. Анализирует заголовки письма
|
||||
2. Проверяет тело письма
|
||||
3. Смотрит IP отправителя в черных списках (DNSBL)
|
||||
4. Применяет эвристические правила
|
||||
5. Использует Bayesian фильтр (обучаемый)
|
||||
6. Выставляет баллы
|
||||
7. Если баллов больше порога (обычно 5) - помечает как SPAM
|
||||
|
||||
**Обучение:**
|
||||
- Показываешь примеры спама → он учится
|
||||
- Показываешь примеры нормальной почты → он учится
|
||||
- Со временем точность растет
|
||||
|
||||
**Конфиг:** `/etc/spamassassin/local.cf`
|
||||
|
||||
### Postgrey - вышибала
|
||||
|
||||
**Что делает:** Временно отклоняет письма от новых отправителей.
|
||||
|
||||
**Как работает:**
|
||||
1. Письмо приходит от нового сервера
|
||||
2. Postgrey: "Приходи через 5 минут"
|
||||
3. Легальный почтовый сервер вернется через 5 минут
|
||||
4. Спам-бот не вернется (ему некогда)
|
||||
5. При повторной попытке - пропускает
|
||||
|
||||
**Эффективность:** Отсекает ~70% спама вообще без анализа.
|
||||
|
||||
**Конфиг:** `/etc/default/postgrey`
|
||||
|
||||
### Fail2ban - охрана
|
||||
|
||||
**Что делает:** Блокирует IP-адреса, которые брутфорсят(взламывают методом перебора парольных комбинаций) пароли.
|
||||
|
||||
**Как работает:**
|
||||
1. Следит за логами
|
||||
2. Видит неудачные попытки входа
|
||||
3. Считает количество попыток
|
||||
4. Если больше 3-5 за короткое время - бан IP через iptables
|
||||
5. Через час-два-пять(как настроишь) разбанивает (если не повторится)
|
||||
|
||||
**Защищает:**
|
||||
- SMTP AUTH (порт 25, 587)
|
||||
- IMAP/POP3 (порт 143, 993, 110, 995)
|
||||
- Веб-интерфейс RoundCube
|
||||
-
|
||||
**Конфиг:** `/etc/fail2ban/jail.local`
|
||||
|
||||
### Let's Encrypt - замок
|
||||
|
||||
**Что делает:** Выдает бесплатные SSL-сертификаты.
|
||||
|
||||
**Зачем:** Шифрование соединений между клиентом и сервером.
|
||||
|
||||
**Как работает:**
|
||||
1. Запускаешь Certbot
|
||||
2. Он доказывает, что домен принадлежит тебе
|
||||
3. Let's Encrypt выдает сертификат на 90 дней
|
||||
4. Certbot автоматически продлевает каждые 60 дней
|
||||
|
||||
**Без этого:** Все пароли и письма идут открытым текстом по сети.
|
||||
|
||||
**Конфиг:** Автоматический, сертификаты в `/etc/letsencrypt/live/`
|
||||
|
||||
### OpenDKIM - печать
|
||||
|
||||
**Что делает:** Подписывает исходящие письма цифровой подписью.
|
||||
|
||||
**Зачем:** Доказывает, что письмо действительно от твоего домена.
|
||||
|
||||
**Как работает:**
|
||||
1. Генеришь пару ключей (открытый + закрытый)
|
||||
2. Открытый публикуешь в DNS
|
||||
3. OpenDKIM подписывает каждое исходящее письмо закрытым ключом
|
||||
4. Получатель проверяет подпись открытым ключом из DNS
|
||||
5. Если подпись валидна - письмо не подделано
|
||||
|
||||
**Без этого:** Gmail/Outlook 100% отправят твои письма в спам.
|
||||
|
||||
**Конфиг:** `/etc/opendkim.conf`
|
||||
|
||||
### RoundCube - веб-морда
|
||||
|
||||
**Что делает:** Веб-интерфейс для работы с почтой.
|
||||
|
||||
**Зачем:** Читать/писать письма через браузер без настройки почтового клиента.
|
||||
|
||||
**Возможности:**
|
||||
- Чтение/отправка писем
|
||||
- Адресная книга
|
||||
- Настройка фильтров (через плагин Sieve)
|
||||
- Смена паролей
|
||||
- Управление папками
|
||||
- Поиск по почте
|
||||
|
||||
**Конфиг:** `/etc/roundcube/config.inc.php`
|
||||
|
||||
### PostgreSQL - база данных
|
||||
|
||||
**Что делает:** Хранит данные.
|
||||
|
||||
**Что хранит:**
|
||||
- Базу RoundCube (сессии, адресная книга, кэш)
|
||||
- Опционально: список пользователей и паролей
|
||||
- Опционально: псевдонимы и пересылки
|
||||
|
||||
**Почему PostgreSQL, а не MySQL:**
|
||||
- Строже к типам данных → меньше косяков
|
||||
- Лучше работает с UTF-8
|
||||
- Проще репликация
|
||||
- В Debian 12 отличная интеграция
|
||||
|
||||
**Конфиг:** `/etc/postgresql/15/main/postgresql.conf`
|
||||
|
||||
### Apache + PHP
|
||||
|
||||
**Что делает:** Крутит RoundCube.
|
||||
|
||||
**Apache:** Веб-сервер, принимает HTTP-запросы.
|
||||
|
||||
**PHP:** Интерпретатор, выполняет код RoundCube.
|
||||
|
||||
**Альтернатива:** Nginx + PHP-FPM (быстрее, но сложнее, может в будущем рассмотрю и такой подход).
|
||||
|
||||
**Почему Apache:** Работает из коробки, тупо проще с той же эффективностью.
|
||||
|
||||
**Конфиг:** `/etc/apache2/sites-available/`
|
||||
|
||||
### Pflogsumm
|
||||
|
||||
**Что делает:** Анализирует логи Postfix и делает отчеты.
|
||||
|
||||
**Показывает:**
|
||||
- Сколько писем отправлено/получено
|
||||
- Сколько отклонено
|
||||
- Топ отправителей/получателей
|
||||
- Ошибки доставки
|
||||
- Статистику по доменам
|
||||
|
||||
**Использование:**
|
||||
```bash
|
||||
pflogsumm /var/log/mail.log
|
||||
```
|
||||
|
||||
**Автоматизация:** Настроишь cron - каждый день отчет на почту.
|
||||
|
||||
|
||||
### Netdata - приборная панель
|
||||
|
||||
**Что делает:** Мониторинг в реальном времени.
|
||||
|
||||
**Показывает:**
|
||||
- Загрузка CPU, RAM, Disk
|
||||
- Очереди Postfix
|
||||
- Соединения Dovecot
|
||||
- Запросы к PostgreSQL
|
||||
- Запросы к Apache
|
||||
- Температура (если есть датчики)
|
||||
|
||||
**Интерфейс:** Веб на порту 19999.
|
||||
|
||||
**Потребление:** ~100MB RAM.
|
||||
|
||||
|
||||
## Как все это работает вместе
|
||||
|
||||
### Сценарий 1: Получение письма
|
||||
|
||||
```
|
||||
1. example.ru отправляет письмо на user@твой-домен.ru
|
||||
↓
|
||||
2. DNS: "MX-запись для твой-домен.ru → mail.твой-домен.ru"
|
||||
↓
|
||||
3. Письмо приходит на твой сервер (Postfix, порт 25)
|
||||
↓
|
||||
4. Postfix: "Проверю отправителя..."
|
||||
- SPF проверка
|
||||
- DNSBL проверка
|
||||
- Greylisting (Postgrey)
|
||||
↓
|
||||
5. Postfix: "Отправлю на проверку в Amavis"
|
||||
↓
|
||||
6. Amavis → ClamAV: "Есть вирусы?"
|
||||
ClamAV: "Чисто"
|
||||
↓
|
||||
7. Amavis → SpamAssassin: "Это спам?"
|
||||
SpamAssassin: "3 балла из 5, норм"
|
||||
↓
|
||||
8. Amavis возвращает в Postfix: "Все ок, доставляй"
|
||||
↓
|
||||
9. Postfix → Dovecot (LMTP): "Положи в ящик user@твой-домен.ru"
|
||||
↓
|
||||
10. Dovecot кладет в /var/spool/mail/твой-домен.ru/user/
|
||||
↓
|
||||
11. Пользователь открывает RoundCube или Thunderbird
|
||||
↓
|
||||
12. Dovecot (IMAP) отдает письмо клиенту
|
||||
```
|
||||
|
||||
### Сценарий 2: Отправка письма
|
||||
|
||||
```
|
||||
1. Пользователь пишет письмо в RoundCube
|
||||
↓
|
||||
2. RoundCube → Postfix (порт 587, SMTP Submission)
|
||||
↓
|
||||
3. Postfix: "Проверю авторизацию..."
|
||||
- SMTP AUTH через Dovecot
|
||||
↓
|
||||
4. Postfix: "Пользователь свой, подпишу письмо"
|
||||
↓
|
||||
5. OpenDKIM добавляет DKIM-подпись
|
||||
↓
|
||||
6. Postfix отправляет письмо на mail.example.ru
|
||||
↓
|
||||
7. example.ru получает и проверяет:
|
||||
- SPF (в DNS твоего домена)
|
||||
- DKIM (подпись валидна?)
|
||||
- DMARC (политика домена)
|
||||
↓
|
||||
8. example.ru: "Все проверки пройдены" → Inbox
|
||||
```
|
||||
|
||||
## Терминология: что есть что
|
||||
|
||||
### MTA (Mail Transfer Agent)
|
||||
**Что:** Программа, которая передает почту между серверами.
|
||||
**Пример:** Postfix, Sendmail, Exim.
|
||||
|
||||
### MDA (Mail Delivery Agent)
|
||||
**Что:** Программа, которая кладет письма в почтовые ящики.
|
||||
**Пример:** Dovecot (в режиме LDA/LMTP).
|
||||
|
||||
### MUA (Mail User Agent)
|
||||
**Что:** Почтовый клиент.
|
||||
**Пример:** Thunderbird, Outlook, RoundCube, K-9 Mail.
|
||||
|
||||
### SMTP (Simple Mail Transfer Protocol)
|
||||
**Что:** Протокол отправки почты.
|
||||
**Порты:** 25 (сервер-сервер), 587 (клиент-сервер с авторизацией).
|
||||
|
||||
### IMAP (Internet Message Access Protocol)
|
||||
**Что:** Протокол доступа к почте (с синхронизацией).
|
||||
**Порты:** 143 (открытый), 993 (с TLS).
|
||||
**Особенность:** Письма хранятся на сервере.
|
||||
|
||||
### POP3 (Post Office Protocol)
|
||||
**Что:** Протокол доступа к почте (скачивание).
|
||||
**Порты:** 110 (открытый), 995 (с TLS).
|
||||
**Особенность:** Письма скачиваются и удаляются с сервера.
|
||||
**Статус:** Устарел, не будем использовать.
|
||||
|
||||
### TLS/SSL
|
||||
**Что:** Шифрование соединения.
|
||||
**Зачем:** Чтобы пароли и письма не перехватили.
|
||||
**Пример:** HTTPS для почты.
|
||||
|
||||
### SPF (Sender Policy Framework)
|
||||
**Что:** DNS-запись, которая говорит "с этих IP можно слать почту от моего домена".
|
||||
**Пример:** `v=spf1 ip4:1.2.3.4 ~all`
|
||||
**Зачем:** Защита от подделки отправителя.
|
||||
|
||||
### DKIM (DomainKeys Identified Mail)
|
||||
**Что:** Цифровая подпись письма.
|
||||
**Как:** Закрытый ключ на сервере, открытый в DNS.
|
||||
**Зачем:** Доказать, что письмо не подделано.
|
||||
|
||||
### DMARC (Domain-based Message Authentication)
|
||||
**Что:** Политика домена: что делать, если SPF или DKIM не прошли.
|
||||
**Варианты:** none (ничего), quarantine (в спам), reject (не принимать).
|
||||
**Пример:** `v=DMARC1; p=quarantine; rua=mailto:dmarc@домен.ru`
|
||||
|
||||
### DNSBL (DNS-based Blackhole List)
|
||||
**Что:** Черные списки IP-адресов спамеров.
|
||||
**Примеры:** zen.spamhaus.org, bl.spamcop.net.
|
||||
**Как работает:** Postfix спрашивает DNSBL: "Этот IP спамер?" → DNSBL отвечает.
|
||||
|
||||
### Greylisting
|
||||
**Что:** Временная задержка писем от новых отправителей.
|
||||
**Логика:** Спам-боты не повторяют попытки, легальные серверы - повторяют.
|
||||
**Задержка:** 5 минут (обычно).
|
||||
|
||||
### Relay
|
||||
**Что:** Пересылка почты через промежуточный сервер.
|
||||
**Пример:** Твой сервер → SMTP провайдера → получатель.
|
||||
**Зачем:** Если твой IP в блэклистах или нет белого IP.
|
||||
|
||||
### Open Relay
|
||||
**Что:** Сервер, который пересылает почту от кого угодно.
|
||||
**Статус:** **ЗЛО**. Мгновенно попадешь в блэклисты.
|
||||
**Защита:** SMTP AUTH + правильные restrictions.
|
||||
|
||||
### Maildir vs Mbox
|
||||
**Mbox:** Все письма в одном файле.
|
||||
**Maildir:** Каждое письмо - отдельный файл.
|
||||
**Используем:** Maildir (надежнее, быстрее).
|
||||
|
||||
### Sieve
|
||||
**Что:** Язык для создания почтовых фильтров.
|
||||
**Пример:** "Если тема содержит 'счет', переложить в папку 'Финансы'".
|
||||
**Управление:** Через плагин managesieve в RoundCube.
|
||||
|
||||
### Quota
|
||||
**Что:** Ограничение на размер почтового ящика.
|
||||
**Пример:** 5GB на пользователя.
|
||||
**Наш случай:** Без ограничений (или устанавливаешь сам).
|
||||
|
||||
## Системные требования
|
||||
|
||||
### Минимальная конфигурация (1-50 пользователей):
|
||||
|
||||
```
|
||||
CPU: 2 ядра (любой современный процессор)
|
||||
RAM: 2 GB
|
||||
Disk: 20 GB (система) + объем почты
|
||||
SSD рекомендуется
|
||||
Net: Стабильное подключение
|
||||
Белый IP (желательно)
|
||||
Открытые порты: 25, 587, 143, 993, 80, 443
|
||||
```
|
||||
|
||||
### Рекомендуемая конфигурация (50-200 пользователей):
|
||||
|
||||
```
|
||||
CPU: 4 ядра
|
||||
RAM: 4 GB
|
||||
Disk: 50 GB (система) + объем почты
|
||||
SSD обязательно
|
||||
Net: 100 Мбит/с
|
||||
Белый статический IP
|
||||
```
|
||||
|
||||
### Оптимальная конфигурация (200-500 пользователей):
|
||||
|
||||
```
|
||||
CPU: 8 ядер
|
||||
RAM: 8 GB
|
||||
Disk: 100 GB (система) + объем почты
|
||||
NVMe SSD
|
||||
Net: 1 Гбит/с
|
||||
Резервный канал
|
||||
```
|
||||
|
||||
### Расчет дискового пространства:
|
||||
|
||||
```
|
||||
Средний пользователь: 1-5 GB в год
|
||||
Активный пользователь: 10-20 GB в год
|
||||
Очень активный: 50+ GB в год
|
||||
|
||||
Пример на 100 пользователей:
|
||||
100 × 5 GB = 500 GB
|
||||
+ запас 20% = 600 GB
|
||||
+ система 50 GB = 650 GB
|
||||
|
||||
Итого: диск на 1 TB с запасом
|
||||
```
|
||||
|
||||
## Что нужно ДО начала установки
|
||||
|
||||
### 1. Домен
|
||||
Зарегистрированный домен с доступом к управлению DNS.
|
||||
|
||||
**Пример:** `example.ru`
|
||||
|
||||
### 2. Сервер
|
||||
VPS/Dedicated с Debian 12 и белым IP.
|
||||
|
||||
**Требования:**
|
||||
- Root-доступ
|
||||
- Чистая установка Debian 12
|
||||
- Статический IP
|
||||
- Обратная DNS (PTR) настроена на твое имя хоста
|
||||
|
||||
**Проверка PTR:**
|
||||
```bash
|
||||
host твой-IP
|
||||
# Должно вернуть: mail.example.ru
|
||||
```
|
||||
|
||||
### 3. DNS-записи (настроишь в процессе)
|
||||
|
||||
**A-запись:**
|
||||
```
|
||||
mail.example.ru. IN A твой-IP
|
||||
```
|
||||
|
||||
**MX-запись:**
|
||||
```
|
||||
example.ru. IN MX 10 mail.example.ru.
|
||||
```
|
||||
|
||||
**SPF-запись:**
|
||||
```
|
||||
example.ru. IN TXT "v=spf1 mx ~all"
|
||||
```
|
||||
|
||||
**DKIM и DMARC** - настроим позже.
|
||||
|
||||
### 4. Открытые порты
|
||||
|
||||
**Обязательно:**
|
||||
- `25 (SMTP)` - прием почты от других серверов
|
||||
- `587 (Submission)` - отправка от клиентов
|
||||
- `143 (IMAP)` - доступ к ящикам
|
||||
- `993 (IMAPS)` - IMAP с TLS
|
||||
- `80 (HTTP)` - для Let's Encrypt
|
||||
- `443 (HTTPS)` - для RoundCube
|
||||
|
||||
**Опционально:**
|
||||
- `22 (SSH)` - для администрирования
|
||||
- `19999 (Netdata)` - для мониторинга
|
||||
|
||||
### 5. Время
|
||||
|
||||
**Реально необходимое:**
|
||||
- Установка и базовая настройка: 4-6 часов
|
||||
- Отладка доставки в Gmail/Outlook: 2-4 часа
|
||||
- Настройка веб-интерфейса: 1-2 часа
|
||||
- Тестирование и доработка: 2-4 часа
|
||||
|
||||
**Итого:** Закладывай полноценные выходные.
|
||||
|
||||
## Проверка готовности
|
||||
|
||||
Прежде чем начинать, убедись:
|
||||
|
||||
```
|
||||
☐ Есть зарегистрированный домен
|
||||
☐ Есть доступ к управлению DNS
|
||||
☐ Есть VPS/Dedicated с Debian 12
|
||||
☐ Есть root-доступ к серверу
|
||||
☐ Настроен PTR для твоего IP
|
||||
☐ Открыты необходимые порты
|
||||
☐ Есть понимание, сколько времени займет
|
||||
☐ Есть план резервного копирования
|
||||
☐ Есть готовность разбираться в проблемах
|
||||
☐ Прочитал эту статью до конца
|
||||
```
|
||||
|
||||
Если все пункты отмечены - можешь начинать.
|
||||
|
||||
## Что дальше
|
||||
|
||||
В следующей части разберем установку и базовую настройку Postfix + Dovecot - сердца почтового сервера.
|
||||
|
||||
Ты получишь работающую систему приема и отправки почты. Без защиты, без веб-интерфейса, без красивостей - но работающую.
|
||||
|
||||
А потом будем навешивать остальное: антивирус, антиспам, шифрование, подписи и все остальное, что превращает голый сервер в production-ready решение.
|
||||
|
||||
Поехали.
|
||||
|
||||
---
|
||||
|
After Width: | Height: | Size: 149 KiB |
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"vk": {
|
||||
"icon": "vk",
|
||||
"title": "sharing.vk",
|
||||
"url": "http://vk.com/share.php?url={$url}&title={$titleVk}&description={$desc}&image={$image}&noparse=true"
|
||||
}
|
||||
}
|
||||
@@ -38,8 +38,8 @@ author:
|
||||
byline_title: "Автор"
|
||||
|
||||
code:
|
||||
copy: "Копировать"
|
||||
copied: "Скопировано"
|
||||
copy: "Копи."
|
||||
copied: "ОК"
|
||||
|
||||
error:
|
||||
404_title: "Страница не найдена: в замешательстве:"
|
||||
|
||||
@@ -1,14 +1,3 @@
|
||||
<!-- VK Comments Widget -->
|
||||
<div class="vk-comments-wrap">
|
||||
<div id="vk_comments"></div>
|
||||
</div>
|
||||
<script type="text/javascript" src="https://vk.com/js/api/openapi.js?169"></script>
|
||||
<script type="text/javascript">
|
||||
VK.init({apiId: 54459311, onlyWidgets: true});
|
||||
VK.Widgets.Comments("vk_comments", {
|
||||
limit: 10,
|
||||
attach: "*",
|
||||
autoPublish: 0,
|
||||
pageUrl: "{{ .Permalink }}"
|
||||
});
|
||||
</script>
|
||||
<!-- Comments Widget -->
|
||||
<script defer src="https://comments.oakazanin.ru/comentario.js"></script>
|
||||
<comentario-comments no-fonts="true" lang="ru" theme="light"></comentario-comments>
|
||||
@@ -11,3 +11,7 @@
|
||||
</script>
|
||||
<noscript><div><img src="https://mc.yandex.ru/watch/106995329" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
||||
<!-- /Yandex.Metrika counter -->
|
||||
|
||||
<!-- Dzen.Metrika counter -->
|
||||
<meta name="zen-verification" content="GDc1yxlsRy03jcI1t8NJ8OTleUE3f4I4PEmfOBjz4KBCezoDX1WWctO3l8Z2HGEx" />
|
||||
<!-- /Dzen.Metrika counter -->
|
||||
@@ -0,0 +1,6 @@
|
||||
{{- $icon := resources.Get (print "icons/" . ".svg") -}}
|
||||
{{- if $icon -}}
|
||||
<span class="relative block icon">
|
||||
{{- $icon.Content | safeHTML -}}
|
||||
</span>
|
||||
{{- end -}}
|
||||