diff --git a/chapters/devopsGiteaFoundation.tex b/chapters/devopsGiteaFoundation.tex index 1c14312..1a0fcad 100644 --- a/chapters/devopsGiteaFoundation.tex +++ b/chapters/devopsGiteaFoundation.tex @@ -1,25 +1,19 @@ \chapter{Startup Foundation with Gitea} \ChapterLabel{GiteaFoundation} -\index{Gitea}\index{SQLite}\index{Docker Compose}\index{Elastic IP}\index{SSH tunnel} +\index{Gitea}\index{SQLite}\index{Docker Compose}\index{Elastic IP} -This chapter records the initial Inviscid work hub on the existing Amazon EC2 -server. AWS is used in place of the assignment's DigitalOcean deployment. -The dated screenshots record completed work; pending items below are not -represented as passed. This draft covers Victor's deployment and access setup. +This chapter records the Inviscid work hub on the existing Amazon EC2 server. +AWS is used in place of the assignment's DigitalOcean deployment. Victor +completed the domain, DNS, Gitea deployment, and account setup; Payton completed +HTTPS, the firewall, persistence testing, and the remaining workflow evidence. \section{Student Pack Domain and DNS} -Victor submitted current school-enrollment evidence to GitHub Education on -October 12, 2026. Academic status was approved that day, while partner benefits -remained in ``Awaiting Benefits'' status with a stated 72-hour activation delay -(Figure~\ref{Figure:GiteaBenefits}). On October 15 the benefits became available, -and Victor redeemed Name.com's GitHub Student Developer Pack offer. - -The offer covered selected extensions rather than the preferred -\texttt{inviscid.net}. Victor selected \texttt{inviscid.dev}, registered it for -one year, and supplied the completed order and domain-management evidence. -The checkout total was \$0.00, with the free Domain Safe option; Domain Safe Pro -and optional hosting/email upgrades were not selected. No registrar-provided -SSL product is needed for the planned Caddy deployment. +After GitHub Education approved Victor's Student Developer Pack application on +October 12, partner benefits activated on October 15. Victor then redeemed +Name.com's Student Pack offer. It covered selected extensions rather than the +preferred \texttt{inviscid.net}, so he registered \texttt{inviscid.dev} for one +year. No registrar-sold SSL product was needed, because Caddy obtains the +certificate (Table~\ref{Table:GiteaCertOptions}). \begin{tabularx}{\linewidth}{L{3.8cm}Y} \caption{Student Domain and DNS Record\label{Table:GiteaDomain}}\\ @@ -28,69 +22,37 @@ Provider and eligibility & Name.com, redeemed through the activated GitHub Stude Registered domain & \texttt{inviscid.dev}\\ Registration date / term & October 15, 2026 / one year\\ Initial charge & \$0.00\\ -Displayed renewal price & \$22.99; verify current price before renewal\\ -Displayed renewal date & October 15, 2027\\ -Account owner / renewal responsibility & Victor; keep payment/contact details current and track renewal before the anniversary\\ -Renewal and protection settings & Automatic renewal ON; WHOIS privacy ON; transfer lock enabled\\ -Gitea hostname & \texttt{git.inviscid.dev}\\ -DNS hosting & Name.com's DNS management; exact authoritative nameserver values not yet captured\\ -DNS record & A, host \texttt{git}, answer \texttt{100.49.80.94}, TTL 300 seconds\\ -Intended public service URL & \url{https://git.inviscid.dev/}; DNS verified, HTTPS still pending in this record\\ +Displayed renewal price & \$22.99; verify the current price before renewal\\ +Renewal date & October 15, 2027\\ +Renewal controls & Automatic renewal ON; WHOIS privacy ON; transfer lock enabled; Victor tracks renewal and keeps payment details current\\ +DNS record & A, host \texttt{git}, answer \texttt{100.49.80.94} (Elastic IP), TTL 300 seconds\\ +Public service URL & \url{https://git.inviscid.dev/}\\ \bottomrule \end{tabularx} -Victor manages registration and renewal; the registrar's automatic-renewal -setting and October 2027 anniversary provide the recorded renewal controls. -Domain registration and DNS are independent of the class AWS account hosting -the application. Prices can change, so the table records the displayed price, -not a guarantee of the eventual renewal charge. - -\begin{figure}[htbp] -\centering -\includegraphics[width=\linewidth]{figures/gitea/student-pack-approved-awaiting-benefits.png} -\caption{October 12: academic approval preceded partner-benefit activation. The delay was resolved before domain registration on October 15.} -\label{Figure:GiteaBenefits} -\end{figure} -\begin{figure}[htbp] -\centering -\includegraphics[width=0.62\linewidth,height=0.60\textheight,keepaspectratio]{figures/gitea/student-pack-domain-offer.png} -\caption{October 15: Name.com Student Pack offer for selected domain extensions.} -\label{Figure:GiteaDomainOffer} -\end{figure} \begin{figure}[htbp] \centering \includegraphics[width=\linewidth]{figures/gitea/domain-free-checkout.png} -\caption{October 15: one-year inviscid.dev registration at a checkout total of zero, with paid Domain Safe Pro unselected.} +\caption{October 15: one-year inviscid.dev registration at a checkout total of zero.} \label{Figure:GiteaDomainCheckout} \end{figure} \begin{figure}[htbp] \centering \includegraphics[width=\linewidth]{figures/gitea/domain-renewal-details.png} -\caption{October 15: registered domain, October 15, 2027 renewal date, displayed renewal price, auto-renewal, privacy, and transfer-lock settings.} +\caption{October 15: registered domain, renewal date and price, auto-renewal, privacy, and transfer-lock settings.} \label{Figure:GiteaDomainRenewal} \end{figure} -In Name.com's Manage DNS page, Victor added an A record with host -\texttt{git}; the interface appended \texttt{.inviscid.dev}. The answer is the -EC2 Elastic IP rather than the old auto-assigned address. The saved record -and creation date are visible in Figure~\ref{Figure:GiteaDNSRecord}. +Victor added the A record in Name.com's DNS manager +(Figure~\ref{Figure:GiteaDNSRecord}) and verified it from outside EC2 at 19:17 EDT +on October 15: both Cloudflare (1.1.1.1) and Google (8.8.8.8) resolvers returned +\texttt{100.49.80.94} (Figure~\ref{Figure:GiteaDNSLookup}). \begin{figure}[htbp] \centering \includegraphics[width=\linewidth]{figures/gitea/dns-a-record.png} \caption{October 15: saved A record for git.inviscid.dev pointing to 100.49.80.94 with a 300-second TTL.} \label{Figure:GiteaDNSRecord} \end{figure} - -Victor independently verified the record from Windows PowerShell, outside -EC2, using two public DNS resolvers: -\begin{minted}{powershell} -Get-Date -Format o -Resolve-DnsName git.inviscid.dev -Type A -Server 1.1.1.1 -Resolve-DnsName git.inviscid.dev -Type A -Server 8.8.8.8 -\end{minted} -The supplied timestamp is October 15, 2026 at 19:17:31, UTC offset -04:00. -Both resolvers returned an A answer of \texttt{100.49.80.94}, TTL 300. -This verifies public name resolution, not application availability or TLS. \begin{figure}[htbp] \centering \includegraphics[width=\linewidth]{figures/gitea/dns-independent-lookups.png} @@ -99,18 +61,19 @@ This verifies public name resolution, not application availability or TLS. \end{figure} \section{Pinned Gitea Deployment} -The existing \texttt{ssw590-group-web} instance runs Ubuntu 26.04 LTS on a -\texttt{t3.micro} in \texttt{us-east-1}. The initial resource check recorded -908 MiB total memory, 468 MiB available memory, and 3.1 GiB available root disk. -Docker Compose reported version 5.6.0. The existing Color Buttons container -continued to serve host port 80 while Gitea was initialized privately. +Gitea runs on the existing \texttt{ssw590-group-web} instance (Ubuntu 26.04 LTS, +\texttt{t3.micro}, \texttt{us-east-1}), whose Elastic IP keeps the DNS record +valid across restarts. Victor deployed Gitea privately on October 12. On +October 15, Payton added Caddy as the HTTPS endpoint and recreated the existing +Color Buttons container without a published port on \texttt{inviscid\_services}, +so that Caddy could take port 80; the recreate command is in the deployment +README. \begin{tabularx}{\linewidth}{L{3.5cm}Y} \caption{Recorded Gitea Deployment\label{Table:GiteaDeployment}}\\ \toprule \textbf{Setting} & \textbf{Value}\\ \midrule Instance & \texttt{i-025a57432604c8d4b}, \texttt{ssw590-group-web}\\ -Current public address & Elastic IP \texttt{100.49.80.94}\\ -Private address & \texttt{172.31.19.91}\\ +Public / private address & Elastic IP \texttt{100.49.80.94} / \texttt{172.31.19.91}\\ Official image & \texttt{docker.gitea.com/gitea:28.1.0}\\ Container & \texttt{inviscid-gitea-1}\\ Remote working directory & \path{/home/ubuntu/gitea}\\ @@ -118,60 +81,39 @@ Persistent volume & \texttt{inviscid\_gitea\_data}, mounted at \path{/data}\\ Docker network & \texttt{inviscid\_services}\\ Restart policy & \texttt{unless-stopped}\\ Published backend & \texttt{127.0.0.1:3000:3000}; no public backend mapping\\ -Temporary public URL setting & \url{http://localhost:3000/}; SSH-tunnel access only\\ +Public URL setting & \url{https://git.inviscid.dev/}\\ +TLS proxy & \texttt{caddy:2.10.2}, container \texttt{inviscid-caddy-1}; the only service publishing host ports 80 and 443\\ \bottomrule \end{tabularx} -The official Docker installation guide supports persistent application storage -and SQLite initialization. We pinned a version rather than using a floating -\texttt{latest} tag. The recorded repository digest is: +Both images are pinned to version tags rather than \texttt{latest}. The recorded +Gitea repository digest is: \begin{minted}[fontsize=\scriptsize]{text} docker.gitea.com/gitea@sha256:f505a0d3a41323b786e05942fc7a4075e07ae7c169fd9485649aa55ee543a21a \end{minted} The non-secret configuration is maintained in -\href{https://github.com/byterand/SSW590-DevOps/tree/main/docker/gitea}{the team's versioned deployment directory}. -The following file is the initial localhost-only configuration. Its URL settings -must be updated for the registered HTTPS hostname before final submission. +\href{https://git.inviscid.dev/Inviscid/SSW590-DevOps/src/branch/main/docker/gitea}{the team's versioned deployment directory} +in the Gitea repository. The deployed Compose file and Caddyfile follow. \inputminted{yaml}{docker/gitea/compose.yaml} +\inputminted{text}{docker/gitea/Caddyfile} -Victor copied the directory from the Windows workstation to EC2 using -\texttt{scp}. In the remote directory, he validated, pulled, and started it: +To repeat the installation, copy the directory to the server and run, from it: \begin{minted}{bash} cd ~/gitea sudo docker compose config --quiet sudo docker compose pull sudo docker compose up -d sudo docker compose ps -sudo docker compose logs --tail=40 gitea \end{minted} -Validation returned without an error, the pinned image pulled, and Compose -created the volume, network, and container. Startup logs showed Gitea 28.1.0 -listening internally on port 3000 and preparing its installation page. - +Validation returned without an error and the pinned image pulled +(Figure~\ref{Figure:GiteaPull}). \begin{figure}[htbp] \centering \includegraphics[width=\linewidth]{figures/gitea/compose-validation-pull.png} \caption{October 12: successful Compose validation and pull of the pinned official image.} \label{Figure:GiteaPull} \end{figure} -\begin{figure}[htbp] -\centering -\includegraphics[width=\linewidth]{figures/gitea/container-startup.png} -\caption{October 12: container startup and localhost port mapping. The image exposes an internal SSH port, but no host SSH mapping was published for Gitea.} -\label{Figure:GiteaStartup} -\end{figure} - -Victor associated the Elastic IP with the same instance, replacing the earlier -\texttt{54.221.175.174} address. The containers remained running. SSH and tunnel -commands were updated to use the new address; the data and private address did -not change. -\begin{figure}[htbp] -\centering -\includegraphics[width=\linewidth]{figures/gitea/elastic-ip-association.png} -\caption{October 12: Elastic IP 100.49.80.94 associated with the existing instance.} -\label{Figure:GiteaElasticIP} -\end{figure} \section{Database and Persistence} SQLite was selected for the small two-person deployment. It requires no @@ -179,199 +121,259 @@ separate database service or network port. The database is at \path{/data/gitea/gitea.db}, within the same persistent named volume that holds application configuration and repository data. The volume exists outside the disposable container layer. Generated configuration, database contents, and -secrets are not stored in the Git repository. +secrets are not stored in the Git repository. SQLite suits this team's size but +may not fit a larger, higher-concurrency deployment. -The following checks were executed on October 12: +\subsection{Container-Replacement Test} +On October 15 at 21:34 EDT, Payton replaced only the Gitea container, using +repository \texttt{Inviscid/SSW590-DevOps} and issue \#3 as the recorded data. +Volumes were not removed: \begin{minted}{bash} -sudo docker inspect inviscid-gitea-1 --format '{{range .Mounts}}{{println .Type .Name .Destination}}{{end}}' -sudo docker compose exec -T gitea ls -lh /data/gitea/gitea.db -sudo docker stats --no-stream -free -h -df -h / +sudo docker compose up -d --force-recreate --no-deps gitea \end{minted} -The mount check showed \texttt{inviscid\_gitea\_data} at \path{/data}; the -SQLite file existed and was approximately 2.1 MiB. Gitea used approximately -187 MiB RAM in that snapshot, with 367 MiB system memory and 2.8 GiB disk -available. These are single observations, not load-test results. - +The container ID changed from \texttt{915c6b6d88d2} to \texttt{82ca41970439}, +confirming that the container and its writable layer were replaced. The same +volume remained mounted at \path{/data}, the 2.2 MiB database file was still +present (Figure~\ref{Figure:GiteaPersistence}), and the repository and issue \#3 +were unchanged and usable (Figure~\ref{Figure:GiteaPersistenceIssue}). The data +survived because Compose reattached the named volume. This is a persistence +test, not a backup; see the Architecture and Operation section. \begin{figure}[htbp] \centering -\includegraphics[width=\linewidth]{figures/gitea/storage-resources.png} -\caption{October 12: named-volume mount, database file, and resource checks. These verify storage configuration, not survival of a container replacement.} -\label{Figure:GiteaStorage} +\includegraphics[width=\linewidth]{figures/gitea/persistencetest.png} +\caption{October 15: container ID, mount, replacement command, new container ID, and database file.} +\label{Figure:GiteaPersistence} +\end{figure} +\begin{figure}[htbp] +\centering +\begin{minipage}{0.49\linewidth}\centering +\includegraphics[width=\linewidth]{figures/gitea/beforeissue.png}\\ \small Before +\end{minipage}\hfill +\begin{minipage}{0.49\linewidth}\centering +\includegraphics[width=\linewidth]{figures/gitea/afterissue.png}\\ \small After +\end{minipage} +\caption{October 15: issue \#3 before and after the Gitea container replacement.} +\label{Figure:GiteaPersistenceIssue} \end{figure} - -\textbf{Pending---Payton:} record a repository and issue, replace only the -Gitea container without deleting its volume, and show that both remain usable. -Include identifiers, exact command, before/after evidence, and conclusion. -This required test is distinct from the optional backup/restore exercise. \section{HTTPS and Certificate Renewal} -\textbf{Pending---Payton:} compare two certificate options and justify one; -configure Caddy using \texttt{git.inviscid.dev}, persistent certificate state, and -private forwarding to \texttt{gitea:3000}; update Gitea's domain and public URL; -record issuer, expiry, redirect, renewal automation, and a successful renewal -test or dry run. The existing website's host-port-80 mapping must be resolved -before Caddy publishes that port. Public HTTPS is not claimed in this draft. +\index{Caddy}\index{Let's Encrypt} +\subsection{Certificate Options} +Both options below use Let's Encrypt, a free certificate authority that issues +90-day certificates through \gls{acme}. A paid or registrar-sold certificate was +rejected because it needs manual installation and renewal. -Initial access used an SSH tunnel from the workstation: -\begin{minted}{bash} -ssh -i /path/to/your-private-key -o ExitOnForwardFailure=yes -N -L 127.0.0.1:3000:127.0.0.1:3000 ubuntu@100.49.80.94 -\end{minted} -The browser opened \url{http://localhost:3000/}; transport between workstation -and server was encrypted by SSH, but this is not hostname-based HTTPS and does -not satisfy the final certificate requirement. Each workstation needs its own -tunnel. The \texttt{-N} option keeps the connection open without a remote shell. - -\section{Firewall and External Exposure} -The attached AWS security group is \texttt{ssw590-web-sg}, identifier -\texttt{sg-0a9eb6dcfc906e78b}. The October 15 screenshot records these IPv4 rules: -\begin{tabularx}{\linewidth}{L{2cm}L{4cm}Y} -\toprule \textbf{TCP port} & \textbf{Source} & \textbf{Purpose}\\ \midrule -22 & \texttt{100.8.28.98/32} & Victor's home SSH access\\ -22 & \texttt{155.246.151.38/32} & Victor's campus source; connection still timed out during the reported attempt\\ -22 & \texttt{108.35.228.38/32} & Payton's administrative SSH access\\ -80 & \texttt{0.0.0.0/0} & Existing HTTP website; final proxy redirect/validation behavior pending\\ -443 & \texttt{0.0.0.0/0} & Permitted for planned Caddy HTTPS; opening a rule does not verify a live HTTPS endpoint\\ +\begin{tabularx}{\linewidth}{L{2.8cm}YY} +\caption{Certificate Options\label{Table:GiteaCertOptions}}\\ +\toprule \textbf{Criterion} & \textbf{Caddy automatic HTTPS (selected)} & \textbf{certbot with nginx}\\ \midrule +Issuance and renewal & Built into the proxy; no extra job & Separate certbot client and renewal timer\\ +Configuration & One Caddyfile site block; HTTP-to-HTTPS redirect is automatic & nginx server blocks plus certbot plugin\\ +Containers & One proxy container & Proxy plus certbot, or certbot on the host\\ +Renewal test & No dry-run command; forced reissue used instead & \texttt{certbot renew --dry-run}\\ \bottomrule \end{tabularx} -No public IPv6 address was shown in the supplied EC2 summary. The provided -inbound-rule screenshot contains IPv4 rules only. Outbound rules, any other -attached security groups, and final external exposure require Payton's review. + +\Gls{caddy} was selected for fewer moving parts and secure defaults. Its main +drawback is the lack of a dry-run renewal command. + +\subsection{TLS Termination and Evidence} +\gls{tls} terminates at Caddy. Caddy forwards plain HTTP to \texttt{gitea:3000} +over the private Docker network \texttt{inviscid\_services}; that traffic never +leaves the instance. Port 80 is open for HTTP-to-HTTPS redirects and ACME +validation. + +Caddy obtained the Let's Encrypt certificate for \texttt{git.inviscid.dev} at +19:52 EDT on October 15; the current certificate is valid until January 13, 2027 +(90 days). From Payton's laptop, \texttt{http://git.inviscid.dev} returned +\texttt{308 Permanent Redirect} to \url{https://git.inviscid.dev/}, and the HTTPS +URL returned HTTP/2 200 (Figure~\ref{Figure:GiteaExternalTests}). The +\texttt{.dev} top-level domain is on browsers' HSTS preload list, so browsers +never request plain HTTP for this host; the redirect matters for non-browser +clients and was tested with \texttt{curl}. + +\subsection{Renewal Automation and Test} +Caddy renews managed certificates automatically in the background, by default +when about one third of the lifetime remains (around mid-December 2026 for +this certificate). No cron job or manual command is required. Certificate and +ACME account state persist in \texttt{inviscid\_caddy\_data}, so replacing the +Caddy container does not force reissuance. + +Because Caddy has no dry-run command, Payton forced a reissue by deleting the +stored certificate and restarting Caddy. This exercises the same unattended +ACME path that renewal uses: +\begin{minted}{bash} +sudo docker compose exec caddy rm -rf \ + /data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/git.inviscid.dev +sudo docker compose restart caddy +sudo docker compose logs --since 1m caddy | grep -i obtained +\end{minted} +Caddy logged \texttt{certificate obtained successfully} without manual +intervention, and the serial changed from \texttt{06ADFF\ldots F6A7} to +\texttt{06CA6A\ldots 034F} with a new validity window +(Figure~\ref{Figure:GiteaRenewal}). This was a forced reissue, not an observed +natural renewal, and was run once to stay within Let's Encrypt's +duplicate-certificate limit. +\begin{figure}[htbp] +\centering +\includegraphics[width=\linewidth]{figures/gitea/caddy.png} +\caption{October 15: certificate serial and validity before and after the forced reissue, with Caddy's log entry.} +\label{Figure:GiteaRenewal} +\end{figure} + +\section{Firewall and External Exposure} +The only security group attached to the instance is \texttt{ssw590-web-sg} +(\texttt{sg-0a9eb6dcfc906e78b}); Figure~\ref{Figure:GiteaArchitecture} shows the +resulting network paths. Team source addresses are partially redacted. +\begin{tabularx}{\linewidth}{L{1.6cm}L{3.4cm}Y} +\toprule \textbf{Inbound TCP} & \textbf{Source} & \textbf{Purpose}\\ \midrule +443 & \texttt{0.0.0.0/0} & HTTPS to Caddy for \texttt{git.inviscid.dev}\\ +80 & \texttt{0.0.0.0/0} & HTTP-to-HTTPS redirect, ACME validation, Color Buttons by IP\\ +22 & \texttt{100.8.x.x/32} & Victor, home\\ +22 & \texttt{155.246.x.x/32} & Victor, campus\\ +22 & \texttt{108.35.x.x/32} & Payton\\ +\bottomrule +\end{tabularx} + +Outbound allows all traffic to \texttt{0.0.0.0/0}. The server needs outbound +access for package and image downloads, DNS, time synchronization, and Let's +Encrypt; the group is stateful, so replies to allowed inbound connections are +permitted. The instance has no IPv6 address, so there are no IPv6 rules. Docker +also binds ports 80 and 443 on IPv6, but they are unreachable without an IPv6 +address. The subnet's network ACL (\texttt{acl-06bdf6081fd8b9d5d}) is the default +allow-all in both directions, so the security group is the effective filter +(Figures~\ref{Figure:GiteaFirewall} and~\ref{Figure:GiteaNACL}). + +The Color Buttons app from an earlier assignment is still served over plain +HTTP by IP, which does not meet the hostname-based HTTPS rule. It is kept +because the instructor expects the instance and this app to stay reachable; it +no longer publishes its own port, and giving it an HTTPS hostname is future work. \begin{figure}[htbp] \centering -\includegraphics[width=\linewidth]{figures/gitea/security-group-team-access.png} -\caption{October 15: saved security-group rules, including Payton's source-specific SSH rule. This is configuration evidence, not an external port scan.} +\includegraphics[width=\linewidth]{figures/gitea/security.png} +\caption{October 15: the single attached security group with final inbound and outbound rules.} \label{Figure:GiteaFirewall} \end{figure} +\begin{figure}[htbp] +\centering +\includegraphics[width=\linewidth]{figures/gitea/subnets.png} +\caption{October 15: default network ACL on the instance's subnet.} +\label{Figure:GiteaNACL} +\end{figure} -During an October 13 campus connection attempt, SSH timed out. Victor's public -IP had changed from the home address; a campus-specific rule was added, but -that attempt still failed. Campus filtering was considered but not established. -On October 15 Victor reported successful SSH access; the exact network for -that successful session was not recorded. Rules should track current approved -source addresses rather than permitting public administrative SSH. +\subsection{Host and Container Exposure} +Docker-published ports can bypass host firewall (ufw) rules, so the security +group is the public boundary. Figure~\ref{Figure:GiteaPorts} shows that only +\texttt{docker-proxy} (Caddy) listens on 80 and 443 and \texttt{sshd} on 22. +Gitea's port 3000 listens on \texttt{127.0.0.1} only, and the Color Buttons +container and Caddy's admin port 2019 have no host mapping. +\begin{figure}[htbp] +\centering +\includegraphics[width=\linewidth]{figures/gitea/ports.png} +\caption{October 15: Docker port mappings and listening sockets on EC2.} +\label{Figure:GiteaPorts} +\end{figure} -\textbf{Pending---Payton:} externally test 443, 80, restricted administrative -SSH, backend 3000, and database exposure; record source, time, expected and -actual results, and corrections. Document outbound rules and final Docker -publishing. HTTPS encrypts an allowed connection; it does not close unrelated -ports. Local HTTP 200 and loopback mappings do not independently prove public -firewall behavior. +\subsection{External Exposure Test} +Tests ran from Payton's laptop on October 15, outside EC2, using +\texttt{nc -vz -w 5} and \texttt{curl}: +\begin{tabularx}{\linewidth}{L{1.5cm}L{3.3cm}L{2.6cm}Y} +\toprule \textbf{Port} & \textbf{Source (EDT)} & \textbf{Expected} & \textbf{Actual}\\ \midrule +443 & \texttt{108.35.x.x}, 21:53 & open & connected; HTTP/2 200\\ +80 & \texttt{108.35.x.x}, 21:53 & open, redirect & connected; 308 to HTTPS; IP returns 200 (Color Buttons)\\ +22 & \texttt{108.35.x.x}, 21:53 & open (allowed) & connected\\ +3000 & \texttt{108.35.x.x}, 21:53 & blocked & timed out\\ +222 & \texttt{108.35.x.x}, 21:53 & blocked & timed out\\ +2019 & \texttt{108.35.x.x}, 21:53 & blocked & timed out\\ +22 & \texttt{174.206.x.x} (phone hotspot), 21:53--21:56 & blocked & timed out\\ +\bottomrule +\end{tabularx} +All results matched expectations, so no corrections were needed. The laptop was +switched to the hotspot before the final SSH test; \texttt{checkip} afterwards +confirmed the hotspot address. Timeouts, rather than refusals, show that the +security group drops the traffic before it reaches the instance. +\begin{figure}[htbp] +\centering +\includegraphics[width=\linewidth]{figures/gitea/laptop.png} +\caption{October 15: external port, redirect, and HTTPS tests from an allowed source and from a non-allowed hotspot.} +\label{Figure:GiteaExternalTests} +\end{figure} + +HTTPS encrypts connections that the firewall admits; it does not close any +port. Without the security group, Gitea's port, SSH, or any accidentally +published container port could be reached directly. \section{Startup Team Workflow} Victor initialized Gitea with site title \texttt{Inviscid}, created the -\texttt{victor} administrator, and disabled public self-registration. A separate -\texttt{payton} regular account was created with a temporary password and -mandatory password change. A shared Gitea account is not used. -\begin{figure}[htbp] -\centering -\includegraphics[width=\linewidth]{figures/gitea/member-accounts.png} -\caption{October 12: separate victor and payton accounts; victor has the site-administrator role. This does not yet establish organization permissions or successful Payton login.} -\label{Figure:GiteaMembers} -\end{figure} - -On October 15 Victor added Payton's public SSH key to the remote Ubuntu -account's authorized keys, preserving the existing key and using directory/file -permissions 700/600. His source IP was allowed separately. Victor subsequently -reported that Payton connected over SSH successfully and subsequently confirmed -that his Gitea account worked. His password-change completion was not separately -captured. Both currently use the Linux \texttt{ubuntu} account with separate -keys, so Linux account-level attribution remains shared. +\texttt{victor} site administrator, and disabled public self-registration. He +created a separate regular \texttt{payton} account with a temporary password, +which Payton changed after first login; no shared Gitea account is used. \subsection{Organization, Repository, and Roles} -Victor took responsibility for the Gitea organization/repository work while -Payton focused on the other infrastructure tasks. Victor created organization -\texttt{Inviscid} and added \texttt{payton} to its Owners team. Both -\texttt{victor} and \texttt{payton} therefore administer the organization and -its repositories; Payton's account is not a site-wide administrator. - +Victor created organization \texttt{Inviscid} and added \texttt{payton} to its +Owners team, so both members administer the organization and its repositories +(Figure~\ref{Figure:GiteaOwners}). Victor leads domain, DNS, and deployment +work; Payton leads HTTPS, firewall, persistence, and repository workflow. +Victor then pushed the existing project history to the product repository, +\texttt{Inviscid/SSW590-DevOps}. Its root \texttt{README.md}, added in PR 7, +describes the product, repository layout, team roles, and contribution workflow +(Figure~\ref{Figure:GiteaReadme}). The organization and repository are at +\url{https://git.inviscid.dev/Inviscid} and +\url{https://git.inviscid.dev/Inviscid/SSW590-DevOps}. \begin{figure}[htbp] \centering \includegraphics[width=\linewidth]{figures/gitea/organization-owners.png} \caption{October 15: victor and payton are members of the Inviscid Owners team.} \label{Figure:GiteaOwners} \end{figure} - -Victor created \texttt{Inviscid/SSW590-DevOps} without initializing an unrelated -README commit, then pushed the existing project's \texttt{main} history. -The repository page showed 13 commits, including the initial Gitea deployment -commit \texttt{2807081}. GitHub remains the \texttt{origin} remote; the new -\texttt{gitea} remote points to the tunneled service. The observed commands were: -\begin{minted}{bash} -git remote add gitea http://localhost:3000/Inviscid/SSW590-DevOps.git -git push gitea main:main -\end{minted} - -The intended final organization and repository links are -\url{https://git.inviscid.dev/Inviscid} and -\url{https://git.inviscid.dev/Inviscid/SSW590-DevOps}. -At the recorded checkpoint, access used the equivalent paths at -\url{http://localhost:3000/} through an SSH tunnel; the final HTTPS links are -not yet verified. These links must be checked before submission. - \begin{figure}[htbp] \centering -\includegraphics[width=\linewidth]{figures/gitea/repository-main-import.png} -\caption{October 15: existing project files and history imported into the organization repository on main.} -\label{Figure:GiteaRepository} +\includegraphics[width=\linewidth]{figures/gitea/readme.png} +\caption{October 15: product repository README.} +\label{Figure:GiteaReadme} \end{figure} -\subsection{Issue 1 and Documentation Pull Request 2} -Victor created issue \#1, ``Document the initial Gitea deployment and team -access,'' assigned it to himself, and recorded acceptance criteria covering -deployment facts, evidence, manual controls, pending work, and Overleaf review. -Victor reported that he had uploaded the draft to Overleaf, compiled it, and -visually reviewed it. This is author-reported validation of the earlier draft, -not verification of later edits or the final combined submission. +\subsection{Backlog} +The backlog contains five actionable issues, each with acceptance criteria: +\begin{tabularx}{\linewidth}{L{0.6cm}YL{2.1cm}L{2.5cm}} +\toprule \textbf{\#} & \textbf{Issue} & \textbf{Type} & \textbf{Status}\\ \midrule +1 & Document the initial Gitea deployment and team access & Operations & Closed (PR 2)\\ +3 & Finalize README.md (product and deployment READMEs) & Product & Closed (PR 7)\\ +4 & Enable HTTPS for git.inviscid.dev & Operations & Closed (PR 7)\\ +5 & Configure EC2 firewall & Operations & Closed after tests\\ +6 & Version the Color Buttons app in the repository & Product & Open\\ +\bottomrule +\end{tabularx} +\subsection{Issue-Linked Pull Requests} +Gitea numbers issues and pull requests from one sequence, so the two PRs are +2 and 7. + +\paragraph{PR 2 (Victor, reviewed by Payton).} Victor documented the initial +deployment on branch \texttt{codex/gitea-foundation-docs} and opened PR 2 with +\texttt{Closes \#1}, requesting Payton's review. Payton reviewed the changes, +approved, and merged the PR, which closed issue 1 +(Figure~\ref{Figure:GiteaPR2}). \begin{figure}[htbp] \centering -\includegraphics[width=\linewidth]{figures/gitea/issue-1-documentation.png} -\caption{October 15: open issue 1, acceptance criteria, and Victor's assignment. Checked criteria do not mean the issue has been merged or closed.} -\label{Figure:GiteaIssueOne} +\includegraphics[width=\linewidth]{figures/gitea/pr2merge.png} +\caption{October 15: PR 2 approved by Payton and merged, closing issue 1.} +\label{Figure:GiteaPR2} \end{figure} -The changes were made on \texttt{codex/gitea-foundation-docs}, staged and -checked, then committed as \texttt{da6a369} and pushed: -\begin{minted}{bash} -git diff --cached --check -git diff --cached --stat -git commit -m "Document initial Gitea deployment and team access; refs #1" -git push -u gitea codex/gitea-foundation-docs -\end{minted} -The commit recorded 12 changed files, 342 insertions, and 32 deletions. -Victor opened PR \#2 targeting \texttt{main}, included \texttt{Closes \#1} -in its description, and requested review from \texttt{payton}. Gitea uses a -shared numbering sequence for issues and PRs: PR \#2 is the first PR, not a -second completed workflow. - +\paragraph{PR 7 (Payton, reviewed by Victor).} Payton authored PR 7 from +\texttt{payton/caddy-https}. It adds the root README, the Caddy service and +Caddyfile, the HTTPS URL settings, and the updated deployment README, with +\texttt{Closes \#3} and \texttt{Closes \#4}. Victor reviewed and approved it, +and Payton merged it as commit \texttt{1d7c528795}, which closed both issues +(Figure~\ref{Figure:GiteaPR7}). The initial \texttt{main} push is not counted +as a reviewed merge. \begin{figure}[htbp] \centering -\includegraphics[width=\linewidth]{figures/gitea/pr-2-review-request.png} -\caption{October 15: open PR 2 targets main, links issue 1, and requests Payton's review. Approval and merge are not shown.} -\label{Figure:GiteaPROpen} +\includegraphics[width=\linewidth]{figures/gitea/mypr.png} +\caption{October 15: PR 7 linked to issues 3 and 4, approved by Victor, and merged.} +\label{Figure:GiteaPR7} \end{figure} -\begin{figure}[htbp] -\centering -\includegraphics[width=\linewidth]{figures/gitea/pr-2-chapter-diff.png} -\caption{October 15: Files Changed view exposes the documentation additions for peer review.} -\label{Figure:GiteaPRDiff} -\end{figure} - -The intended final links are -\url{https://git.inviscid.dev/Inviscid/SSW590-DevOps/issues/1} and -\url{https://git.inviscid.dev/Inviscid/SSW590-DevOps/pulls/2}. -These were accessed through the SSH-tunnel hostname at this checkpoint. - -\textbf{Pending---Payton and both authors:} Payton's formal review, any -requested corrections, merge, and issue closure; a separate issue-linked PR -authored by Payton and reviewed by Victor; README and at least five actionable -issues, including product and operations work. Only issue 1 and open PR 2 are -fully evidenced here. The dashboard issue was proposed but its creation was -not confirmed. Payton has taken ownership of the remaining Gitea tasks. -The initial main push is not counted as a reviewed PR. \section{Architecture and Operation} Figure~\ref{Figure:GiteaArchitecture} shows the deployed request and data paths. @@ -382,42 +384,6 @@ Figure~\ref{Figure:GiteaArchitecture} shows the deployed request and data paths. \label{Figure:GiteaArchitecture} \end{figure} -The initial October 12 access path, before Caddy was deployed, is kept for -reference in Figure~\ref{Figure:GiteaInitialPath}. -\begin{figure}[htbp] -\centering -\fbox{\parbox{0.92\linewidth}{\centering -Workstation browser: localhost:3000\\ -$\downarrow$ local SSH tunnel\\ -AWS security group: approved source to TCP 22\\ -$\downarrow$ EC2: 100.49.80.94\\ -EC2 loopback:3000 to Gitea container:3000\\ -$\downarrow$ persistent named volume\\ -\texttt{inviscid\_gitea\_data}: repositories, SQLite, configuration}} -\caption{October 12: initial SSH-tunnel-only access path, before Caddy and hostname-based HTTPS were deployed.} -\label{Figure:GiteaInitialPath} -\end{figure} - -The completed local check \texttt{curl -I http://127.0.0.1:3000/} returned -HTTP 200. Logs, container status, and the image digest were collected without -printing configuration secrets. -\begin{figure}[htbp] -\centering -\includegraphics[width=\linewidth]{figures/gitea/http-image-digest.png} -\caption{October 12: local HTTP 200 and exact image digest.} -\label{Figure:GiteaHTTP} -\end{figure} - -The following are operational commands, not additional claimed test results: -\begin{minted}{bash} -cd ~/gitea -sudo docker compose ps -sudo docker compose logs --tail=40 gitea -sudo docker compose logs --tail=40 caddy -sudo docker compose restart gitea -sudo docker compose restart caddy -\end{minted} - \paragraph{Where data lives.} Repositories, attachments, Gitea configuration, and the SQLite database (\path{/data/gitea/gitea.db}) live in the named volume \texttt{inviscid\_gitea\_data}. Certificates and the ACME account live in @@ -429,49 +395,63 @@ layers. Deployment files are versioned in \path{docker/gitea/} of the repository TCP 80 (redirect to HTTPS, ACME validation, and the Color Buttons app by IP), both from any source. Restricted: TCP 22 (administrative SSH) from approved team addresses only. Private: Gitea's port 3000 (reachable only on the Compose network -and EC2 loopback), the Color Buttons container's port 3000, Caddy's admin port -2019, and the SQLite file, which has no network port. +and EC2 loopback, for SSH-tunnel administration), the Color Buttons container's +port 3000, Caddy's admin port 2019, and the SQLite file, which has no network port. -\paragraph{Troubleshooting.} If \url{https://git.inviscid.dev} fails, check in -order: (1) \texttt{docker compose ps} shows both services running; (2) the Caddy -log shows no certificate or upstream errors; (3) the Gitea log shows it listening -on port 3000; (4) \texttt{curl -I http://127.0.0.1:3000/} on EC2 returns 200, -which isolates Gitea from Caddy; (5) DNS still resolves to the Elastic IP and the -security group still admits 443. Restarting one service leaves its volume intact. +\paragraph{Restart and troubleshooting.} From \path{~/gitea} on EC2: +\begin{minted}{bash} +sudo docker compose ps +sudo docker compose logs --tail=40 caddy +sudo docker compose logs --tail=40 gitea +sudo docker compose restart gitea # or caddy; volumes are kept +\end{minted} +If \url{https://git.inviscid.dev} fails, check in order: (1) both services are +running; (2) the Caddy log shows no certificate or upstream errors; (3) the Gitea +log shows it listening on port 3000; (4) \texttt{curl -I http://127.0.0.1:3000/} +on EC2 returns 200, which isolates Gitea from Caddy; (5) DNS still resolves to +the Elastic IP and the security group still admits 443. \paragraph{Persistence test versus backup/restore.} The container-replacement test shows that data survives when the Gitea \emph{container} is discarded, because the named volume is reattached. It does not protect against loss of the volume, the EBS disk, or the instance, and it creates no copy elsewhere. A backup/restore test would copy the volume and database to separate storage and -prove recovery into a different instance; that optional exercise -(Section~2.8 of the assignment) was not performed. +prove recovery into a different instance; that optional exercise was not +performed. -Closing a workstation tunnel does not stop Gitea on EC2. Reconnect the tunnel -for browser access. Container replacement should retain the named volume; -never delete that volume as part of the persistence check. Before destructive -cleanup, retain needed data. Victor owns the class AWS environment; compute, +\paragraph{Costs and cleanup.} Victor owns the class AWS environment; compute, EBS, and public IPv4 are billable and covered by budget alerts rather than a spending cap. End-of-course cleanup must include the instance, unneeded storage, and release of the Elastic IP; stopping the instance alone is not full cleanup. +The domain renews automatically on October 15, 2027 unless auto-renewal is +turned off. \section{Manual Quality and Controls} -This revision adds the Gitea chapter, updates the Hosts inventory and Elastic IP -cleanup notes, and adds Gitea account and SSH-key handling hints to the Passwords -register. No passwords, private keys, database contents, or generated -\texttt{app.ini} are included. Existing security policies are distinguished -from observed deployment facts where necessary. Material AI help is recorded -in \ChapterRef{AIPromptLog}. The linked history entry identifies both table -updates. Optional backup/restore extra credit is outside this draft's scope. +This version updates the Hosts and Passwords tables, as recorded in the +document history. No passwords, private keys, database contents, or generated +\texttt{app.ini} are included, and team IP addresses are redacted. Material AI +help is recorded in \ChapterRef{AIPromptLog}. -\textbf{Pending before submission:} finish all pending sections, update the -final Compose/proxy configuration for \texttt{git.inviscid.dev}, complete the required team workflow, -verify external exposure and renewal, compile the combined PDF, inspect its -pages and links, and submit only the PDF. No deadline time was supplied for -the professor's extension to Thursday, October 15, 2026; check Canvas. +\paragraph{Failed attempts and corrective actions.} +\begin{itemize}[nosep] +\item Victor's SSH from campus timed out after his public IP changed; a +campus-specific rule was added, and his later access succeeded. +\item Payton's first SSH login failed (\texttt{Permission denied (publickey)}); +Victor authorized Payton's public key. +\item EC2 Instance Connect failed because the team's AWS role lacks +\texttt{ec2-instance-connect:SendSSHPublicKey}; the temporary security-group +rule added for it was removed immediately. +\item Caddy could not use port 80 while Color Buttons published it; the app was +moved behind Caddy on the private network. +\item The server's \texttt{compose.yaml} differed from the repository copy by a +stray Windows line ending; the file was rewritten with LF endings, and the +checksums now match. +\end{itemize} \paragraph{Technical references.} \href{https://docs.gitea.com/installation/install-with-docker/}{Gitea Docker installation}; \href{https://docs.gitea.com/administration/config-cheat-sheet/}{Gitea configuration}; +\href{https://caddyserver.com/docs/running\#docker-compose}{Caddy with Docker Compose}; +\href{https://caddyserver.com/docs/automatic-https}{Caddy automatic HTTPS}; \href{https://education.github.com/pack}{GitHub Student Developer Pack}; \href{https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/elastic-ip-addresses-eip.html}{AWS Elastic IP documentation}.