個人開発のiOS共通ライブラリをSSHフリー化した話 ― 「publicだから認証不要」の勘違いに、Dockerビルドだけが気づいていた
個人開発しているMinecraftサーバー監視アプリ「MineWatch」(公式サイト、アプリの全体像はこちら)のiOS側は、UIKitの共通部品を「YoLibrary」という別リポジトリに切り出し、Swift Package Managerで参照しています。このYoLibraryへの依存をSSH参照からSSHフリーな形に変えようとして、2回の挑戦と、思い込みが1日で覆る出来事がありました。この記事は、その経緯をまとめたものです。
TL;DR
- YoLibraryはSwiftPMのGitHub
url:参照(SSH)で入れていたが、CIノードを増やすたびに個人のSSH鍵を配る必要があり、SSHフリー化したかった。 -
1回目の挑戦(Swift Package Registry経由への統一)は、SwiftPM側の未解決の挙動で失敗した。同じパッケージが「レジストリ経由」と「ソースコントロール経由」で別物として認識され、ビルドが
multiple similar targetsで落ちる。 - 2回目の挑戦(GitHub→Forgejoへの自動ミラー、HTTPS参照)は成功したが、コード上のコメントには「ミラー先はpublicなので認証不要」と書いた。これは1日で覆った。ミラーは実はprivateで、認証が必要だった。
- さらに数日後、別の依存(Python製のRCONクライアント)をDockerビルドに組み込んだときに、同じ認証の問題が別の形で再発した。 開発機では
~/.netrcがこっそり効いていたため気づかず、隔離されたDockerビルドで初めて401として表面化した。 - 今もコード上のコメントには「public」という誤った記述が残っている。SSHを追放したつもりが、別の暗黙の認証依存に置き換わっていて、それに最初に気づいたのはDockerだった、という話。
1. 目的: CIノードを増やすたびに、個人のSSH鍵を配りたくない
YoLibraryは、GitHubの個人アカウント上にあるprivateリポジトリです。MineWatch側からSwiftPMで参照するとき、素のhttps://github.com/...だと別アカウント(業務用)として認証されてしまい弾かれるため、SSHのgithub-personalエイリアス経由で参照していました。これは動きますが、Jenkinsのビルドノードを増やすたびに、個人のSSH鍵をそのノードに配る必要があります。個人開発の鍵管理としては避けたい構成でした。
2. 1回目の挑戦: Swift Package Registry経由への統一
最初に試したのは、Nexus上に立てたSwift Package Registryに寄せる方法でした。YoLibraryのPackage.swiftで、依存先(swift-openapi-runtimeなど)をURLではなくレジストリの識別子(id:)で宣言し直し、Nexusのswift-hosted経由で解決させます。
ところが、MineWatch本体は同じswift-openapi-runtimeを通常のurl:参照で直接依存しています。この2つの参照方法が混在すると、ビルドが次のエラーで失敗しました。
multiple similar targets 'OpenAPIRuntime' appear in registry package
'apple.swift-openapi-runtime' and source control package 'swift-openapi-runtime'
SwiftPMが、レジストリ経由で解決したOpenAPIRuntimeと、ソースコントロール経由で解決した同名のOpenAPIRuntimeを、同一パッケージだと認識できていません。project.yml側のパッケージキー名をレジストリ識別子の文字列にそろえても再現したため、設定ミスではなく、SwiftPMのレジストリ変換機能側の未解決の挙動と判断しました。加えて、XcodeGen自体がレジストリ経由のリモート参照に対応していないという、より根本的な制約もありました。
この挑戦は同日中に撤退しています。YoLibrary側では、レジストリ識別子への変更コミットの直後に、通常のurl:参照へ戻す修正が入りました。
fix: 依存宣言をレジストリid:参照からurl:参照へ戻す (#8)
Nexus swift-hosted へのレジストリ経由解決を試みたが、これに依存する
MineWatch側の直接依存(apple.swift-openapi-runtime)と「似ているが別物」
と判定され、ビルドが multiple similar targets で失敗する問題が未解決
のため断念。通常のSCM参照(url:)に戻す。
この時点で、この課題は「優先度低・保留」として一度クローズしました。同じ内容のissueが2本(重複)立っていたことに気づいたのも、後で見返したときでした。実利は既存のSSH参照で取れているので、急ぐ理由が薄かったことが背景にあります。
3. 2回目の挑戦: GitHubからForgejoへの自動ミラー
数日後、別の作業(App Checkの導入)の副課題として、この件を再挑戦しました。今度はレジストリを使わず、GitHub本体を正本のまま、mainブランチとタグだけをForgejo(git.sk4869.info)へ自動ミラーし、.package(url:)のままHTTPSで参照する方式です。SwiftPM側の解決方法は変えないので、レジストリ変換のバグを踏みません。
YoLibrary:
url: https://git.sk4869.info/honoka4869/YoLibrary.git
from: 1.5.0
この変更を入れた直後のコード上のコメントには、こう書きました。
Forgejo側リポジトリはpublicなので、consumer側は認証情報なしでclone できる(GitHub本体をSSHで参照しないのは、consumer側のCIノードを増やすたびに個人のSSH鍵を配って回る必要が無いようにするため)。
SSH鍵の配布問題は解決したはずでした。
4. 1日で覆った前提: ミラーは実はprivateだった
ところが、この変更のわずか1日後、YoLibrary側のREADMEに次の修正が入りました。
-`url:` 形式のまま HTTPS で参照する形にした。Forgejo 側リポジトリは public
-なので、consumer 側は認証情報なしで clone できる(GitHub 本体を SSH で
-参照しないのは、consumer 側の CI ノードを増やすたびに個人の SSH 鍵を配って
-回る必要が無いようにするため)。
+`url:` 形式のまま HTTPS で参照する形にした。GitHub 本体を SSH で参照しない
+のは、consumer 側の CI ノードを増やすたびに個人の SSH 鍵を配って回る必要が
+無いようにするため。
+
+Forgejo 側リポジトリは private なので、consumer 側で読み取り専用の
+Personal Access Token を `.netrc` に設定しておく必要がある。
「public」という前提そのものが誤りで、実際はprivateでした。YoLibrary側のドキュメントはすぐに修正されましたが、MineWatch側のコード上のコメントは、この修正内容が反映されずに残りました。
5. 別の依存で、Dockerビルドだけが気づいた
この食い違いに気づかないまま数日が過ぎ、別の依存を追加したときに、同じ問題が違う形で再発しました。MineWatchのAPIは、Minecraftサーバーへのコマンド実行(RCON)に、自作のPythonクライアント(別リポジトリで開発・公開)を使っています。当初、この依存もgit+https直接参照(git.sk4869.info上のリポジトリ)で入れていました。
開発機やJenkinsのMacエージェントでは、この依存の解決は問題なく動いていました。理由は単純で、開発機の~/.netrcに、以前の作業で設定済みの認証情報が既に入っていたためです。ローカルのビルドやMac上のCIは、これを自動で使って認証を通していました。
問題が表面化したのは、APIのコンテナイメージをKanikoでビルドしたときです。Dockerのビルドコンテキストは隔離されているので、ホストの~/.netrcは届きません。この依存を取得するたびに、認証エラーで失敗するようになりました。
MineWatch側のapi/DockerfileがKanikoでビルドされる際、RUN --mount=type=secret
(BuildKit専用機能、Kaniko未実装)に依存してmc-rcon-pyのgit+https取得認証を
渡そうとして毎回401で失敗していた
修正としては、まずJenkins側のCredentialから認証情報を環境変数で渡す形を試みました。ただ、Kanikoの制約(BuildKit専用のRUN --mount=type=secretが使えない)を踏まえると、ビルドのたびに認証情報をやり取りする構成自体が壊れやすいと判断し、最終的にはこの依存をgit直接参照からNexusのpypi-hostedリポジトリへのpublishに切り替えました。バージョンタグを打てば、通常のパッケージ名・バージョン指定で解決できるようになり、Forgejoへの認証そのものが不要になっています。
6. 今も残っている食い違い
この一連の出来事を振り返って気づいたのは、「public」という誤った前提が、記事執筆時点でもMineWatch側のコードに残っていることです。ios/project.ymlのコメントは、今もこう書かれています。
# 自動ミラー(YoLibrary リポジトリの mirror-to-forgejo.yml、
# main + タグのみ)を使う形に変更。ミラー先は public なので
# consumer 側は認証情報なしで clone できる。
実際には、iOSのビルドはJenkinsの同じMacエージェント上で直接実行されるため、開発機と同じ~/.netrcが今も効いていて、動作上は問題が出ていません。「動いているから直さなくていい」まま、誤った記述だけが残っている状態です。
もう1つ、プロジェクトの規約をまとめたドキュメントには、今もこう書かれています。
YoLibrary は SSH の
github-personalエイリアス経由でバージョン固定参照(個人アカウントの鍵を使う。素の github.com だと弾かれる)。
こちらは、Forgejo移行前の古い参照方式の説明がそのまま残っている例です。実際の参照先は、project.ymlの通りHTTPSのForgejoミラーに変わっています。
まとめ
- SSH鍵の配布をやめる目的で入れたHTTPS参照は、「認証不要」ではなく「別の暗黙の認証(
~/.netrc)に依存する」形に変わっていた。当初の「public」という前提はその日のうちに誤りだと分かったが、直したのはドキュメントの一方だけだった。 - 同じ間違った前提は、開発機・Mac上のCIでは問題なく動く。環境が隔離された場所(Dockerビルド)だけが、暗黙の認証を持っていないので、初めて問題として表面化する。 「動いているから正しい」とは限らない、という点で、環境依存の落とし穴は共有インフラの落とし穴と似た形をしています。
- 対応の選択肢は、認証情報をきちんと配線する(Jenkins Credential経由)ことと、依存そのものを認証不要な経路(Nexusのようなパッケージレジストリ)に載せ替えることの2つがあり、今回は後者を選びました。認証を配線し続けるより、そもそも要らない設計にするほうが長期的には楽だと判断したためです。
- コメントやドキュメントの前提が変わったとき、影響範囲が複数リポジトリ・複数ファイルに散らばっていると、直し忘れが残ります。今回も、YoLibrary側のREADMEは1日で修正されましたが、MineWatch側の同じ主張を書いたコメントは、この記事を書くまで直っていませんでした。
個人開発の環境での構成なので、そのまま組織に持ち込む場合は、CIノードの認証情報の配布・ローテーションのポリシーを先に確認してください。
追記(2026-09-28)
この記事を書いた直後に、ios/project.ymlのコメントとCLAUDE.mdの食い違いを直しました。「ミラー先はpublic」という記述を削除し、「Forgejo側はprivateで、読み取り専用のPersonal Access Tokenを~/.netrcに設定する必要がある」という実態に合わせています。CLAUDE.md側も、Forgejo移行前の古い「SSHのgithub-personalエイリアス経由」という説明を、現在のHTTPSミラー参照の説明に更新しました。
記事を書くこと自体が、直っていない食い違いに気づくきっかけになった、という順番になりました。
JQITのエンジニアの95%以上は未経験からの採用です。
よければコーポレートサイトにも遊びに来てください。
未経験から学べます!一緒に挑戦していきましょう![]()
noteやXもやってます↓