diff --git a/ansible/group_vars/all/main.yml b/ansible/group_vars/all/main.yml index f77ae48..1d16bb4 100644 --- a/ansible/group_vars/all/main.yml +++ b/ansible/group_vars/all/main.yml @@ -48,6 +48,11 @@ subdomains: # DATUM Gateway dashboard (knots-box, proxied via vipy) datum_gateway: datum + # Monero restricted RPC (monerod-box, proxied via vipy). Not a dashboard: + # this is the JSON-RPC endpoint Feather points at. Caddy terminates TLS and + # monerod's own Digest auth does the authentication. + monero: monero + # Read by plays across several groups, so global rather than group_vars/. headscale_namespace: counter-net diff --git a/ansible/group_vars/all/vault.yml b/ansible/group_vars/all/vault.yml index 76eb7bf..9c3fb3e 100644 --- a/ansible/group_vars/all/vault.yml +++ b/ansible/group_vars/all/vault.yml @@ -1,164 +1,191 @@ $ANSIBLE_VAULT;1.1;AES256 -34383033613438623432656438313763613639313139626432303637383363643339613166626665 -6537663336353661373437303636613364643736663031300a623461353135623537343763383333 -37323731646262613436646231656263396532616132316237353762376363383062646132303435 -3435316666316235660a323163613932616661303935336633656332353766393536393736356539 -61396164386266303464373262373862363361363365363235383363613335663933313062663565 -39613466353161356462656539613335613536363532333431393935303430373435373737363537 -62383266323339373062313134316464303263313830646161333530383736336637333865373764 -31313165396366653763343466643561383236643836616662383439666165343139326232326364 -32633133373863616163343365656231353939366435346534383462396134653064663566343566 -66343464373865383864313264646635316233336133346365396634626261613561373965643339 -30353831393037316334313632376537393361336366326561343832373531303139323937613531 -34386164303430316162363536386137316330336638303365393462386465633862663932376563 -34323862636435306563333065383134343362333733623066656439613339376666353331373665 -32353938663364313036373237323062393430306661633732356434333232376338313835663330 -32306536393666323464663238306562613936376236613935616664653865373330626365626330 -64613066653632646638306338393331376430383165316363323437373362613866313962663432 -62356663636630356239613831353066303530363966396135376134633936313530643262353965 -38383330613962313935366633353635363638303937316362303866343433353437363334396431 -32393134626133353564643233616632376633303934653065613262353436333533376137303862 -35303861383833326537376433336634633730373833313036643365373431666438653033653930 -33346230396661636333613031353133353631306331666362386366633062666534303539316238 -39366634656333646634643032393738333330636230663366623265666533356534623465316665 -35336535326237343231653661613736626239316262386363666264613636326332366232333964 -38353635616662663133376534306366643037613732343233373336653166316330643638353539 -35353532643534613864333839393365313439343663333337373639633832383630316164306132 -63636162373034363839656638353534613733663737626432636436373164303239316436313566 -66623466396663353136323565643262383865323830616232373466373431373261306537646463 -61353966623062356438626635663461396165343366393132393965383734633632666335313030 -37333631373239303231396264393031373162373936303462363736313538343562343737356332 -63663762633032356130303964386664366137373432353533386566613037613163383963623732 -32343765376535343630656633333362393765666131653632613361363163383038323138333537 -37373333316466316562613439356365316163336366623435666166653835616531653563343664 -30303464663838646333373837363961356465613234636138633162366636633030383337643131 -62636236663066316630396132333934353139383465363034363033656161383663656462313939 -62353738396132343734343363636164323163616237613861643862303433633934656165343263 -35313737373333386335663731643664393630653839396464376639306231653934386336353630 -61393735336662663435613431386361373561363531643232303831363163636139653538393931 -66313965646562616237383439343637623835303065333730613865666638383131616261346463 -31396464306537613135653830653138323539393731626264326335336432666333333735646534 -30616439393464326631633635363466303336613135346231626232313361303666323661616638 -64363937343231363536353034363966326132333734386638613737636130646232363666643633 -61353563636466356630346232613761636432303430336461663434333636623962643764336639 -35303565613861323431393036303534303061666437366538306236373930313439353330313632 -61363363303562316664303065613863613339626632643931386438326330373938613762303334 -33316430363931393262373661623137633835656136313235613666613932313236343966306331 -36343031303632323432366337633637336335343638393564313738386162386164613339343662 -62383939663265373265613932633265626263623939303638383838646531343433393864363235 -37313761396436623833376435393537376136373162393465343764326533666533393061643965 -61306365346562333737313962633764313232323161623861336566343735353737633539656162 -34323939306661373662663331653333346264323930663636633134666532623438323537383165 -66343033363962633766303331623866306462623235373838616565653066646132633034363535 -31613462356361613535383963643662373162363363653334393937313039666266366363653537 -64356166396139393262313565303731346534646462333638316661383139346362333364613466 -38303561316263636136303431393366343936653161336238613439366266346136636465303337 -61373634376464333037303862616335623031656133636165636264386265643261373735663266 -39396265366138363430343035356337303438346165366361316230656239326633653537626339 -39613334316563376133343336343563363564653237633764393532336437303334363830396635 -31333963666462313830333163646465356337643263636462356363613266323630393866626535 -35336436663063333364353663616564626535396133386461326433323936323161613633353539 -66323935626433353162666335613839346561343264323763303034613366663233613037343538 -66376236373462383430313030373235373337653338333033306431616562353435656433356365 -37653063363435366532306334373438306632306435653334396630343863623938666335656164 -37386634366430383762316435396237313362313965353634613266616532633465646464313662 -35303433363236393465396161346431306231666432666531626136623966663864376663373562 -37653239633732346536323265643434616136326561666330633435383264333937353366653736 -61353430633363323263323163646639663330663632343038623964396433626337353232373334 -32343938356539303938666137323831646562393233323733336464316165623232326132316362 -61313631626234336164656533616231393663363364623130313439623466396231383537656562 -35393163323131383838356130643863633736613635306635306135666261623731613239303962 -61373432336638353339646665653561653738353731373032366361313562306466373365653333 -35306236336331333961613864346165613763383266613236343066623432303766333434333331 -66376263363132616132366165393235626336323665373135653462653533376138346632393363 -35396165383966326635333134663138366136336430393532373935373264616530613762386430 -37643862316232613261343430616438333831623835663239656531356666613032653837373531 -30386232643537306530323936386561646265616339376265353763663833623539653831656634 -63626564633964343663656435636635656562666236386639353332356566316631643431353330 -32643238623932643732353332633535643738663066383830376432346463393464333236386163 -64376165333266323166613564313061643832656239333165393035343836376434626162643061 -31373861393534636236383765313834356638613332626666653762343537363339383939666263 -61366230623930643534353565316165656132396664393138326139613536343738396330653735 -37623463343834626133313231393130646134363331393238663065643930383237326439343831 -32393536613361616637373761393361303936363863333838666633343063386235316237323531 -62633362316436623761616366383336613765373362393161303765613464643632666462343565 -63616430396164346263666436336463623638313130656231643131336663336565616135373433 -39326533643733333564373434333738343634383033353036646534646632383730613161393130 -62653836376435663435353739373536366466646332653030653332353761626466353631663163 -38613734383634663662376464653733306564313761336566336336656366653536643834653631 -35333963306562393234623039613338643266333762333236366331333431343830393864623830 -34323439386133613035363861623332663933313734313637623739613163663730316134303434 -35363239363836666463366334356435666632396132306163303037663430383535393934323537 -30353266373864346662333035393761646632313032383738346439343232663666343833333565 -35363564623038376666363436663830356165323234653230636264303066666230643632356237 -66633734346633366533623966393462313438333965623033343337323135353730613561363236 -30636333613034626162393137323762306432663761393766303163393536656338366664643432 -34636630323965396166356538623165663335333739323034333237306531393734306138343436 -36613937373932633234303336373530663533646635396664343935326334333239646534343336 -35396666353938326337646431353965653836323731313861613031356561376262363965623934 -65336164623134366365303062393161353965363937646363396564313138323938656330333964 -64663862363766613630333531623463616331363533343962653164376665303463336134346662 -66376238663632663932656230323638323135653666363736643065336330636236323064386361 -37646631303138616565383665663634333338613336346438366333636165366635313337373635 -62656236653961393061373262326534393339363236353336613339656462336661663739396261 -35363636653237316436656662633462306333313336313865333037636661383731336466666536 -36646537653431666261353232653233653930346538383438633466346636656432383963333765 -62346363633265306665366630383831303434336139343837616361666539656262633735363064 -30376138386531336430613563366666333964663231363465326230316233323532366632663930 -63653164373931353861653833326130643365373530663039363166366331303134366336613438 -63633336363539636265396331653630633162316366316130326531363463616464376635376538 -32376638353337343639633434613663666437313564333264383766623561393933333561663235 -37666163626538353736383435373530663438363636643665626266316338336532623235353337 -61653762383630616266353438386137653137616532633066623165383663646437343565366237 -33333463613539346136613135386466656135616136663036313661323738363066633038656139 -38353230313861666463383737303038613731316334333031643438343463666164633536313031 -39613663376436643165333362636462303230656336333234363336633262336363643736336231 -66316630613762383832613836333263303038306235663662323162633932653733626130356665 -65316630346534356261373961353130393334396365376237353439326261336339393336303030 -63323439666439313831626639323735653730356565643938376564316337613333616664643865 -30633530383865663731383635366566613565393632336337306334663937396135303539363536 -65656331386532306438383031313164626237393762303636663831666239336637666465633630 -63383961336333663537666561376532376631343132363638633364616531306363623432396332 -31343864626335613131653762363338393738343531626330663663376531336230656365363661 -64363530333965386439376161313534333435393966656231613161633034303964653564363436 -63396131623562303962383137666439373132633330363237353031306561306362666561376435 -66383535333233663736363762323662363264393365643135323834366565653932313863353437 -38363536323739336165356561613335653464363637636234313637383066633865353239393961 -31613532373361383062393238303430353662383930373764373237383035633364323661623661 -62653661623234366666616532663563316234663137616338393036333937633963323635366165 -61613032363531666338353132346365323661626265626461396362646338373536306362303338 -31646564343434633433663039366533656665643235336136333134366231376336343465336539 -37363139346239666232313366613432646265626564313838346364306133306434313337626562 -32346430316532623037633838303363346130623636386134313566383333613565316135373264 -38393261313435323433326130303032393538333963646430646366653363613830613534353832 -32643434613964613239363966376138376661636335373130356430336631333461623734383535 -35356461326136636234663738343738303064386536336462303632303461303733333666383036 -38383563393739343134363038623638393766316533373163336439336238613734656132623163 -32626564666430663339316163393934323133306238353562323866633738363737643937376435 -66306430623334643766623564363239346361393666663766306637313265313833396435626234 -35366239616132623863343365663833363934316362636638303536656631633364646235366566 -39386637613735373761643339396132323031323438316633363464336636316534643435323861 -62663732366430646538626663313035616235643537643234356434323635373962343633336266 -30323031306438656537626430356231393466313334633934623632663661306361313330633339 -33396234386536336331616161363132343765306562303932313963623037366633383765663134 -62636466636433666530363930633765626531613539363832313361666565323635326334326533 -36336439363836663033643262336139613437623633616138656564393032393263393132303031 -31626336663039633362366663306662333432643261613464303939326562653562346239383261 -37373966383635626433363831653936333964626262363839383936356634633233336365623765 -62313031353538636464303234383865613932323164386336316362323731303263346637346534 -32353734343130343364646161643431353230336364366330363261326334613936633234646264 -39623163346663613431323630303034383761663835373565663166366239303130386139396236 -62393565353761636636366462353038366664663430616332326465373364396264323064666536 -61653035633562316634336263636332363733663666626131383161656535383133613939623034 -66363632613733306336323165336534346337386163656631343332323636353539353737316131 -34616336343865656565646338653037333838663736333330376330663834373138363739633064 -61646439636365323838376131663266636235333062616532613936616339633661303634323531 -63356335386534353639323566376134373565353137333134363761666532366431633634333430 -39326662633765626230326130386333393463323433366162363432613234336634663439313339 -33653737333830336264343839396563316462613032376335356634383436353962613636333866 -32333032623635323430356636623138366635386533646133626164316438393937626462313239 -31636635363064363763396566306234363965346438653738333961623435303233396634643763 -65343763386464366636383734336335646464306262623363303934393734333666636635356365 -65353931303862343765343665373139376530656263356365366333323135383838383030613166 -30303963383833643937636539656164336537383337346436303534303035313935323633663765 -333464336235373230316337333632326138 +61616634393062353739616362333531616534666431633234333539376337313235366337346364 +3135643764366262633636353735613233343361363165390a303638313465643332363839326530 +66623032363563306135643838626435646639306438393638353133393731386139336661383461 +3965353830373536360a653364663734333762616630313563656335333933333566333334393837 +64636438386264316333633839623736333932356365643639376262353564363639303438373534 +37346139393038643332626337646430376165316133343362636235623838343637343062623431 +35333530376430643562656233356165313065633865343439363238663966366332346536363430 +37313261363434376438306333373161303465653132353864366562363235376630373262356633 +64393830643037346565303435633966303234666638653833353138346464383462383136313433 +38356663303231313134623862643762633762303364616433623963326463326232333662643839 +35346435643234663838643934376461393631666363333435623965376239353137613533383033 +34663662666362613661613833383166343965333939336631363633343332653064653761353063 +31356263396434386433393332353532373663396431313539343138623564393434373135616239 +39353163363063323064633935346134663165626265633136633833643538616662333663366664 +61323866313266626637356532336166643261343662636464346566613531626532663961613238 +65353434653466316438343631376332316566376438623835393438313239326339323564623564 +37313938396562623865646338336634333637666538656139646539376531656464653136663462 +30343537303230306331386139376261396162326437623265373336356163636562623932653562 +64353763646636363839613365386233316262356339653332323565383033613561356236666336 +39616636353530636434366430643239383164376139343064316430396431363363383166346130 +61373735313633313639323933646632393231633935326439316466623438393233623139626139 +38353333376164613765656164363436613831663333656239346661623031393238353765356233 +65336632633361356432326339613766633938336537306261633236333835646335356365393330 +38656236363864613862643365383265633134643338383033643434376530303462336564383066 +66663061646432333034316431353236323938623639303263393537313939373162633838343062 +32316230613033363931343162386538376435386139396634333061313930373335306663393938 +30643466303338316663613563636239343432363964653438666662356437623962653266346434 +64616136643835396430306633313736623736316634313466366538663437633437613963386231 +39666565366464326132643836366431653535343162336563356664346166313532393664353139 +39383037323234323038316632383766383266393663646438653038643566376133636237653733 +38623438343432323664303461373930363437356136333235393262336336633266396531663638 +30393335396537356635343730376532363261353039396639653966633033383037656536646161 +39653531303962656234343634346236373362616634313664613764373439643734306639386461 +38663364613365643030616430356139393362386438613838326236643063633538346262626133 +33666537353862353461393939656231666330633434316261656134363062353835663833356435 +62663037383266353037326431376438313165343231656366306265666464303132613133303662 +39646462333439633065356137633932336162333438356337663637353033326362313036303564 +38643662633531383539393064356535363631336266316334386465313965376665306163393664 +64643434326561613333353134633166613464393537323263656466613239343562653636353764 +36343365656636393031626231343033653764393361376532646139613166306561323364356364 +62623838316430616636323966616634343732353031393665356137393532383762363565663038 +35323930633933626464633966343865346466353465626466663435316137366435373734363661 +33663939366165313939643032396562326565343961376366313730333735336333353164623266 +30306261363230306562343433656133303662306563646166386531386439633534623632373932 +30623361346632346463333661366539613233383337353961383564303438643965636235396163 +63363264383762303631613033663039626539336566653062333632383534306230396139613234 +62633862643535383536353537393431376431303262323332613731636534643033303635613030 +61653234313732643563383639303633343266623761623435373734613235656263353864353434 +63353039386665623164626666626330623730616438373832303337633364633735613364623131 +35613433366361616239363464343937363566383934313863636231373434636331383939623566 +34623933373434346561633331653163323664393130336665393465303162396630376236653262 +39393465346632323435636333613361353539643238313239373231376436313631663065386262 +34663637386632396137353261613666316564333636326231636137613235623765313137306537 +38306161633731343838356635613038616363653833623634633061376364376534323532666337 +65366634623331323562323562613735356235393434323063303766653334363236613036353638 +35353438623930366336383062303462646338633939393639643832623730633562306338313939 +35666334336334653534646662316534376235656364356563316632363130356635333336666130 +34616639393834373362323930613863383833663161316339323465616639633531376232303332 +33333333643934623538633738643631386232643932303138383861313261306261323532393233 +61353431373538313762653839343861313337643633643032333765396466393831393162306436 +32393832363638333832666364646266376634663031333932333735643831623361323564386261 +61323937393762613531646564363963626461343262373839383938663661626562613837323133 +39646632316666396637646664356230356131313062363039656163613238386334636636636636 +38363463633733663132366239343766353038383962353639313562343636623261376566653635 +35326338663935386132333035373362326235643864336162303063623539656439306466663834 +38333362353630363835613464303131306333313865623236633362623832633062333565343762 +39336161376661646238313839633165666435346433336337633734353166636566326464666330 +37386536613430343139653134343963626466333734356461323631623761663639633532376133 +61363632303566336161653430613735316562656334323265303163383939323564356465636530 +61626262653461386332646566306666323434613162613033373730303265306231316630346239 +39343735363632623561373838316334393166303365363766663965306161643365656132653531 +32613662663362653366633237386362623962323230303538633732663061376334393666326535 +35653038306233626133363732393964623630383637363937363335663836346564636639383266 +32326433316236376363333163346234303938626635363464636563653032656361323138326530 +36636163356632613738626136376563656233313034666161323362313533366461326132316162 +61646335383662313234666332623932396632333435316466346538313630633038663231373764 +39343933613361336439636536356136383535336262613538323837633563633038306231663632 +33396366343466383538306638656234383930306539633865373031343030333765663735386438 +35363232616533656632396661386566643738353862646535323533323237636137646239353664 +32353261366334353933376436336533306531356462343535346664656435626466356662613032 +32656666336235326165653861326431653335653131663034386166373434303936646634366635 +38356536343664363836616235326465616131636132643331373366656164653638613066333737 +38373864333734306234383130376239373238636262376664613865363265363337393031616566 +35336436373535613535653330306164373462313839316636326534366163326538653633376337 +34643039373461393565653838323061656330346632316133373064613661306365336462646237 +62326134623063346431643335313166306461653239303537663132643239386636653363323662 +39393262613766343563636561356362653665313234366532626638346264316363343264313435 +36386531656534666462383337343835313638376438653165663839393433376332623938363738 +35356538383136333534386531376530383936633463363166326133303264373534613066356538 +34373637303333333836386238303061303834383331653735336233666164656530656666356533 +39343030643236613831663465323735343737623362383734326162356333323864333035366433 +63316264613337363136613062663263313731636335383531383933623339666134643539353330 +61333330336132636666383561356565346563393937653539373431653561653664616137623133 +39336333313330336137346238306130653730326130323163306464376434353736623232656134 +33356435663762666266393461363531396334313733333132666363316133346531626564626334 +66356436393062373364316439646235666336333039383137353764303763636461663638333362 +66306430396530636538353965333862323363323832373033353530306331653935396134323634 +61353964303666643163646464613737393863373236613532353761636230336633373461343266 +34383031353734346465613030653639656366313530333265383632343465393566386161373633 +34313563636265336239323362316432383637303134376331633039333537613461353135303934 +66366334663032336330386632656162643933316666643562616564613633303231666536653536 +61376534663632316338653231653561643338356661353232633431623834396635383034366438 +30376161663339633530646539313132383536303931333366316631356135303933316162373730 +38343139653932666335353437613734366530616261343134386261313330303766666136616532 +38663635393236376661623862646265626239656665323332633532616536643237333137343763 +37663432366337633662303931353939346462633837346638353530636132313436626161633865 +30653065666261363238366534656265663039373962333938633335373163383261363434646437 +35356538623765343866653065306164363363616137323065333062386435393539303930363239 +62653638303766343738346437323536306139303261393863373035336662316566646565316662 +30633464663666383135663365636436363132363437633933643464626339643865383066623039 +66356338633939386337353135613066343236356262363164666466333462383364663134616433 +38626332623530633765656464383136383466623561633666366566316461356437626165316166 +65626339383133623439366662373666346539613231306564643731383935633939343865613534 +66363934373433363134323165383830643535613139313139316137643532303562393432383330 +61376363363939633835383161396464613662383961366634626463346430303631653930613430 +66616132616364623261363731343437633032383361356433333531363964633962336338373338 +61353134616332663135663737613230666534656232623933613862326366393538373737393838 +30343037343432306437663035613831316432336431383938363338316437616164376435376261 +66363664333031653962383630656430646238353337616535343535373935663561306536353165 +65343235396537366635633638306135383164353562336331376336323633323338353361353061 +65623431633731303936303732613964346139306336356461626466343132383162666337333664 +38636631636338353339316666643436386537336231353332663636383337343831323939396636 +30663634653733623530343537636137623332666666343433643830333737336639313064386333 +34656362613863323262326130666361373434336234366337663236663739383366373635343862 +62656365366533633263323963346363396237393462653633313133353935343631393032303566 +66326266353264306266613936333734643631376637663137373232653962646534643565326366 +64646634346534613730646430333432373464653365663666346232373332346231306563636136 +38646565366661613161383530326261613537366633323239656461343337363139353164653935 +66356636653232643035363564306438396233376465646462316463333832636361363137363364 +66653164363836333261386437373864666438383338383963616662386631306530326535303666 +37393037313539393535633333663662363164323635333635646638663264383533303032303561 +34613563353237633761363435636138616462656263353966636363326533363235663761643033 +63613934323330363732353465316261653964656430383532623833653663306632633832653763 +32333932663362663664663535363566656665653839386363373066343336353264663136326136 +66383461626137353430316266356366393937613234656261646632366130613739353266613163 +30383731373137653137353663643866343632383937613136666361626435393331313130386230 +33306439633138306331616165386239643765323765366530336337623264343030333234336234 +64333333613838333734346361636162346564656334396139373535333830313037383864623561 +38336131343532663761326635633531323766363963663763653630643232663631613465333132 +35306635636437323835326633616665313133393162623737386538373364343865383634636463 +33626165623962656432396237653665346464663066633633633564313266343662336333656166 +34646636663864663439356364383031313065663433386530663365353937626366663334346434 +35656365316134306161656133653736306537613133386634386638376534376133346237353765 +64363964303535653037323566653838633231363132666237353765643137383030623930393931 +33313935663366313362656137613865386532303737306437353437336331643435323030663666 +66383139313333633538373836396263353337316264656631376236346466353765653830366366 +66386131336431336466636166366139396337386135636262316237306561353563623061313266 +33343563393031303133393163653638643065636636653766613836386438626332373965383864 +36343936613738336132396265663130643739373264356530636363633239346637653136343761 +64646137383235636231313736343162353237636566663830373739636536393864646639343564 +61323039376537646531633030626665323537366233353939653665326666653263623431356166 +34343538613031336561303165323134303862666139633937323331643930383938666138346230 +33633537666431643234336134333333633664333564396139363136363434376130323632363833 +32363764656133653462633132366630396233613463643833353038316232353962623761373966 +35613132306662663937366464373034336531366262643939333137613031666162383434313664 +65363332383330623863326465353039306338393230303165656366386161656239316161336633 +61616434363637376131316135643336653636313565613035643038386332356263383061643262 +63333439303736623835653636353261363532646266663331313566363562343730303436353636 +63636237343965316366613137353062393832373133316438656433313132643061633130396662 +65346530313962306238373634366266386232613566656337396663626535393964323737323939 +66333165356138353362336166376466303965663439636432653862316433376264376563313137 +30653565386636363730623630646633633832306131626235613862393239386437386561356663 +64303862616530393838383364643266633336326665323961643935356339316564316332633635 +32323239333339633838666233376165303061373465376133383863333232663065626462303362 +37613638303266356537633965323966363331383130613963363933656531663031643030396632 +38343838383731346536616364303531373030643730626165633464646137376439643637633636 +62346664633931376438313639613165306363356532346566336336363662353233613134346132 +30646633363364363234396435323037306663633863383530383034383338636639326362626639 +64313532313939316562353561323534313438346530303536333066393863613136646664353264 +65663530366238343732396430633138613664653962366338643738623636373031363839393534 +38393439393366396262643461303361613666336631313037306231363537346364346432616539 +64616431633730333233616362363638613837356638343131376262363736666532356532653330 +64353431663261623033323337373932396436656633396439323162626563653630633239306233 +37633939363536396439643663623434316161383938393864653437316535313433336461383065 +32396536313835393030313136356530623539646361633935333362373339393662393335353361 +37353366666264613563663836666332353561666464623561653238666537316233343838663363 +38616463643935376233363031323838393163306465333030393331663231623061646530393237 +33356466363261653235336134376166343537643065663235333161383366336132316666383731 +39326662373162313861363132653531323165393939313461663262316337333762393134663030 +35313762356234396336356336393937316262646264306361363235663334333663363136383430 +35366538386335633038326563623833313031383762653266643034373838653863363835383466 +37323138316564383262356633646262623439326464633530313639313637326136656139376362 +35323866663262373938343863653665373139663565623730666366643630636566393266303261 +35666261383464393236353964623234306637643838623535313264356364366661653639303662 +37616531626435323664636538396439373064333563303637303562653763323935666138383338 +32326630303465366332656165343036336630383163643361306136333361373738306238393932 +63363331373434353563353631306361383431636239376464643239326531306130633463653665 +64636536383430613538623663363066646437666537306161613265323936663366353864323230 +32666333326630636433656166343331396166383438313866306566356238653061343461653035 +6236383630643231646264333133343738663663323332336536 diff --git a/ansible/host_vars/monerod_box_local/main.yml b/ansible/host_vars/monerod_box_local/main.yml new file mode 100644 index 0000000..10e28ee --- /dev/null +++ b/ansible/host_vars/monerod_box_local/main.yml @@ -0,0 +1,27 @@ +# monerod-box: the Monero node. +# +# These ports are read twice: by the role that deploys the service here, and by +# the Caddy play that runs on the EDGE host and publishes the restricted RPC. +# A role default is invisible to that second play, which is why they live in +# host_vars - the edge play reads them as +# hostvars['monerod_box_local']., and the role picks them up +# automatically because host_vars outranks role defaults. +monero_p2p_port: 18080 +monero_restricted_rpc_port: 18089 + +# The 500 GB disk added to this VM in Proxmox as scsi1. Named here rather than +# left to the role default so that "which disk holds the blockchain" is a +# property of the machine, recorded next to the machine. The role only ever +# uses it to create the filesystem; the mount itself goes by UUID. +monero_data_device: /dev/sdb + +# Systemd services deployed on this host, monitored every 5 minutes. +# +# The fact lives with the machine rather than in a central map, for the same +# reason the cross-host ports do: "what runs here" is a property of the host, +# and a central list is one more thing to forget to update when a service moves. +# +# Only units WE deploy belong here. Distro units (ssh, cron) have their own +# supervision and would be noise. +monitored_services: + - monerod diff --git a/ansible/infra/402_public_monitoring.yml b/ansible/infra/402_public_monitoring.yml index d924468..a43a8af 100644 --- a/ansible/infra/402_public_monitoring.yml +++ b/ansible/infra/402_public_monitoring.yml @@ -31,6 +31,7 @@ - {sub: "{{ subdomains.memos }}", host: vipy} - {sub: "{{ subdomains.mempool }}", host: vipy} - {sub: "{{ subdomains.datum_gateway }}", host: vipy} + - {sub: "{{ subdomains.monero }}", host: vipy} # A public resolver on purpose: this must test what the internet sees, not # what a local cache or the tailnet's MagicDNS happens to answer. @@ -50,6 +51,10 @@ - {name: memos, sub: "{{ subdomains.memos }}", path: "/", status: 200} - {name: mempool, sub: "{{ subdomains.mempool }}", path: "/", status: 200} - {name: datum, sub: "{{ subdomains.datum_gateway }}", path: "/", status: 401} + # 401 is CORRECT: monerod answers an unauthenticated request with an HTTP + # Digest challenge. Asserting 200 here would go green precisely when + # rpc-login broke and the node started serving the world for free. + - {name: monero, sub: "{{ subdomains.monero }}", path: "/get_info", status: 401} # Ports published from the edge host by socket_proxy. public_tcp: diff --git a/ansible/infra/403_service_probe_registration.yml b/ansible/infra/403_service_probe_registration.yml index c92b767..a9c5de5 100644 --- a/ansible/infra/403_service_probe_registration.yml +++ b/ansible/infra/403_service_probe_registration.yml @@ -45,6 +45,7 @@ - {name: mempool-mariadb, host: mempool_box_local} - {name: mempool-backend, host: mempool_box_local} - {name: mempool-frontend, host: mempool_box_local} + - {name: monerod, host: monerod_box_local} tasks: - name: Build the probe endpoint list diff --git a/ansible/inventory.ini b/ansible/inventory.ini index c39439d..f14deb4 100644 --- a/ansible/inventory.ini +++ b/ansible/inventory.ini @@ -15,6 +15,7 @@ memos_box_local ansible_host=memos-box lan_ip=192.168.1.145 ansible_user=counter forgejo_runner_local ansible_host=forgejo-runner-box lan_ip=192.168.1.132 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua arbret_staging_local ansible_host=arbret-staging-box lan_ip=192.168.1.147 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua small_backups_local ansible_host=small-backups-box lan_ip=192.168.1.131 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua +monerod_box_local ansible_host=monerod-box lan_ip=192.168.1.146 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua # Local connection to laptop: this assumes you're running ansible commands from your personal laptop [lapy] @@ -48,6 +49,9 @@ fulcrum_box_local [mempool] mempool_box_local +[monero] +monerod_box_local + [memos] memos_box_local diff --git a/ansible/roles/monero/README.md b/ansible/roles/monero/README.md new file mode 100644 index 0000000..abbe4a1 --- /dev/null +++ b/ansible/roles/monero/README.md @@ -0,0 +1,151 @@ +# monero + +A Monero full node (`monerod`) on Debian 13, bare metal, systemd, no Docker. +Runs on `monerod-box` (Proxmox VMID 2400), reachable from Feather over the WAN. + +## Why the published binary and not a build from source + +The intent was to build from source and check the result against the published +binary checksums. That is not achievable for the current release, and it is +worth writing down why so nobody re-litigates it: + +- `contrib/guix`, the reproducible-build system that produces a byte-identical + tarball, **exists only on monero `master`**. At tag `v0.18.5.1` `contrib/` + contains `gitian`, not `guix`. +- Reproducing v0.18.5.1 therefore means **Gitian**, whose README requires an + Ubuntu 18.04 host (EOL 2023), LXC or Docker, and `python-cheetah` (Python 2). +- A plain `make release-static` is **not** reproducible. It would give us a + self-built binary whose hash matches nothing - not upstream's, not anyone + else's - so it satisfies neither half of "build it ourselves AND check the + checksum". + +So the role installs the published binary and proves its provenance instead. +That chain is real, and it is the strongest claim available for this release: + +1. binaryFate's public key is **vendored** at `files/binaryfate.asc`. A trust + root fetched over the network at the moment you need it is not a trust root. + Its fingerprint is asserted after import anyway + (`81AC591FE9C4B65C5806AFC3F0AF4D462A0BDF92`), so a swapped file is caught. +2. `hashes.txt` is downloaded and its clearsigned signature verified. The + assertion is on gpg's `VALIDSIG ` status line, **not** on the + string "Good signature" - `bitcoin_knots` greps for the latter, which passes + for *any* key in the keyring. +3. The verified plaintext is written out by `gpg --decrypt`, and the tarball's + SHA256 is looked up in *that*, not in the raw clearsigned file. +4. The lookup requires the hash **and** the filename on one line, so a hash + belonging to a different artefact cannot satisfy it. + +`monero_sha256` in `defaults/` is a second, in-git assertion passed to +`get_url`. It is not the security control - step 4 is - but it means a version +bump cannot happen without the checksum moving in the same diff. + +**Bumping the version: change `monero_version` and `monero_sha256` together.** + +### The forward path + +The published `monero-linux-x64-v0.18.5.1.tar.bz2` unpacks to +`monero-x86_64-linux-gnu-v0.18.5.1/` - that is the Guix `DISTNAME`, renamed at +publish time. So once v0.19 ships with Guix on the release branch, +`sha256(guix output) == hashes.txt entry` holds exactly, and a `guix` install +method can be added here without changing anything else. + +## The data disk + +The blockchain is on a **dedicated 500 GB disk** (`scsi1`), not on the 10 GB +root filesystem. `tasks/storage.yml` creates the filesystem and the fstab entry. + +This is done in code on purpose. `knots-box` mounts `/mnt/knots_data` (~875 GB +of chain) entirely by hand, and that mount appears nowhere in Ansible - a +rebuild of that host would come up with the chain silently missing. + +Three things there are load-bearing: + +- **The whole device, no partition table.** One disk, one job; a partition table + is just another layer that can disagree with itself. +- **Everything after mkfs goes by UUID.** `/dev/sdX` ordering is decided by the + kernel at boot. This estate has been bitten eight times by an identifier + written down once whose thing then moved; `monero_data_device` is used *only* + to create the filesystem. +- **`RequiresMountsFor=` in the unit.** `nofail` in fstab means a missing disk + does not drop a headless VM into an emergency shell - but without + `RequiresMountsFor` monerod would then start, find an empty directory, and + begin writing a ~300 GB chain onto a 10 GB root disk. With it, the unit + refuses to start and `infra/401` reports it within five minutes. + +The mount-point task and the post-mount ownership task deliberately use the +**same** owner and mode. Writing the obvious `root:root 0755` on the first made +the two disagree, and every run flipped the directory back and forth. + +## Exposure: one HTTPS vhost, no new open ports + +- **P2P is outbound only.** Nothing forwards 18080 to this box. The node dials + out, syncs fully and serves us; it just does not relay for strangers. + `no-igd=1` follows - with nothing forwarded there is nothing for UPnP to do + except announce us to the router. +- **Two RPC servers, not one.** + - unrestricted on `127.0.0.1:18081` - administration and the health check. + - restricted on `:18089` - the only surface anything off-box touches. + + This is `rpc-restricted-bind-port`, deliberately **not** `restricted-rpc=1`. + That flag restricts the loopback server too, and then we could not ask our own + node anything useful. Verified live: the restricted port reports + `restricted=true` and blanks `version` and `start_time`. +- **`rpc-login` is HTTP Digest** (`contrib/epee/src/http_auth.cpp`), and + `core_rpc_server::init` hands the same login to *both* servers. Both ports + answer an unauthenticated request with 401. +- **Caddy on the edge host terminates TLS and nothing else.** No + `caddy_site_basic_auth`: Caddy's Basic challenge would collide with the Digest + challenge Feather expects. monerod's own auth is the authentication. + +### No UFW rule, and that is correct + +Traffic arriving over the tailnet **bypasses UFW entirely**. Verified on +`mempool-box`: UFW is active with default-deny-incoming and `22/tcp` as its only +allow rule, yet the edge host reaches its frontend port - because +`iptables -S INPUT` puts `-A INPUT -j ts-input` ahead of every `ufw-*` chain. +Adding a UFW allow here would be cargo cult. + +## Monitoring + +Four Gatus endpoints, all verified passing: + +| Endpoint | Source | Answers | +|---|---|---| +| `liveness/monerod_box_local` | `infra/400` | is the host up | +| `disk/monerod_box_local` | `infra/400` | is it running out of space | +| `services/monerod-box-local/monerod` | `infra/401` via `monitored_services` | is the unit active | +| `probe/monerod` | this role + `infra/403` | does monerod actually answer RPC | + +**The probe deliberately does not check `synchronized`.** A full initial sync +takes days. A check requiring `synchronized: true` would report DOWN for every +one of those days, which is not a monitor - it is an alarm with the batteries +taken out. Answering RPC is the health question; height and sync state go in the +message so they are visible without being alertable. + +The timer carries **no `Requires=monerod.service`**. Requires on a timer stops +the timer when the watched unit stops - "if the thing I am watching goes down, +stop watching it" - and leaves nothing to re-arm it. Fulcrum's timer carried it +and sat `active`/`enabled` for seven months without firing once. `bitcoin_knots` +still carries it; do not copy that. + +`Enable and restart the monerod health check timer` reports `changed` on every +run. That is intentional and matches `datum_gateway`: `state: restarted` is how +a timer gets re-armed, and `started` is a no-op on an already-active timer. + +## Backups: none, deliberately + +The blockchain is re-syncable public data and there is no wallet on this node, +so a backup job would move hundreds of GB around to protect nothing. This +follows the `phoenixd` precedent (Plan 5, Model C) of recording an exclusion as +a decision rather than leaving it to look like an oversight. + +## Known constraint: RAM + +The VM has **2 GB and no swap**. Mid-sync, monerod's RSS was already 1,333 MiB +of 1,973 MiB with ~100 MiB free. No OOM kill has happened yet, but the working +set grows with the database and there is no swap to absorb a spike. `out-peers` +and `in-peers` are held down (16/8) partly for this reason. + +**8 GB is the recommendation.** Bumping it means editing `memory_mb` in +`tofu/nodito/vms.auto.tfvars` and resizing the VM in the Proxmox UI - the vm +resource sets `ignore_changes = all`, so tofu will not do it for you. diff --git a/ansible/roles/monero/defaults/main.yml b/ansible/roles/monero/defaults/main.yml new file mode 100644 index 0000000..633f3c0 --- /dev/null +++ b/ansible/roles/monero/defaults/main.yml @@ -0,0 +1,96 @@ +# Monero node (monerod) configuration variables. +# https://docs.getmonero.org/interacting/monerod-reference/ + +# --- Version and artefact --------------------------------------------------- +# Bump monero_version and monero_sha256 TOGETHER. The sha256 here is a second, +# in-git assertion; the one that actually matters is the grep against the +# GPG-signed hashes.txt in install.yml. Having both means a version bump cannot +# be done silently - the checksum has to move in the same diff. +monero_version: "0.18.5.1" +monero_platform: "linux-x64" +monero_tarball: "monero-{{ monero_platform }}-v{{ monero_version }}.tar.bz2" +monero_url: "https://downloads.getmonero.org/cli/{{ monero_tarball }}" +monero_sha256: "22a7dda7b0cb699fdd6b7674c3b4a4465b337cc98a54983523b759e1e7cc9958" + +# The signed list of checksums for every published Monero artefact. +monero_hashes_url: "https://www.getmonero.org/downloads/hashes.txt" + +# binaryFate, the release signing key. The .asc is vendored in files/ rather +# than fetched at deploy time: a trust root downloaded over the network at the +# moment you need it is not a trust root. The fingerprint is asserted after +# import anyway, so a swapped file cannot go unnoticed. +monero_signing_fingerprint: "81AC591FE9C4B65C5806AFC3F0AF4D462A0BDF92" + +# --- Service user ----------------------------------------------------------- +monero_user: monero +monero_group: monero + +# --- Directories ------------------------------------------------------------ +monero_conf_dir: /etc/monero +monero_log_dir: /var/log/monero +monero_bin_dir: /usr/local/bin + +# The blockchain. This is the mount point of the dedicated 500 GB disk, NOT a +# directory on the 10 GB root filesystem - see storage.yml. +monero_data_dir: /var/lib/monero + +# --- The data disk ---------------------------------------------------------- +# The whole device, no partition table: this disk has one job and a partition +# table would only add a layer that can disagree with itself. +# +# monero_data_device is used ONLY to create the filesystem, once. Everything +# afterwards - fstab, the mount, the systemd unit - goes by UUID, because +# /dev/sdX ordering is decided by the kernel at boot and is not a stable name +# for anything. This estate has been bitten eight times by an identifier that +# was written down once and then moved. +monero_data_device: /dev/sdb +monero_data_fstype: ext4 +# -m 0: the 5% ext4 reserve exists to stop root-owned daemons deadlocking a full +# root filesystem. On a dedicated data disk it is 25 GB thrown away. +monero_data_mkfs_opts: "-m 0 -L monero-data" + +# --- Network ---------------------------------------------------------------- +# P2P is OUTBOUND ONLY. No inbound port is published from the edge host, so the +# node dials out to peers and never receives. It syncs and serves us completely; +# it just does not relay for strangers. no-igd follows from that: with nothing +# forwarded there is nothing for UPnP to do but tell the router about us. +monero_p2p_port: 18080 + +# Unrestricted RPC, loopback only: administration and the health check. +monero_rpc_bind_ip: "127.0.0.1" +monero_rpc_port: 18081 + +# The RESTRICTED RPC is a second server on its own port, reachable over the +# tailnet and published by Caddy on the edge host. This is deliberately +# rpc-restricted-bind-port and NOT `restricted-rpc=1` - the latter would +# restrict the loopback server too, and then we could not ask our own node +# anything useful. +# +# The authoritative value for the live deployment is in +# host_vars/monerod_box_local/main.yml, which the edge play also reads; the +# value here is the upstream default so the role still works standalone. +monero_restricted_rpc_port: 18089 + +# Peer counts kept modest on purpose: this VM has 2 GB of RAM. See README. +monero_out_peers: 16 +monero_in_peers: 8 + +# --- RPC credentials -------------------------------------------------------- +# monerod's --rpc-login is HTTP Digest (contrib/epee/src/http_auth.cpp) and the +# same login guards BOTH the loopback and the restricted server. Feather sends +# these as the node username/password. +monero_rpc_user: "" +monero_rpc_password: "" + +# monerod.conf holds the RPC login, so --diff is suppressed by default. +# Pass -e monero_reveal_config=true to see what moved. +monero_reveal_config: false + +# --- Health check ----------------------------------------------------------- +# WHERE TO REPORT HEALTH — the one place to plug in monitoring. Empty means +# check, exit honestly, report nowhere. Any endpoint accepting an HTTP ping +# works; nothing here is specific to a monitoring product. +healthcheck_push_url: "" +# Bearer token for the Gatus external endpoint. Required whenever a push URL +# is set: Gatus rejects an unauthenticated push with 401. +healthcheck_push_token: "" diff --git a/ansible/roles/monero/files/binaryfate.asc b/ansible/roles/monero/files/binaryfate.asc new file mode 100644 index 0000000..55c5b16 --- /dev/null +++ b/ansible/roles/monero/files/binaryfate.asc @@ -0,0 +1,87 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +lmE0kDaYQqoBKXNXcybElBID8Q== +=tkJZ +-----END PGP PUBLIC KEY BLOCK----- diff --git a/ansible/roles/monero/handlers/main.yml b/ansible/roles/monero/handlers/main.yml new file mode 100644 index 0000000..15f7e63 --- /dev/null +++ b/ansible/roles/monero/handlers/main.yml @@ -0,0 +1,17 @@ +--- +# Ungated, and that is the whole point. Across this estate the restart handlers +# were written with `when: uptime_kuma_enabled | default(false)`, which made +# them inert: Fulcrum's config changes landed on disk and silently never took +# effect for months. Three tasks notify this handler - the config, the systemd +# unit and the binary itself - and all three need it to actually fire. +- name: Restart monerod + systemd: + name: monerod + state: restarted + daemon_reload: yes + +- name: Restart monerod health check timer + systemd: + name: monerod-healthcheck.timer + state: restarted + daemon_reload: yes diff --git a/ansible/roles/monero/tasks/configure.yml b/ansible/roles/monero/tasks/configure.yml new file mode 100644 index 0000000..b8af394 --- /dev/null +++ b/ansible/roles/monero/tasks/configure.yml @@ -0,0 +1,55 @@ +--- +# Directory layout follows upstream's systemd guide: +# https://docs.getmonero.org/running-node/monerod-systemd/ +# The data directory is not here - it is the mount point, handled in storage.yml. + +- name: Create the Monero config directory + ansible.builtin.file: + path: "{{ monero_conf_dir }}" + state: directory + owner: root + group: "{{ monero_group }}" + mode: '0750' + +- name: Create the Monero log directory + ansible.builtin.file: + path: "{{ monero_log_dir }}" + state: directory + owner: "{{ monero_user }}" + group: "{{ monero_group }}" + mode: '0750' + +- name: Assert the RPC credentials are set + ansible.builtin.assert: + that: + - monero_rpc_user | length > 0 + - monero_rpc_password | length > 0 + quiet: true + fail_msg: >- + monero_rpc_user and monero_rpc_password must be set. They are the only + authentication in front of the restricted RPC that Caddy publishes to the + internet, and monerod started without them would serve it to anyone. + +- name: Create monerod.conf + ansible.builtin.template: + src: monerod.conf.j2 + dest: "{{ monero_conf_dir }}/monerod.conf" + owner: root + group: "{{ monero_group }}" + mode: '0640' + # monerod.conf carries the RPC login, which is the only authentication in + # front of the endpoint Caddy publishes. `--diff` prints rendered content, so + # running with --diff would put it on the terminal and into any log capturing + # it. Suppressed by default; pass -e monero_reveal_config=true when you + # genuinely need the diff. This is a task keyword, not a module parameter. + diff: "{{ monero_reveal_config | default(false) | bool }}" + notify: Restart monerod + +- name: Create the monerod systemd unit + ansible.builtin.template: + src: monerod.service.j2 + dest: /etc/systemd/system/monerod.service + owner: root + group: root + mode: '0644' + notify: Restart monerod diff --git a/ansible/roles/monero/tasks/healthcheck.yml b/ansible/roles/monero/tasks/healthcheck.yml new file mode 100644 index 0000000..794ccba --- /dev/null +++ b/ansible/roles/monero/tasks/healthcheck.yml @@ -0,0 +1,58 @@ +--- +# "Is monerod actually answering", as opposed to infra/401's "is the unit +# active". A daemon can be perfectly `active` and wedged. + +- name: Install the health check's dependencies + apt: + name: + - curl + - jq + state: present + +- name: Create the monerod health check script + ansible.builtin.template: + src: healthcheck.sh.j2 + dest: /usr/local/bin/monerod-healthcheck-push.sh + owner: root + group: root + mode: '0755' + validate: "bash -n %s" + +# 0600: this unit carries both the Gatus bearer token and the RPC password. +# They live here rather than in the script so the script itself stays readable +# and credential-free. +- name: Create the monerod health check systemd service + ansible.builtin.template: + src: healthcheck.service.j2 + dest: /etc/systemd/system/monerod-healthcheck.service + owner: root + group: root + mode: '0600' + notify: Restart monerod health check timer + +- name: Create the monerod health check systemd timer + ansible.builtin.template: + src: healthcheck.timer.j2 + dest: /etc/systemd/system/monerod-healthcheck.timer + owner: root + group: root + mode: '0644' + notify: Restart monerod health check timer + +- name: Reload systemd daemon after health check units + systemd: + daemon_reload: yes + +# Ungated: enabling a timer is deployment, not monitoring. +- name: Enable and restart the monerod health check timer + systemd: + name: monerod-healthcheck.timer + enabled: yes + state: restarted + daemon_reload: yes + +# Arms the timer and smoke-tests the check. Restarting the timer alone is not +# enough with OnBootSec + OnUnitActiveSec - see roles/bitcoin_knots/README.md. +- name: Run the monerod health check once to arm the timer + ansible.builtin.command: systemctl start monerod-healthcheck.service + changed_when: false diff --git a/ansible/roles/monero/tasks/install.yml b/ansible/roles/monero/tasks/install.yml new file mode 100644 index 0000000..0175680 --- /dev/null +++ b/ansible/roles/monero/tasks/install.yml @@ -0,0 +1,232 @@ +--- +# Install monerod from the published release, and prove the bytes are the ones +# binaryFate signed before they are allowed anywhere near /usr/local/bin. +# +# These tasks are the security control of this role. Do not "simplify" them. +# +# Why the published binary and not a build from source: the reproducible-build +# system that yields a byte-identical tarball (contrib/guix) exists only on +# monero master. The v0.18.x release branch still uses Gitian, which wants an +# Ubuntu 18.04 host and Python 2. A plain `make release-static` is not +# reproducible, so it would give us a self-built binary whose hash matches +# nothing - neither upstream's nor anyone else's. The signature chain below is +# the strongest claim actually available for this release. See README.md. + +- name: Install the tools the verification chain needs + apt: + name: + - gnupg + - curl + - bzip2 + state: present + update_cache: yes + +- name: Check whether monerod is already installed + ansible.builtin.stat: + path: "{{ monero_bin_dir }}/monerod" + register: monero_binary + +- name: Read the installed monerod version + ansible.builtin.command: "{{ monero_bin_dir }}/monerod --version" + register: monero_installed_version + changed_when: false + failed_when: false + check_mode: false + when: monero_binary.stat.exists + +- name: Decide whether monerod needs installing + ansible.builtin.set_fact: + monero_needs_install: >- + {{ not monero_binary.stat.exists + or monero_version not in (monero_installed_version.stdout | default('')) }} + +# Everything below runs only for a fresh install or a version bump. + +# A throwaway keyring. The verification must not depend on, or add to, whatever +# happens to be in root's keyring already. +- name: Create a temporary GnuPG home for the verification + ansible.builtin.tempfile: + state: directory + suffix: monero-gpg + register: monero_gnupghome + when: monero_needs_install | bool + +- name: Copy binaryFate's signing key to the host + ansible.builtin.copy: + src: binaryfate.asc + dest: "{{ monero_gnupghome.path }}/binaryfate.asc" + mode: '0644' + when: monero_needs_install | bool + +- name: Import binaryFate's signing key + ansible.builtin.command: "gpg --batch --import {{ monero_gnupghome.path }}/binaryfate.asc" + environment: + GNUPGHOME: "{{ monero_gnupghome.path }}" + register: monero_key_import + changed_when: "'imported' in monero_key_import.stderr" + when: monero_needs_install | bool + +- name: Read the fingerprints of the imported key + ansible.builtin.shell: >- + set -o pipefail && + gpg --batch --with-colons --fingerprint | awk -F: '/^fpr:/{print $10}' + args: + executable: /bin/bash + environment: + GNUPGHOME: "{{ monero_gnupghome.path }}" + register: monero_key_fingerprints + changed_when: false + when: monero_needs_install | bool + +# The vendored file could be swapped in a bad pull request. The fingerprint is +# the thing being trusted, so it is checked, not assumed. +- name: Assert the imported key is binaryFate's + ansible.builtin.assert: + that: + - monero_signing_fingerprint in monero_key_fingerprints.stdout_lines + quiet: true + fail_msg: >- + files/binaryfate.asc does not contain the expected key + {{ monero_signing_fingerprint }}. Refusing to go on. + when: monero_needs_install | bool + +- name: Download the signed hashes.txt + ansible.builtin.get_url: + url: "{{ monero_hashes_url }}" + dest: "{{ monero_gnupghome.path }}/hashes.txt" + mode: '0644' + force: yes + when: monero_needs_install | bool + +# One command that both verifies the signature and writes out ONLY the verified +# plaintext. Grepping the clearsigned file directly would search text that has +# not been through the signature check. +- name: Verify hashes.txt and extract the verified plaintext + ansible.builtin.command: >- + gpg --batch --status-fd 2 + --output {{ monero_gnupghome.path }}/hashes-verified.txt + --decrypt {{ monero_gnupghome.path }}/hashes.txt + environment: + GNUPGHOME: "{{ monero_gnupghome.path }}" + register: monero_hashes_verify + changed_when: false + failed_when: false + when: monero_needs_install | bool + +# Greping for "Good signature" - which is what the bitcoin_knots role does - +# passes for ANY key in the keyring. VALIDSIG names the key, and the line +# carries both the signing subkey and the primary key fingerprint, so checking +# that our fingerprint appears on it survives a subkey rotation. +- name: Assert hashes.txt carries a valid signature from binaryFate + ansible.builtin.assert: + that: + - monero_hashes_verify.rc == 0 + - monero_hashes_verify.stderr is search('VALIDSIG[^\n]*' ~ monero_signing_fingerprint) + quiet: true + fail_msg: >- + hashes.txt did not verify against {{ monero_signing_fingerprint }}. + This is the whole security control of this role - aborting. + gpg said: {{ monero_hashes_verify.stderr | default('') }} + when: monero_needs_install | bool + +- name: Download the monerod release tarball + ansible.builtin.get_url: + url: "{{ monero_url }}" + dest: "/tmp/{{ monero_tarball }}" + mode: '0644' + checksum: "sha256:{{ monero_sha256 }}" + force: yes + when: monero_needs_install | bool + +- name: Compute the SHA256 of the downloaded tarball + ansible.builtin.stat: + path: "/tmp/{{ monero_tarball }}" + checksum_algorithm: sha256 + get_checksum: yes + register: monero_tarball_stat + when: monero_needs_install | bool + +# The check that actually ties these bytes to binaryFate's signature. The +# get_url checksum above only ties them to a string in our own repo; this ties +# them to the signed list. Both the hash AND the filename must be on one line, +# so a hash belonging to a different artefact cannot satisfy it. +- name: Look up the downloaded tarball's checksum in the signed hashes + ansible.builtin.command: + argv: + - grep + - -F + - -x + - "{{ monero_tarball_stat.stat.checksum }} {{ monero_tarball }}" + - "{{ monero_gnupghome.path }}/hashes-verified.txt" + register: monero_hash_match + changed_when: false + failed_when: false + when: monero_needs_install | bool + +- name: Assert the tarball is the one binaryFate signed + ansible.builtin.assert: + that: + - monero_hash_match.rc == 0 + quiet: true + fail_msg: >- + {{ monero_tarball }} hashes to {{ monero_tarball_stat.stat.checksum }}, + which is not the value signed for that filename in hashes.txt. + Do not install this file. + when: monero_needs_install | bool + +- name: Create a temporary extraction directory + ansible.builtin.tempfile: + state: directory + suffix: monero-extract + register: monero_extract_dir + when: monero_needs_install | bool + +- name: Extract the monerod tarball + ansible.builtin.unarchive: + src: "/tmp/{{ monero_tarball }}" + dest: "{{ monero_extract_dir.path }}" + remote_src: yes + when: monero_needs_install | bool + +# By find rather than by a hardcoded path: the tarball unpacks to +# monero-x86_64-linux-gnu-v/, which is not the name of the tarball. +- name: Locate the extracted monerod binary + ansible.builtin.find: + paths: "{{ monero_extract_dir.path }}" + patterns: monerod + recurse: yes + file_type: file + register: monero_extracted + when: monero_needs_install | bool + +- name: Assert the archive contained monerod + ansible.builtin.assert: + that: + - monero_extracted.files | length > 0 + quiet: true + fail_msg: "No monerod binary in {{ monero_tarball }}" + when: monero_needs_install | bool + +# Only monerod. The wallet and blockchain utilities are in the same tarball if +# they are ever wanted; a node does not need them and every binary installed is +# surface that has to be kept current. +- name: Install monerod + ansible.builtin.copy: + src: "{{ monero_extracted.files[0].path }}" + dest: "{{ monero_bin_dir }}/monerod" + remote_src: yes + owner: root + group: root + mode: '0755' + when: monero_needs_install | bool + notify: Restart monerod + +- name: Clean up the download and verification artefacts + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: + - "/tmp/{{ monero_tarball }}" + - "{{ monero_extract_dir.path | default('/nonexistent') }}" + - "{{ monero_gnupghome.path | default('/nonexistent') }}" + when: monero_needs_install | bool diff --git a/ansible/roles/monero/tasks/main.yml b/ansible/roles/monero/tasks/main.yml new file mode 100644 index 0000000..699cf3b --- /dev/null +++ b/ansible/roles/monero/tasks/main.yml @@ -0,0 +1,15 @@ +--- +# import_tasks, not include_tasks: static imports stay visible to --list-tasks, +# which is the main verification tool in this repo. +# +# The order is load-bearing: +# user must exist before anything chowns to it +# storage the data directory IS the mount point of the 500 GB disk, so if the +# disk is not there the daemon must not start and quietly begin +# filling a 10 GB root filesystem with a 300 GB blockchain +- ansible.builtin.import_tasks: user.yml +- ansible.builtin.import_tasks: storage.yml +- ansible.builtin.import_tasks: install.yml +- ansible.builtin.import_tasks: configure.yml +- ansible.builtin.import_tasks: service.yml +- ansible.builtin.import_tasks: healthcheck.yml diff --git a/ansible/roles/monero/tasks/service.yml b/ansible/roles/monero/tasks/service.yml new file mode 100644 index 0000000..ea11f62 --- /dev/null +++ b/ansible/roles/monero/tasks/service.yml @@ -0,0 +1,35 @@ +--- +- name: Reload systemd daemon + systemd: + daemon_reload: yes + +- name: Enable and start monerod + systemd: + name: monerod + enabled: yes + state: started + +- name: Flush handlers so monerod is running before we ask it anything + meta: flush_handlers + +# Generous, and non-fatal. On a first start monerod initialises LMDB on a fresh +# 500 GB disk before it opens the RPC port; on a later start it is quick. A +# timeout here is not a deployment failure, and the health check is the thing +# that decides whether the node is actually well. +- name: Wait for the monerod RPC port + ansible.builtin.wait_for: + port: "{{ monero_rpc_port }}" + host: "{{ monero_rpc_bind_ip }}" + delay: 5 + timeout: 180 + ignore_errors: yes + +- name: Check the monerod service status + systemd: + name: monerod + register: monero_service_status + changed_when: false + +- name: Display the monerod service status + ansible.builtin.debug: + msg: "monerod is {{ 'running' if monero_service_status.status.ActiveState == 'active' else 'NOT running' }}" diff --git a/ansible/roles/monero/tasks/storage.yml b/ansible/roles/monero/tasks/storage.yml new file mode 100644 index 0000000..beef59c --- /dev/null +++ b/ansible/roles/monero/tasks/storage.yml @@ -0,0 +1,133 @@ +--- +# The 500 GB data disk. +# +# knots-box does this by hand: /mnt/knots_data holds ~875 GB of blockchain and +# appears nowhere in Ansible, so a rebuild of that host would come up with the +# chain silently missing. This role does it in code instead. + +- name: Check the Monero data device + ansible.builtin.stat: + path: "{{ monero_data_device }}" + register: monero_device + +- name: Find the device backing the root filesystem + ansible.builtin.command: findmnt -no SOURCE / + register: monero_root_source + changed_when: false + check_mode: false + +# A guard, not ceremony. mkfs on the wrong device is unrecoverable, and +# /dev/sdX names are assigned by the kernel at boot - the whole reason +# everything below this point works by UUID. +- name: Assert the data device is a real block device and is not the root disk + ansible.builtin.assert: + that: + - monero_device.stat.exists + - monero_device.stat.isblk | default(false) + - not monero_root_source.stdout.startswith(monero_data_device) + quiet: true + fail_msg: >- + {{ monero_data_device }} is not a usable data device, or it is the disk + the root filesystem lives on ({{ monero_root_source.stdout }}). + Refusing to touch it. Check `lsblk` on the host. + +- name: Read any existing filesystem on the data device + ansible.builtin.command: "blkid -o value -s TYPE {{ monero_data_device }}" + register: monero_existing_fs + changed_when: false + failed_when: false + check_mode: false + +# community.general.filesystem is idempotent and will not rebuild a filesystem +# that is already the requested type, but it would happily create one on a disk +# holding something it does not recognise. Say so out loud instead. +- name: Refuse to reformat a disk that already carries a different filesystem + ansible.builtin.assert: + that: + - monero_existing_fs.stdout | trim in ['', monero_data_fstype] + quiet: true + fail_msg: >- + {{ monero_data_device }} already holds a '{{ monero_existing_fs.stdout | trim }}' + filesystem, not '{{ monero_data_fstype }}'. Refusing to overwrite it. + +- name: Create the filesystem on the data device + community.general.filesystem: + fstype: "{{ monero_data_fstype }}" + dev: "{{ monero_data_device }}" + opts: "{{ monero_data_mkfs_opts }}" + +- name: Read the filesystem UUID + ansible.builtin.command: "blkid -o value -s UUID {{ monero_data_device }}" + register: monero_fs_uuid + changed_when: false + failed_when: false + check_mode: false + +# Everything past this point mounts by UUID, so there is nothing sensible to do +# without one. On a --check run against a disk that has no filesystem yet this +# is expected and unavoidable: check mode did not create the filesystem, so +# there is no UUID to read. Say that, rather than leaving a bare blkid rc=2. +- name: Assert the UUID looks like a UUID + ansible.builtin.assert: + that: + - monero_fs_uuid.stdout | trim | length == 36 + quiet: true + fail_msg: >- + No filesystem UUID on {{ monero_data_device }}. + {% if ansible_check_mode %}This is expected under --check when the disk + has not been formatted yet - check mode cannot create the filesystem, so + it cannot dry-run the mount either. Run without --check once, then + --check is meaningful from then on.{% else %}mkfs appears to have failed; + check `lsblk` and `blkid` on the host.{% endif %} + +# Same owner and mode as the "Own the mounted data directory" task below, and +# that is deliberate rather than duplication. Before the disk is mounted this +# creates the mount point; afterwards it targets the mounted filesystem and is a +# no-op. Giving it root:root 0755 here - the obvious thing to write - made the +# two tasks disagree, and each run flipped the directory back and forth. +- name: Create the Monero data directory as a mount point + ansible.builtin.file: + path: "{{ monero_data_dir }}" + state: directory + owner: "{{ monero_user }}" + group: "{{ monero_group }}" + mode: '0750' + +# By UUID, never by /dev/sdb. nofail so a missing disk does not drop a headless +# VM into an emergency shell - monerod is stopped from starting by +# RequiresMountsFor= in its unit instead, which the systemd monitor then reports. +# noatime because LMDB touches a lot of pages and none of it wants an atime write. +- name: Mount the data disk by UUID + ansible.posix.mount: + path: "{{ monero_data_dir }}" + src: "UUID={{ monero_fs_uuid.stdout | trim }}" + fstype: "{{ monero_data_fstype }}" + opts: defaults,noatime,nofail + state: mounted + +# AFTER the mount, never before: chowning the mount point first sets permissions +# on the directory the mount then hides, and the blockchain lands on a root-owned +# filesystem that monerod cannot write to. +- name: Own the mounted data directory + ansible.builtin.file: + path: "{{ monero_data_dir }}" + state: directory + owner: "{{ monero_user }}" + group: "{{ monero_group }}" + mode: '0750' + +- name: Confirm the data directory is a mount point and not the root filesystem + ansible.builtin.command: "findmnt -no SOURCE --target {{ monero_data_dir }}" + register: monero_mounted_source + changed_when: false + check_mode: false + +- name: Assert the blockchain will land on the data disk + ansible.builtin.assert: + that: + - monero_mounted_source.stdout | trim != monero_root_source.stdout | trim + quiet: true + fail_msg: >- + {{ monero_data_dir }} resolves to {{ monero_mounted_source.stdout | trim }}, + which is the root filesystem. The 500 GB disk is not mounted and a full + sync would fill the 10 GB root disk. diff --git a/ansible/roles/monero/tasks/user.yml b/ansible/roles/monero/tasks/user.yml new file mode 100644 index 0000000..269ed53 --- /dev/null +++ b/ansible/roles/monero/tasks/user.yml @@ -0,0 +1,24 @@ +--- +# The service account, created before anything that chowns to it. +# +# This is its own file because two later steps both depend on it and neither +# owns it: storage.yml chowns the mount point to this user, and install.yml +# installs a binary that runs as it. Creating it inside either one makes the +# other's ordering an accident. + +- name: Create the monero system group + group: + name: "{{ monero_group }}" + system: yes + state: present + +- name: Create the monero system user + user: + name: "{{ monero_user }}" + group: "{{ monero_group }}" + system: yes + shell: /usr/sbin/nologin + home: "{{ monero_data_dir }}" + create_home: no + comment: "Monero node daemon" + state: present diff --git a/ansible/roles/monero/templates/healthcheck.service.j2 b/ansible/roles/monero/templates/healthcheck.service.j2 new file mode 100644 index 0000000..7619581 --- /dev/null +++ b/ansible/roles/monero/templates/healthcheck.service.j2 @@ -0,0 +1,19 @@ +[Unit] +Description=Monero Node Health Check +After=network.target monerod.service + +[Service] +Type=oneshot +User=root +ExecStart=/usr/local/bin/monerod-healthcheck-push.sh +Environment=HEALTHCHECK_PUSH_URL={{ healthcheck_push_url }} +Environment=HEALTHCHECK_PUSH_TOKEN={{ healthcheck_push_token }} +Environment=MONERO_RPC_HOST={{ monero_rpc_bind_ip }} +Environment=MONERO_RPC_PORT={{ monero_rpc_port }} +Environment=MONERO_RPC_USER={{ monero_rpc_user }} +Environment=MONERO_RPC_PASSWORD={{ monero_rpc_password }} +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/ansible/roles/monero/templates/healthcheck.sh.j2 b/ansible/roles/monero/templates/healthcheck.sh.j2 new file mode 100644 index 0000000..7aebd80 --- /dev/null +++ b/ansible/roles/monero/templates/healthcheck.sh.j2 @@ -0,0 +1,70 @@ +#!/bin/bash +# Monero node health check — managed by Ansible (roles/monero) +# +# The exit code is the answer and systemd keeps it: +# systemctl is-failed monerod-healthcheck.service +# Reporting anywhere else is optional and generic. +# +# WHAT THIS DELIBERATELY DOES NOT CHECK: whether the node is synchronised. +# A full initial sync takes days. A check that required `synchronized: true` +# would report DOWN for every one of those days, which is not a monitor, it is +# an alarm with the batteries taken out. Answering RPC is the health question; +# sync progress is reported in the message so it is visible without being +# alertable. + +RPC_HOST="${MONERO_RPC_HOST:-127.0.0.1}" +RPC_PORT="${MONERO_RPC_PORT:-18081}" +RPC_USER="${MONERO_RPC_USER:-}" +RPC_PASSWORD="${MONERO_RPC_PASSWORD:-}" +PUSH_URL="${HEALTHCHECK_PUSH_URL:-}" +PUSH_TOKEN="${HEALTHCHECK_PUSH_TOKEN:-}" + +report() { + local status=$1 + local msg=$2 + + # No push URL is normal, not an error: the exit code below is still a + # complete answer for anything reading unit state. + [ -n "$PUSH_URL" ] || return 0 + + local encoded_msg="${msg// /%20}" + + # Gatus external endpoint: a POST with a bearer token. + local _ok=false + [ "${status}" = "up" ] && _ok=true + curl -s --max-time 15 --retry 2 -o /dev/null -X POST \ + -H "Authorization: Bearer ${PUSH_TOKEN}" \ + "${PUSH_URL}?success=${_ok}&error=${encoded_msg}" || true +} + +# --digest, not --user: monerod's --rpc-login is HTTP Digest auth, so curl has +# to answer the 401 challenge rather than send credentials up front. +response=$(curl -s --max-time 30 --digest -u "${RPC_USER}:${RPC_PASSWORD}" \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":"healthcheck","method":"get_info"}' \ + "http://${RPC_HOST}:${RPC_PORT}/json_rpc" 2>/dev/null) + +if [ -z "$response" ]; then + report "down" "monerod RPC not responding" + exit 1 +fi + +status=$(echo "$response" | jq -r '.result.status // empty' 2>/dev/null) + +if [ "$status" != "OK" ]; then + report "down" "monerod RPC status: ${status:-unparseable}" + exit 1 +fi + +height=$(echo "$response" | jq -r '.result.height // 0' 2>/dev/null) +target=$(echo "$response" | jq -r '.result.target_height // 0' 2>/dev/null) +synced=$(echo "$response" | jq -r '.result.synchronized // false' 2>/dev/null) + +# monerod reports target_height=0 once it has caught up, so show the height we +# actually reached rather than a confusing "of 0". +if [ "$target" = "0" ]; then + report "up" "OK height=${height} synced=${synced}" +else + report "up" "OK height=${height}/${target} synced=${synced}" +fi +exit 0 diff --git a/ansible/roles/monero/templates/healthcheck.timer.j2 b/ansible/roles/monero/templates/healthcheck.timer.j2 new file mode 100644 index 0000000..8c850a9 --- /dev/null +++ b/ansible/roles/monero/templates/healthcheck.timer.j2 @@ -0,0 +1,15 @@ +[Unit] +Description=Monero Node Health Check Timer +# Deliberately NO `Requires=monerod.service`. Requires on a timer stops the +# timer when the required unit stops - "if the thing I am watching goes down, +# stop watching it" - and leaves nothing to re-arm when the service returns. +# Fulcrum's timer carried it and sat active+enabled for seven months without +# firing once. bitcoin_knots' timer still carries it; do not copy that. + +[Timer] +OnBootSec=2min +OnUnitActiveSec=5min +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/ansible/roles/monero/templates/monerod.conf.j2 b/ansible/roles/monero/templates/monerod.conf.j2 new file mode 100644 index 0000000..9043ca8 --- /dev/null +++ b/ansible/roles/monero/templates/monerod.conf.j2 @@ -0,0 +1,46 @@ +# monerod configuration - managed by Ansible (roles/monero) +# +# Config-file syntax is the command-line options with the leading -- removed; +# valueless flags take =1. https://docs.getmonero.org/interacting/monero-config-file/ + +data-dir={{ monero_data_dir }} +log-file={{ monero_log_dir }}/monerod.log +log-level=0 +max-log-file-size=104850000 + +# --- P2P: outbound only ----------------------------------------------------- +# Nothing forwards {{ monero_p2p_port }} to this box, so the node dials out to +# peers and never receives. It syncs and serves us completely; it simply does +# not relay for strangers. no-igd follows: with nothing forwarded there is +# nothing for UPnP to do except announce us to the router. +p2p-bind-ip=0.0.0.0 +p2p-bind-port={{ monero_p2p_port }} +no-igd=1 +out-peers={{ monero_out_peers }} +in-peers={{ monero_in_peers }} + +# --- Unrestricted RPC: loopback only ---------------------------------------- +# Administration and the health check. Never leaves the box. +rpc-bind-ip={{ monero_rpc_bind_ip }} +rpc-bind-port={{ monero_rpc_port }} + +# --- Restricted RPC: the only surface anything off-box touches -------------- +# A SECOND server, view-only, reached over the tailnet by Caddy on the edge +# host. This is rpc-restricted-bind-port and deliberately NOT `restricted-rpc=1`: +# that flag restricts the loopback server too, and then we could not ask our own +# node anything useful. +# +# Bound to 0.0.0.0 and reachable only over the tailnet - Tailscale's ts-input +# chain sits ahead of UFW, so nothing on the LAN or the internet reaches this +# port without going through Caddy. confirm-external-bind is monerod's +# "yes, I meant to do that" for a non-loopback bind. +rpc-restricted-bind-ip=0.0.0.0 +rpc-restricted-bind-port={{ monero_restricted_rpc_port }} +confirm-external-bind=1 + +# HTTP Digest, and it guards BOTH servers above - core_rpc_server::init passes +# the same login to each. This is what Feather sends as the node credentials. +rpc-login={{ monero_rpc_user }}:{{ monero_rpc_password }} + +# Nothing subscribes to ZMQ here, so it is one less listener. +no-zmq=1 diff --git a/ansible/roles/monero/templates/monerod.service.j2 b/ansible/roles/monero/templates/monerod.service.j2 new file mode 100644 index 0000000..929d2ca --- /dev/null +++ b/ansible/roles/monero/templates/monerod.service.j2 @@ -0,0 +1,38 @@ +[Unit] +Description=Monero Daemon +Documentation=https://docs.getmonero.org/running-node/monerod-systemd/ +After=network-online.target +Wants=network-online.target +# The blockchain lives on a separate 500 GB disk mounted at {{ monero_data_dir }}. +# Without this line a failed mount would not stop monerod - it would start, find +# an empty directory, and begin writing a ~300 GB chain onto the 10 GB root +# filesystem. This turns that silent disaster into a unit that refuses to start, +# which the systemd monitor reports within five minutes. +RequiresMountsFor={{ monero_data_dir }} + +[Service] +Type=simple +User={{ monero_user }} +Group={{ monero_group }} +ExecStart={{ monero_bin_dir }}/monerod --config-file {{ monero_conf_dir }}/monerod.conf --non-interactive + +Restart=always +RestartSec=30 +# monerod flushes LMDB on shutdown and that is not instant on a full chain. +# Killing it mid-flush is how the database gets corrupted and resynced from zero. +TimeoutStopSec=300 + +StandardOutput=journal +StandardError=journal + +# Hardening: the daemon only ever writes to its own data and log directories. +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=read-only +ReadWritePaths={{ monero_data_dir }} {{ monero_log_dir }} + +LimitNOFILE=65535 + +[Install] +WantedBy=multi-user.target diff --git a/ansible/services/headscale/templates/acl.json.j2 b/ansible/services/headscale/templates/acl.json.j2 index 98ada80..6ab6fa5 100644 --- a/ansible/services/headscale/templates/acl.json.j2 +++ b/ansible/services/headscale/templates/acl.json.j2 @@ -42,6 +42,7 @@ "tag:fulcrum-box:50002", "tag:mempool-box:8080", "tag:memos-box:5230", + "tag:monerod-box:18089", "tag:arbret-staging-box:80", "tag:stalwart-box:25", "tag:stalwart-box:465", @@ -108,6 +109,9 @@ "tag:mempool-box": [ "{{ headscale_namespace }}@" ], + "tag:monerod-box": [ + "{{ headscale_namespace }}@" + ], "tag:nodito": [ "{{ headscale_namespace }}@" ], diff --git a/ansible/services/monero/deploy_monero_playbook.yml b/ansible/services/monero/deploy_monero_playbook.yml new file mode 100644 index 0000000..ca90693 --- /dev/null +++ b/ansible/services/monero/deploy_monero_playbook.yml @@ -0,0 +1,39 @@ +--- +# Monero: a full node, reachable from Feather over the WAN. +# +# The chain lives on a dedicated 500 GB disk mounted at /var/lib/monero; the +# role creates the filesystem and the fstab entry, and monerod's unit carries +# RequiresMountsFor so it cannot start without it. An initial sync takes days +# and nothing here touches the data directory afterwards. +# +# Exposure is one HTTPS vhost and no new open ports. See roles/monero/README.md. +- name: Deploy the Monero node + hosts: monero + become: yes + vars: + healthcheck_push_url: "https://{{ subdomains.gatus }}.{{ root_domain }}/api/v1/endpoints/probe_monerod/external" + healthcheck_push_token: "{{ gatus_push_tokens[inventory_hostname] }}" + roles: + - monero + +- name: Publish the restricted Monero RPC from the edge host + hosts: edge + become: yes + tasks: + # No basic auth here on purpose. monerod's own --rpc-login is HTTP Digest + # and guards this endpoint; Caddy's basic_auth would put a second, Basic + # challenge in front of it that Feather would answer with the wrong scheme. + # Caddy's job is TLS and routing, nothing else. + - name: Expose the restricted Monero RPC over HTTPS + ansible.builtin.include_role: + name: caddy_site + vars: + caddy_site_name: monero + caddy_site_domain: "{{ subdomains.monero }}.{{ root_domain }}" + caddy_site_upstream: "{{ hostvars['monerod_box_local'].ansible_host }}:{{ hostvars['monerod_box_local'].monero_restricted_rpc_port }}" + caddy_site_resolvers: "100.100.100.100" + + # The per-site validate cannot catch a conflict between sites. + - name: Validate the whole Caddyfile + ansible.builtin.command: caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile + changed_when: false diff --git a/ansible/site.yml b/ansible/site.yml index 35f5867..c777577 100644 --- a/ansible/site.yml +++ b/ansible/site.yml @@ -53,6 +53,7 @@ - import_playbook: services/fulcrum/deploy_fulcrum_playbook.yml - import_playbook: services/datum-gateway/deploy_datum_gateway_playbook.yml - import_playbook: services/mempool/deploy_mempool_playbook.yml +- import_playbook: services/monero/deploy_monero_playbook.yml - import_playbook: services/memos/deploy_memos_playbook.yml - import_playbook: services/forgejo-runner/deploy_forgejo_runner_playbook.yml - import_playbook: services/phoenixd/deploy_phoenixd_playbook.yml diff --git a/tofu/nodito/vms.auto.tfvars b/tofu/nodito/vms.auto.tfvars index a9ea0a5..8e50853 100644 --- a/tofu/nodito/vms.auto.tfvars +++ b/tofu/nodito/vms.auto.tfvars @@ -48,6 +48,20 @@ vms = { disk_size_gb = 30 ipconfig0 = "ip=dhcp" }, + # disk_size_gb is the ROOT disk only. monerod-box also carries a second + # 500 GB disk (scsi1) for the blockchain, attached in the Proxmox UI and + # therefore invisible here - the vm resource sets ignore_changes = all, so + # tofu neither created it nor would notice if it vanished. It is mounted at + # /var/lib/monero by ansible/roles/monero (tasks/storage.yml), by UUID, and + # monerod's unit refuses to start without it. + "monerod-box" = { + vmid = 2400 + name = "monerod-box" + cores = 4 + memory_mb = 2048 + disk_size_gb = 10 + ipconfig0 = "ip=dhcp" + }, "memos-box" = { vmid = 8100 name = "memos-box"